This multiple-image upload Magento 2 module provides advanced functionality for uploading multiple images using a single upload button and displaying an image gallery with Slick carousel and Fancybox JavaScript integration.
The module also offers features for managing the main photo image for each story, allowing for better content management and display on the website.You can find the complete module on GitHub at Magelearn_MultipleImageUpload
Or Check the images below for a better understanding of the functionality of this module.
Let's start it by creating a custom extension.
Create a folder inside app/code/Magelearn/Story
Add registration.php file in it:
<?php declare(strict_types=1); use Magento\Framework\Component\ComponentRegistrar; ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magelearn_Story', __DIR__);
Add composer.json file in it:
{ "name": "magelearn/module-story-with-image-gallery", "description": "A Magento 2 module that offers advanced functionality for uploading multiple images using a single upload button and displaying an image gallery with Fancybox JavaScript integration.", "type": "magento2-module", "license": "proprietary", "authors": [ { "name": "vijay rami", "email": "vijaymrami@gmail.com" } ], "minimum-stability": "dev", "require": {}, "autoload": { "files": [ "registration.php" ], "psr-4": { "Magelearn\\Story\\": "" } } }
Add etc/module.xml file in it:
<?xml version="1.0" ?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="Magelearn_Story" setup_version="1.0.0"> </module> </config>
In this implementation, we will be presenting the story listing and detail pages on the front end, utilizing data retrieved from our MySQL database tables.
To achieve this, we will establish two distinct tables: one for storing story metadata and another for managing the associated image gallery data for each story. We will define a foreign key relationship between the story gallery images and the corresponding story ID to ensure data integrity and facilitate efficient querying.
we will create a db_schema.xml
file within the etc
directory to define the database schema, including the structure of our tables and the relationships between them.
<?xml version="1.0" ?> <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="magelearn_story" resource="default" engine="innodb" comment="Magelearn Story Table"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="id"/> <column name="name" nullable="false" xsi:type="varchar" comment="name" length="255" default=""/> <column name="status" xsi:type="smallint" padding="6" nullable="true" comment="status" unsigned="false"/> <column xsi:type="smallint" name="position" padding="6" unsigned="false" nullable="true" identity="false" comment="Story Position"/> <column name="photo" nullable="false" xsi:type="text" length="255" comment="photo"/> <column name="description" nullable="true" xsi:type="text" length="255" comment="description"/> <column xsi:type="text" name="stores" nullable="false" comment="Stores Ids"/> <column xsi:type="varchar" name="url_key" nullable="true" length="255" comment="Url Key"/> <column xsi:type="varchar" name="meta_title" nullable="true" length="255" comment="Meta Title"/> <column xsi:type="text" name="meta_description" nullable="true" comment="Meta Description"/> <column xsi:type="text" name="meta_robots" nullable="true" comment="Meta Robots"/> <column xsi:type="varchar" name="canonical_url" nullable="true" length="255" comment="Canonical Url"/> <column name="created_at" nullable="true" xsi:type="datetime" comment="created_at"/> <column name="updated_at" nullable="true" xsi:type="datetime" comment="updated_at" on_update="true"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> <index referenceId="MAGELEARN_STORY_STATUS" indexType="btree"> <column name="status"/> </index> </table> <table name="magelearn_story_gallery" resource="default" engine="innodb" comment="Table for magelearn story gallery"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> <column name="story_id" padding="10" unsigned="true" nullable="false" xsi:type="int" comment="Story id"/> <column name="image_name" nullable="false" xsi:type="text" length="255" comment="Image Name"/> <column name="is_base" nullable="false" xsi:type="boolean" comment="Base Image Flag"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> <constraint xsi:type="foreign" referenceId="FK_MAGELEARN_STORY_GALLERY_STORY_ID" table="magelearn_story_gallery" column="story_id" referenceTable="magelearn_story" referenceColumn="id" onDelete="CASCADE"/> </table> </schema>
Next, we will create a di.xml
file within the etc
directory of our Magento module. This configuration file will define the necessary dependency injection nodes required for displaying data in the admin grid and for handling data persistence.
In the di.xml, we will specify the required virtual types to ensure that our custom data models are properly integrated with Magento's framework. This will facilitate the retrieval and manipulation of data within the admin interface, allowing for efficient management of our story and image gallery entities.
By leveraging Magento's dependency injection system, we can ensure that our module adheres to best practices and maintains compatibility with the overall architecture of the Magento.
<?xml version="1.0" ?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magelearn\Story\Api\Data\StoryInterface" type="Magelearn\Story\Model\Story"/> <preference for="Magelearn\Story\Api\Data\StorySearchResultsInterface" type="Magento\Framework\Api\SearchResults"/> <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> <arguments> <argument name="collections" xsi:type="array"> <item name="magelearn_story_listing_data_source" xsi:type="string">Magelearn\Story\Model\ResourceModel\Story\Grid\Collection</item> </argument> </arguments> </type> <virtualType name="Magelearn\Story\Model\ResourceModel\Story\Grid\Collection" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult"> <arguments> <argument name="mainTable" xsi:type="string">magelearn_story</argument> <argument name="resourceModel" xsi:type="string">Magelearn\Story\Model\ResourceModel\Story</argument> </arguments> </virtualType> <virtualType name="Magelearn\Story\Model\ImageUploader" type="Magento\Catalog\Model\ImageUploader"> <arguments> <argument name="baseTmpPath" xsi:type="const">\Magelearn\Story\Model\ImageProcessor::ML_STORY_MEDIA_TMP_PATH</argument> <argument name="basePath" xsi:type="const">\Magelearn\Story\Model\ImageProcessor::ML_STORY_MEDIA_PATH</argument> <argument name="allowedExtensions" xsi:type="array"> <item name="jpg" xsi:type="string">jpg</item> <item name="jpeg" xsi:type="string">jpeg</item> <item name="gif" xsi:type="string">gif</item> <item name="png" xsi:type="string">png</item> </argument> </arguments> </virtualType> <type name="Magelearn\Story\Controller\Adminhtml\File\Upload"> <arguments> <argument name="imageUploader" xsi:type="object">Magelearn\Story\Model\ImageUploader</argument> </arguments> </type> <type name="Magelearn\Story\Model\ImageProcessor"> <arguments> <argument name="imageUploader" xsi:type="object">Magelearn\Story\Model\ImageUploader</argument> </arguments> </type> </config>
Add etc/acl.xml file.
<?xml version="1.0" ?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> <acl> <resources> <resource id="Magento_Backend::admin"> <resource id="Magento_Backend::stores"> <resource id="Magento_Backend::stores_settings"> <resource id="Magento_Config::config"> <resource id="Magelearn_Story::config" title="Magelearn Story Page" sortOrder="192012" /> </resource> </resource> </resource> <resource id="Magelearn_Story::Story" title="Story" sortOrder="10"> <resource id="Magelearn_Story::Story_save" title="Save Story" sortOrder="10"/> <resource id="Magelearn_Story::Story_delete" title="Delete Story" sortOrder="20"/> <resource id="Magelearn_Story::Story_update" title="Update Story" sortOrder="30"/> <resource id="Magelearn_Story::Story_view" title="View Story" sortOrder="40"/> </resource> </resource> </resources> </acl> </config>
Additionally, we will include options for:
- Configuring the URL key.
- Setting the meta title and meta description for the listing page.
- Specifying the number of pages for pagination.
- Defining the character limit for descriptions.
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> <system> <tab id="magelearn" translate="label" sortOrder="400"> <label>Magelearn Modules</label> </tab> <section id="mlstory" translate="label" type="text" sortOrder="192012" showInDefault="1" showInWebsite="1" showInStore="1"> <resource>Magelearn_Story::config</resource> <class>separator-top</class> <label>Story Page Setting</label> <tab>magelearn</tab> <group id="general" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>General</label> <field id="enable_pages" translate="label" type="select" sortOrder="15" showInDefault="1" showInStore="1" showInWebsite="1"> <label>Enable Story Pages</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="label" translate="label comment" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Title</label> <tooltip>The title of the Story page will be displayed in the top (breadcrumbs) and bottom menu (footer).</tooltip> </field> <field id="add_to_toolbar_menu" translate="label comment" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Add Story Listing Link to the Toolbar</label> <tooltip>Link to the Story page will be added to the toolbar</tooltip> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="add_to_footer_menu" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Add Story Listing Link to the Footer</label> <tooltip>Link to the Story page will be added to the footer</tooltip> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> </group> <group id="story" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Story Page</label> <group id="main_settings" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Main Settings</label> <field id="url" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>URL Key</label> <validate>required-entry</validate> </field> <field id="meta_title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Meta Title</label> </field> <field id="meta_description" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Meta Description</label> </field> <field id="pagination_limit" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Number of Stories on a Page</label> <validate>validate-number-range</validate> </field> <field id="description_limit" translate="label" type="text" sortOrder="45" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Description Length Limit</label> </field> </group> </group> </section> </system> </config>
And to provide the default values add etc/config.xml file.
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> <default> <mlstory> <general> <enable_pages>1</enable_pages> <enable_link>1</enable_link> <linktext>Available in Story</linktext> <new_page>1</new_page> <label>Story Listing</label> </general> <story> <main_settings> <meta_title>Story Teller</meta_title> <pagination_limit>5</pagination_limit> <url>mlstory</url> </main_settings> </story> </mlstory> </default> </config>
Now we will provide admin menu and routes links to redirect to the story listing page.
Add etc/adminhtml/menu.xml
<?xml version="1.0" ?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> <add id="Magelearn_Story::story" title="Magelearn Story" module="Magelearn_Story" parent="Magento_Backend::content" sortOrder="110" resource="Magelearn_Story::story" /> <add id="Magelearn_Story::magelearn_story" title="Story" module="Magelearn_Story" sortOrder="10" resource="Magelearn_Story::magelearn_story" parent="Magelearn_Story::story" action="magelearn_story/story/index" /> </menu> </config>
Add etc/adminhtml/routes.xml
<?xml version="1.0" ?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> <router id="admin"> <route frontName="magelearn_story" id="magelearn_story"> <module before="Magento_Backend" name="Magelearn_Story"/> </route> </router> </config>
We will now proceed to create the essential files required for backend management in our Magento module.
Our development will follow the specifications outlined in the etc/di.xml configuration file, ensuring that we adhere to the defined dependency injection nodes.
Create Api/Data/StoryInterface.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Api\Data; interface StoryInterface { public const ID = 'id'; public const NAME = 'name'; public const STATUS = 'status'; public const POSITION = 'position'; public const DESCRIPTION = 'description'; public const PHOTO = 'photo'; public const STORES = 'stores'; public const URL_KEY = 'url_key'; public const META_TITLE = 'meta_title'; public const META_DESCRIPTION = 'meta_description'; public const META_ROBOTS = 'meta_robots'; public const CANONICAL_URL = 'canonical_url'; public const CREATED_AT = 'created_at'; public const UPDATED_AT = 'updated_at'; public const STATUS_ENABLED = 1; public const STATUS_DISABLED = 0; /** * Get id * @return string|null */ public function getId(); /** * @param int $id * @return $this */ public function setId($id); /** * @return string|null */ public function getName(): ?string; /** * @param string|null $name * @return void */ public function setName(?string $name): void; /** * @return Int|null */ public function getStatus(): ?Int; /** * @param Int|null $status * @return void */ public function setStatus(?Int $status): void; /** * @return string|null */ public function getPosition(): ?string; /** * @param string|null $position * @return void */ public function setPosition(?string $position): void; /** * Get photo * @return string|null */ public function getPhoto(): ?string; /** * @param string|null $photo * @return void */ public function setPhoto(?string $photo): void; /** * @return string|null */ public function getDescription(): ?string; /** * @param string|null $description * @return void */ public function setDescription(?string $description): void; /** * @return string|null */ public function getStores(): ?string; /** * @param string|null $stores * @return void */ public function setStores(?string $stores): void; /** * @return string|null */ public function getUrlKey(): ?string; /** * @param string|null $urlKey * @return void */ public function setUrlKey(?string $urlKey): void; /** * @return string|null */ public function getMetaTitle(): ?string; /** * @param string|null $metaTitle * @return void */ public function setMetaTitle(?string $metaTitle): void; /** * @return string|null */ public function getMetaDescription(): ?string; /** * @param string|null $metaDescription * @return void */ public function setMetaDescription(?string $metaDescription): void; /** * @return string|null */ public function getMetaRobots(): ?string; /** * @param string|null $metaRobots * @return void */ public function setMetaRobots(?string $metaRobots): void; /** * @return string|null */ public function getCanonicalUrl(): ?string; /** * @param string|null $canonicalUrl * @return void */ public function setCanonicalUrl(?string $canonicalUrl): void; /** * Get created_at * @return string|null */ public function getCreatedAt(): ?string; /** * Set created_at * @param string $createdAt * @return void */ public function setCreatedAt(?string $createdAt): void; /** * Get updated_at * @return string|null */ public function getUpdatedAt(): ?string; /** * Set updated_at * @param string $updatedAt * @return void */ public function setUpdatedAt(?string $updatedAt): void; }
Create Api/Data/StorySearchResultsInterface.php file.
<?php /** * Copyright © All rights reserved. * See COPYING.txt for license details. */ declare(strict_types=1); namespace Magelearn\Story\Api\Data; interface StorySearchResultsInterface extends \Magento\Framework\Api\SearchResultsInterface { /** * Get question list. * @return \Magelearn\Story\Api\Data\StoryInterface[] */ public function getItems(); /** * Set id list. * @param \Magelearn\Story\Api\Data\StoryInterface[] $items * @return $this */ public function setItems(array $items); }Create Model/Story.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Model; use Magelearn\Story\Api\Data\StoryInterface; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Exception\NoSuchEntityException; class Story extends AbstractModel implements StoryInterface { public const CACHE_TAG = 'mlstory_story'; /** * @var ImageProcessor */ private $imageProcessor; /** * @var \Magento\Cms\Model\Template\FilterProvider */ private $filterProvider; /** * @var string */ protected $_cacheTag = self::CACHE_TAG; /** * @var ConfigHtmlConverter */ private $configHtmlConverter; public function __construct( \Magento\Framework\Model\Context $context, \Magento\Framework\Registry $registry, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, ImageProcessor $imageProcessor, \Magento\Cms\Model\Template\FilterProvider $filterProvider, ConfigHtmlConverter $configHtmlConverter, \Magelearn\Story\Model\ResourceModel\Story $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [] ) { $this->imageProcessor = $imageProcessor; parent::__construct( $context, $registry, $resource, $resourceCollection, $data ); $this->filterProvider = $filterProvider; $this->configHtmlConverter = $configHtmlConverter; } /** * @inheritDoc */ public function _construct() { $this->_init(\Magelearn\Story\Model\ResourceModel\Story::class); } /** * Get story associated store Ids * Note: Story can be for All Store View (sore_ids = array(0 => '0')) * * @return array * @throws \Magento\Framework\Exception\LocalizedException */ public function getStoreIds() { $storesArray = explode(',', $this->_getData('stores')); return array_filter($storesArray); } /** * Get story associated website Ids * Note: Story can be for All Store View (sore_ids = array(0)) * * @return array * @throws NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException */ public function getWebsiteIds() { if (!$this->hasWebsiteIds()) { $stores = $this->getStoreIds(); $websiteIds = []; foreach ($stores as $storeId) { $websiteIds[] = $this->storeManager->getStore($storeId)->getWebsiteId(); } $this->setData('website_ids', array_unique($websiteIds)); } return $this->_getData('website_ids'); } /** * Set templates html */ public function setTemplatesHtml() { $this->configHtmlConverter->setHtml($this); } /** * @return string */ public function getPhotoMediaUrl() { if ($this->getPhotoImg()) { return $this->imageProcessor->getImageUrl( [ImageProcessor::ML_STORY_MEDIA_PATH, $this->getId(), $this->getPhotoImg()] ); } } /** * Optimized get data method * * @return array */ public function getFrontendData(): array { $result = [ 'id' => (int)$this->getDataByKey('id') ]; if ($this->getDataByKey('photo_url')) { $result['photo_url'] = $this->getDataByKey('photo_url'); } return $result; } /** * Generate SEO-friendly URL for the story * * @param bool $canonical Whether to return canonical URL * @return string */ public function getStoryUrl($canonical = false) { // If canonical URL is set and not empty, return it if ($canonical && $this->getCanonicalUrl()) { return $this->getCanonicalUrl(); } // Generate SEO-friendly URL using url_key $urlKey = $this->getUrlKey(); // Fallback to ID if no URL key exists if (!$urlKey) { $urlKey = 'story-' . $this->getId(); } // Ensure URL key is URL-friendly $urlKey = $this->sanitizeUrlKey($urlKey); return 'story/view/' . $urlKey; } /** * Sanitize URL key to make it URL-friendly * * @param string $urlKey * @return string */ private function sanitizeUrlKey($urlKey) { // Convert to lowercase $urlKey = strtolower($urlKey); // Replace non-alphanumeric characters with hyphens $urlKey = preg_replace('/[^a-z0-9-]/', '-', $urlKey); // Remove multiple consecutive hyphens $urlKey = preg_replace('/-+/', '-', $urlKey); // Trim hyphens from beginning and end $urlKey = trim($urlKey, '-'); return $urlKey; } public function activate() { $this->setStatus(1); $this->setData('massAction', true); $this->save(); return $this; } public function inactivate() { $this->setStatus(0); $this->setData('massAction', true); $this->save(); return $this; } /** * @inheritDoc */ public function getId() { return $this->getData(self::ID); } /** * @inheritDoc */ public function setId($id) { return $this->setData(self::ID, $id); } /** * @return string|null */ public function getName(): ?string { return $this->getData(self::NAME); } /** * @param string|null $name */ public function setName(?string $name): void { $this->setData(self::NAME, $name); } /** * @return Int|null */ public function getStatus(): ?Int { return $this->getData(self::STATUS); } /** * @param Int|null $status */ public function setStatus(?Int $status): void { $this->setData(self::STATUS, $status); } /** * @return string|null */ public function getPosition(): ?string { return $this->getData(self::POSITION); } /** * @param string|null $position */ public function setPosition(?string $position): void { $this->setData(self::POSITION, $position); } /** * @return string|null */ public function getPhoto(): ?string { return $this->getData(self::PHOTO); } /** * @param string|null $photo */ public function setPhoto(?string $photo): void { $this->setData(self::PHOTO, $photo); } /** * @return string|null */ public function getDescription(): ?string { return $this->getData(self::DESCRIPTION); } /** * @param string|null $description */ public function setDescription(?string $description): void { $this->setData(self::DESCRIPTION, $description); } /** * @return string|null */ public function getStores(): ?string { return $this->getData(self::STORES); } /** * @param string|null $stores */ public function setStores(?string $stores): void { $this->setData(self::STORES, $stores); } /** * @return string|null */ public function getUrlKey(): ?string { return $this->getData(self::URL_KEY); } /** * @param string|null $urlKey */ public function setUrlKey(?string $urlKey): void { $this->setData(self::URL_KEY, $urlKey); } /** * @return string|null */ public function getMetaTitle(): ?string { return $this->getData(self::META_TITLE); } /** * @param string|null $metaTitle */ public function setMetaTitle(?string $metaTitle): void { $this->setData(self::META_TITLE, $metaTitle); } /** * @return string|null */ public function getMetaDescription(): ?string { return $this->getData(self::META_DESCRIPTION); } /** * @param string|null $metaDescription */ public function setMetaDescription(?string $metaDescription): void { $this->setData(self::META_DESCRIPTION, $metaDescription); } /** * @return string|null */ public function getMetaRobots(): ?string { return $this->getData(self::META_ROBOTS); } /** * @param string|null $metaRobots */ public function setMetaRobots(?string $metaRobots): void { $this->setData(self::META_ROBOTS, $metaRobots); } /** * @return string|null */ public function getCanonicalUrl(): ?string { return $this->getData(self::CANONICAL_URL); } /** * @param string|null $canonicalUrl */ public function setCanonicalUrl(?string $canonicalUrl): void { $this->setData(self::CANONICAL_URL, $canonicalUrl); } /** * @return string|null */ public function getCreatedAt(): ?string { return $this->getData(self::CREATED_AT); } /** * @param string|null $createdAt */ public function setCreatedAt(?string $createdAt): void { $this->setData(self::CREATED_AT, $createdAt); } /** * @return string|null */ public function getUpdatedAt(): ?string { return $this->getData(self::UPDATED_AT); } /** * @param string|null $updatedAt */ public function setUpdatedAt(?string $updatedAt): void { $this->setData(self::UPDATED_AT, $updatedAt); } }
<?php declare(strict_types=1); namespace Magelearn\Story\Model; use Magento\Framework\App\Filesystem\DirectoryList; class ImageProcessor { /** * Story area inside media folder */ public const ML_STORY_MEDIA_PATH = 'magelearn/story'; /** * Story temporary area inside media folder */ public const ML_STORY_MEDIA_TMP_PATH = 'magelearn/story/tmp'; /** * Gallery area inside media folder */ public const ML_STORY_GALLERIES_MEDIA_PATH = 'magelearn/story/galleries'; /** * Gallery temporary area inside media folder */ public const ML_STORY_GALLERIES_MEDIA_TMP_PATH = 'magelearn/story/galleries/tmp'; /** * Type image option photo */ public const PHOTO_IMAGE_TYPE = 'photo'; /** * Type image option gallery_image */ public const GALLERY_IMAGE_TYPE = 'gallery_image'; /** * @var \Magento\Catalog\Model\ImageUploader */ private $imageUploader; /** * @var \Magento\Framework\ImageFactory */ private $imageFactory; /** * @var \Magento\Store\Model\StoreManagerInterface */ private $storeManager; /** * @var \Magento\Framework\Filesystem\Directory\WriteInterface */ private $mediaDirectory; /** * @var \Magento\Framework\Filesystem */ private $filesystem; /** * @var \Magento\Framework\Message\ManagerInterface */ private $messageManager; /** * @var \Psr\Log\LoggerInterface */ private $logger; /** * @var array */ protected $allowedExtensions = ['jpg', 'jpeg', 'gif', 'png']; public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Catalog\Model\ImageUploader $imageUploader, \Magento\Framework\ImageFactory $imageFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Message\ManagerInterface $messageManager, \Psr\Log\LoggerInterface $logger ) { $this->filesystem = $filesystem; $this->imageUploader = $imageUploader; $this->imageFactory = $imageFactory; $this->storeManager = $storeManager; $this->messageManager = $messageManager; $this->logger = $logger; } /** * @return \Magento\Framework\Filesystem\Directory\WriteInterface */ private function getMediaDirectory() { if ($this->mediaDirectory === null) { $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); } return $this->mediaDirectory; } /** * @param string $imageName * * @return string */ public function getImageRelativePath($imageName) { return $this->imageUploader->getBasePath() . DIRECTORY_SEPARATOR . $imageName; } /** * @param array $params * * @return array */ private function getFileMediaPath($params) { return $this->getMediaDirectory()->stat(implode(DIRECTORY_SEPARATOR, $params)); } /** * @param array $params * * @return string */ public function getImageSize($params) { $fileHandler = $this->getFileMediaPath($params); return $fileHandler['size'] ?? 0; } /** * * @return string */ public function getMediaUrl() { return $this->storeManager ->getStore() ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA); } /** * @param array $params * * @return string */ public function getImageUrl($params = []) { return $this->getMediaUrl() . implode(DIRECTORY_SEPARATOR, $params); } /** * Move file from temporary directory * * @param string $imageName * @param string $imageType * @param int $storyId * @param bool $storyIsNew */ public function processImage($imageName, $imageType, $storyId, $storyIsNew) { $this->setBasePaths($imageType, $storyId, $storyIsNew); $this->imageUploader->moveFileFromTmp($imageName, true); $filename = $this->getMediaDirectory()->getAbsolutePath($this->getImageRelativePath($imageName)); try { $this->prepareImage($filename, $imageType); } catch (\Exception $e) { $errorMessage = $e->getMessage(); $this->messageManager->addErrorMessage( __($errorMessage) ); $this->logger->critical($e); } } /** * @param string $filename * @param string $imageType * @param bool $needResize */ public function prepareImage($filename, $imageType, $needResize = false) { /** @var \Magento\Framework\Image $imageProcessor */ $imageProcessor = $this->imageFactory->create(['fileName' => $filename]); $imageProcessor->keepAspectRatio(true); $imageProcessor->keepFrame(true); $imageProcessor->keepTransparency(true); /*if ($imageType == self::PHOTO_IMAGE_TYPE || $needResize) { $imageProcessor->resize(27, 43); }*/ $imageProcessor->save(); } /** * @param string $imageName */ public function deleteImage($imageName) { if ($imageName && strpos($imageName, '.') !== false) { $this->getMediaDirectory()->delete( $this->getImageRelativePath($imageName) ); } } /** * @param string $imageType * @param int $storyId * @param bool $storyIsNew */ public function setBasePaths($imageType, $storyId, $storyIsNew) { // if story doesn't exist, we set 0 to tmp path $tmpStoryId = $storyIsNew ? 0 : $storyId; $tmpPath = ImageProcessor::ML_STORY_MEDIA_TMP_PATH . DIRECTORY_SEPARATOR . $tmpStoryId; $this->imageUploader->setBaseTmpPath( $tmpPath ); switch ($imageType) { case ImageProcessor::PHOTO_IMAGE_TYPE: $this->imageUploader->setBasePath( ImageProcessor::ML_STORY_MEDIA_PATH . DIRECTORY_SEPARATOR . $storyId ); break; case ImageProcessor::GALLERY_IMAGE_TYPE: $this->imageUploader->setBasePath( ImageProcessor::ML_STORY_GALLERIES_MEDIA_PATH . DIRECTORY_SEPARATOR . $storyId ); break; } } }
<?php declare(strict_types=1); namespace Magelearn\Story\Model; use Magento\Cms\Model\Template\FilterProvider; use Magento\Directory\Model\CountryFactory; use Magento\Directory\Model\RegionFactory; use Magento\Framework\Escaper; use Magento\Framework\UrlInterface; use Psr\Log\LoggerInterface; class ConfigHtmlConverter { /** * @var ConfigProvider */ private $configProvider; /** * @var Escaper */ private $escaper; /** * @var FilterProvider */ private $filterProvider; /** * @var CountryFactory */ private $countryFactory; /** * @var RegionFactory */ private $regionFactory; /** * @var Story */ private $story; /** * @var UrlInterface */ private $urlBuilder; /** * @var BaseImageStory */ private $baseImageStory; /** * @var LoggerInterface */ private $logger; /** * @var \Magelearn\Story\Helper\Data */ private $dataHelper; /** * @var array */ private $countryNameByCode = []; /** * @var array */ private $stateNameByCode = []; public function __construct( ConfigProvider $configProvider, Escaper $escaper, FilterProvider $filterProvider, LoggerInterface $logger, CountryFactory $countryFactory, RegionFactory $regionFactory, UrlInterface $urlBuilder, BaseImageStory $baseImageStory, \Magelearn\Story\Helper\Data $dataHelper ) { $this->configProvider = $configProvider; $this->escaper = $escaper; $this->filterProvider = $filterProvider; $this->countryFactory = $countryFactory; $this->regionFactory = $regionFactory; $this->urlBuilder = $urlBuilder; $this->baseImageStory = $baseImageStory; $this->logger = $logger; $this->dataHelper = $dataHelper; } /** * @param Story $story */ public function setHtml($story) { $this->story = $story; $this->story->setPhotoUrl($this->baseImageStory->getMainImageUrl($story)); $this->story->setPhoto($this->baseImageStory->getPhotoImageUrl($story)); } }
And as per highlighted code above add Model/BaseImageStory.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Model; class BaseImageStory { /** * @var ImageProcessor */ private $imageProcessor; public function __construct( ImageProcessor $imageProcessor ) { $this->imageProcessor = $imageProcessor; } /** * @param \Magelearn\Story\Model\Story $story * * @return string */ public function getMainImageUrl($story) { $baseImage = $story->getMainImageName(); if ($baseImage) { return $this->imageProcessor->getImageUrl( [ImageProcessor::ML_STORY_GALLERIES_MEDIA_PATH, $story->getId(), $baseImage] ); } return ''; } /** * @param \Magelearn\Story\Model\Story $story * * @return string */ public function getPhotoImageUrl($story) { $photoImage = $story->getPhoto(); if ($photoImage) { return $this->imageProcessor->getImageUrl( [ImageProcessor::ML_STORY_MEDIA_PATH, $story->getId(), $photoImage] ); } return ''; } }
<?php declare(strict_types=1); namespace Magelearn\Story\Model\ResourceModel; use Magento\Framework\Model\AbstractModel; use Magento\Framework\DB\Select; use Magelearn\Story\Model\ResourceModel\Gallery\Collection as GalleryCollection; use Magelearn\Story\Model\GalleryFactory; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Store\Model\StoreManagerInterface; class Story extends AbstractDb { public const TABLE_NAME = 'magelearn_story'; /** * @var GalleryCollection */ private $galleryCollection; /** * @var Gallery */ private $galleryResource; /** * @var GalleryFactory */ private $galleryFactory; /** * @var \Magelearn\Story\Model\ImageProcessor */ private $imageProcessor; /** * @var StoreManagerInterface */ private $storeManager; public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magelearn\Story\Model\ImageProcessor $imageProcessor, GalleryCollection $galleryCollection, GalleryFactory $galleryFactory, Gallery $galleryResource, StoreManagerInterface $storeManager, $connectionName = null ) { parent::__construct($context, $connectionName); $this->imageProcessor = $imageProcessor; $this->galleryCollection = $galleryCollection; $this->galleryFactory = $galleryFactory; $this->galleryResource = $galleryResource; $this->storeManager = $storeManager; } /** * @inheritDoc */ protected function _construct() { $this->_init(self::TABLE_NAME, 'id'); } /** * Perform actions before object save * @param AbstractModel|\Magento\Framework\DataObject $object * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _beforeSave(AbstractModel $object) { if (($object->getOrigData('photo') && $object->getOrigData('photo') != $object->getPhoto())) { $this->imageProcessor->deleteImage($object->getOrigData('photo')); $object->setPhoto($object->getPhoto() ? $object->getPhoto() : ''); } return $this; } protected function _beforeDelete(AbstractModel $object) { //remove story images $allImages = $this->galleryCollection->getImagesByStory($object->getId()); foreach ($allImages as $image) { $this->galleryResource->delete($image); } //remove photo image if ($photoImg = $object->getPhoto()) { $this->imageProcessor->setBasePaths( \Magelearn\Story\Model\ImageProcessor::PHOTO_IMAGE_TYPE, $object->getId(), $object->isObjectNew() ); $this->imageProcessor->deleteImage($photoImg); } } protected function _afterSave(AbstractModel $object) { $data = $object->getData(); if ($object->getPhoto() && ($image = $object->getData('photo')) && $object->getOrigData('photo') != $object->getPhoto() ) { $this->imageProcessor->processImage( $object->getPhoto(), \Magelearn\Story\Model\ImageProcessor::PHOTO_IMAGE_TYPE, $object->getId(), $object->isObjectNew() ); } if (!($object->getData('inlineEdit') || $object->getData('massAction'))) { $this->saveGallery($object->getData(), $object->isObjectNew()); } $this->_isPkAutoIncrement = true; } private function saveGallery($data, $isObjectNew = false) { $storyId = $data['id']; $allImages = $this->galleryCollection->getImagesByStory($storyId); $baseImgName = isset($data['base_img']) ? $data['base_img'] : ''; if (!isset($data['gallery_image'])) { foreach ($allImages as $image) { $this->galleryResource->delete($image); } return; } $galleryImages = $data['gallery_image']; $imagesOfStory = []; $isImport = false; foreach ($allImages as $image) { $imagesOfStory[$image->getData('image_name')] = $image; } foreach ($galleryImages as $galleryImage) { $isImageNew = isset($galleryImage['tmp_name']); if (array_key_exists($galleryImage['name'], $imagesOfStory)) { unset($imagesOfStory[$galleryImage['name']]); if ($isImageNew) { continue; } } if ($isImageNew && isset($galleryImage['name'])) { $isImport = true; $newImage = $this->galleryFactory->create(); $newImage->addData( [ 'story_id' => $storyId, 'image_name' => $galleryImage['name'], 'is_base' => $baseImgName === $galleryImage['name'], 'story_is_new' => $isObjectNew ] ); $this->galleryResource->save($newImage); } } if (!empty($galleryImages) && !$isImport) { foreach ($imagesOfStory as $imageToDelete) { $this->galleryResource->delete($imageToDelete); } } $baseImg = $this->galleryCollection->getByNameAndStory($storyId, $baseImgName); if (!empty($baseImg->getData())) { foreach ($allImages as $image) { if ($image->getData('is_base') == true) { $image->addData(['is_base' => false]); $this->galleryResource->save($image); } } $baseImg->addData(['is_base' => true]); $this->galleryResource->save($baseImg); } } /** * Set _isPkAutoIncrement for saving new story */ public function setResourceFlags() { $this->_isPkAutoIncrement = false; } /** * @param string $urlKey * @param array $storeIds * * @return int */ public function matchStoryUrl($urlKey, $storeIds) { $where = []; foreach ($storeIds as $storeId) { $where[] = 'FIND_IN_SET("' . (int)$storeId . '", `stores`)'; } $where = implode(' OR ', $where); $select = $this->getConnection()->select() ->from(['stories' => $this->getMainTable()]) ->where('stories.url_key = ?', $urlKey) ->where($where) ->reset(Select::COLUMNS) ->columns('stories.id'); return (int)$this->getConnection()->fetchOne($select); } /** * Retrieve select object for load object data * * @param string $field * @param mixed $value * @param AbstractModel $object * @return Select * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _getLoadSelect($field, $value, $object) { $select = parent::_getLoadSelect($field, $value, $object); return $select; } }
Create Model/ResourceModel/Story/Collection.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Model\ResourceModel\Story; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magelearn\Story\Api\Data\StoryInterface; use Magelearn\Story\Model\ResourceModel\Gallery; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\DB\Select; use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; use Magento\Framework\Data\Collection\EntityFactoryInterface; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\HTTP\PhpEnvironment\Request; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Registry; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface; class Collection extends AbstractCollection { /** * @var StoreManagerInterface */ protected $storeManager; /** * @var Registry */ protected $coreRegistry; /** * @var ScopeConfigInterface */ protected $scopeConfig; /** * @var RequestInterface */ protected $request; /** * @var Request */ protected $httpRequest; public function __construct( EntityFactoryInterface $entityFactory, LoggerInterface $logger, FetchStrategyInterface $fetchStrategy, ManagerInterface $eventManager, StoreManagerInterface $storeManager, RequestInterface $request, Registry $registry, ScopeConfigInterface $scope, Request $httpRequest, AdapterInterface $connection = null, AbstractDb $resource = null ) { $this->storeManager = $storeManager; $this->request = $request; $this->coreRegistry = $registry; $this->scopeConfig = $scope; $this->httpRequest = $httpRequest; parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource); } /** * @inheritDoc */ protected function _construct() { parent::_construct(); $this->_init( \Magelearn\Story\Model\Story::class, \Magelearn\Story\Model\ResourceModel\Story::class ); $this->_setIdFieldName($this->getResource()->getIdFieldName()); } /** * Apply filters to story collection * * @throws NoSuchEntityException */ public function applyDefaultFilters() { $store = $this->storeManager->getStore(true)->getId(); $select = $this->getSelect(); if (!$this->storeManager->isSingleStoreMode()) { $this->addFilterByStores([Store::DEFAULT_STORE_ID, $store]); } $select->where('main_table.status = 1'); $select->order('main_table.position ASC'); $select->order('main_table.id ASC'); $select->order(sprintf('main_table.id %s', Select::SQL_ASC)); } public function load($printQuery = false, $logQuery = false) { parent::load($printQuery, $logQuery); return $this; } /** * Get SQL for get record count * * @return Select $countSelect */ public function getSelectCountSql() { $select = parent::getSelectCountSql(); $select->reset(Select::COLUMNS); $columns = array_merge($select->getPart(Select::COLUMNS), $this->getSelect()->getPart(Select::COLUMNS)); $select->setPart(Select::COLUMNS, $columns); $countSelect = $this->getConnection()->select() ->from($select) ->reset(Select::COLUMNS) ->columns(new \Zend_Db_Expr(("COUNT(*)"))); return $countSelect; } /** * @param array $storeIds * @return Select */ public function addFilterByStores($storeIds) { $where = []; foreach ($storeIds as $storeId) { $where[] = 'FIND_IN_SET("' . (int)$storeId . '", `main_table`.`stores`)'; } $where = implode(' OR ', $where); return $this->getSelect()->where($where); } /** * Get story data * * @return array $storyArray */ public function getStoryData() { $storyArray = []; foreach ($this->getItems() as $story) { /** @var \Magelearn\Story\Model\Story $story */ $story = $story->getData(); $storyArray[] = $story; } return $storyArray; } /** * Get Base Image * * @return $this */ public function joinMainImage() { $fromPart = $this->getSelect()->getPart(Select::FROM); if (isset($fromPart['img'])) { return $this; } $this->getSelect()->joinLeft( ['img' => $this->getTable(Gallery::TABLE_NAME)], 'main_table.id = img.story_id AND img.is_base = 1', ['main_image_name' => 'img.image_name'] ); return $this; } /** * @return array */ public function getAllIds() { return \Magento\Framework\Data\Collection::getAllIds(); } public function getIdsOnPage(): array { $idsSelect = clone $this->getSelect(); $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); $idsSelect->columns($this->getResource()->getIdFieldName(), 'main_table'); return $this->getConnection()->fetchCol($idsSelect, $this->_bindParams); } }
Similarly, we will create our Model, Resource Model and Collection files for Gallery.
Add Model/Gallery.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Model; use Magento\Framework\Model\AbstractModel; /** * Class Gallery */ class Gallery extends AbstractModel { public function _construct() { $this->_init(ResourceModel\Gallery::class); } }
Add Model/ResourceModel/Gallery.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Model\ResourceModel; use Magelearn\Story\Model\ImageProcessor; use Magento\Directory\Model\Region; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Io\File; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\ResourceModel\Db\Context; class Gallery extends AbstractDb { public const TABLE_NAME = 'magelearn_story_gallery'; /** * @var ImageProcessor */ private $imageProcessor; /** * @var Region */ private $region; /** * @var Filesystem */ private $filesystem; /** * @var File */ private $ioFile; public function __construct( ImageProcessor $imageProcessor, Region $region, Filesystem $filesystem, File $ioFile, Context $context, $connectionName = null ) { parent::__construct($context, $connectionName); $this->imageProcessor = $imageProcessor; $this->region = $region; $this->filesystem = $filesystem; $this->ioFile = $ioFile; } public function _construct() { $this->_init(self::TABLE_NAME, 'id'); } protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) { $data = $object->getData(); if (isset($data['image_name']) && $object->isObjectNew()) { $this->imageProcessor->processImage( $data['image_name'], ImageProcessor::GALLERY_IMAGE_TYPE, $object->getStoryId(), $object->getStoryIsNew() ); } } protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object) { $data = $object->getData(); if (isset($data['image_name'])) { $this->imageProcessor->setBasePaths( ImageProcessor::GALLERY_IMAGE_TYPE, $object->getStoryId(), $object->getStoryIsNew() ); $this->imageProcessor->deleteImage($data['image_name']); } } }
Add Model/ResourceModel/Gallery/Collection.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Model\ResourceModel\Gallery; use Magelearn\Story\Model\Gallery; use Magelearn\Story\Model\ResourceModel\Gallery as GalleryResource; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; /** * @method Gallery[] getItems() */ class Collection extends AbstractCollection { /** * @var CollectionFactory */ private $factory; public function __construct( \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory, \Psr\Log\LoggerInterface $logger, \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, \Magento\Framework\Event\ManagerInterface $eventManager, CollectionFactory $factory, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null ) { parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource); $this->factory = $factory; } /** * @throws \Magento\Framework\Exception\LocalizedException */ public function _construct() { $this->_init(Gallery::class, GalleryResource::class); $this->_setIdFieldName($this->getResource()->getIdFieldName()); } public function getImagesByStory($storyId) { /** @var Collection $imagesCollection */ $imagesCollection = $this->factory->create(); /** @var Gallery[] $images */ $images = $imagesCollection->addFieldToFilter('story_id', $storyId)->getItems(); return $images; } public function getByNameAndStory($storyId, $name) { /** @var Collection $imagesCollection */ $imagesCollection = $this->factory->create(); /** @var Gallery $image */ $image = $imagesCollection ->addFieldToFilter('story_id', $storyId) ->addFieldToFilter('image_name', $name) ->getFirstItem(); return $image; } /** * @param string $storyId * * @return \Magelearn\Story\Model\Gallery */ public function getBaseStoryImage($storyId) { $imagesCollection = $this->factory->create(); $imagesCollection ->addFieldToFilter('story_id', $storyId) ->addFieldToFilter('is_base', 1) ->addOrder('is_base'); return $imagesCollection->getFirstItem(); } }
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml\File; use Magento\Framework\Controller\ResultFactory; use Magelearn\Story\Model\ImageProcessor; use Magento\Backend\App\Action; use Magento\Catalog\Model\ImageUploader; /** * Class Upload */ class Upload extends Action { /** * @var ImageUploader */ private $imageUploader; public function __construct( Action\Context $context, ImageUploader $imageUploader ) { parent::__construct($context); $this->imageUploader = $imageUploader; } /** * Upload file controller action. * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { try { $imageType = $this->getRequest()->getParam('type'); $storyId = (int)$this->getRequest()->getParam('id'); $this->imageUploader->setBaseTmpPath( ImageProcessor::ML_STORY_MEDIA_TMP_PATH . DIRECTORY_SEPARATOR . $storyId ); $result = $this->imageUploader->saveFileToTmpDir($imageType); $result['cookie'] = [ 'name' => $this->_getSession()->getName(), 'value' => $this->_getSession()->getSessionId(), 'lifetime' => $this->_getSession()->getCookieLifetime(), 'path' => $this->_getSession()->getCookiePath(), 'domain' => $this->_getSession()->getCookieDomain(), ]; } catch (\Exception $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); } }
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml\Story; class Index extends \Magento\Backend\App\Action { protected $resultPageFactory; /** * 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 ) { $this->resultPageFactory = $resultPageFactory; parent::__construct($context); } /** * Index action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { $resultPage = $this->resultPageFactory->create(); $resultPage->getConfig()->getTitle()->prepend(__("Story")); return $resultPage; } }
Create view/adminhtml/layout/magelearn_story_story_index.xml file.
<?xml version="1.0" ?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="styles"/> <body> <referenceContainer name="content"> <uiComponent name="magelearn_story_listing"/> </referenceContainer> </body> </page>
Now as per highlighted, code above add view/adminhtml/ui_component/magelearn_story_listing.xml file.
<?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"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="provider" xsi:type="string">magelearn_story_listing.magelearn_story_listing_data_source</item> <item name="deps" xsi:type="string">magelearn_story_listing.magelearn_story_listing_data_source</item> </item> <item name="spinner" xsi:type="string">magelearn_story_columns</item> <item name="buttons" xsi:type="array"> <item name="add" xsi:type="array"> <item name="name" xsi:type="string">add</item> <item name="label" xsi:type="string" translate="true">Add New</item> <item name="class" xsi:type="string">primary</item> <item name="url" xsi:type="string">*/*/new</item> </item> </item> </argument> <dataSource name="magelearn_story_listing_data_source"> <argument name="dataProvider" xsi:type="configurableObject"> <argument name="class" xsi:type="string">Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider</argument> <argument name="name" xsi:type="string">magelearn_story_listing_data_source</argument> <argument name="primaryFieldName" xsi:type="string">id</argument> <argument name="requestFieldName" xsi:type="string">id</argument> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item> <item name="update_url" xsi:type="url" path="mui/index/render"/> <item name="storageConfig" xsi:type="array"> <item name="indexField" xsi:type="string">id</item> </item> </item> </argument> </argument> </dataSource> <listingToolbar name="listing_top"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="sticky" xsi:type="boolean">true</item> </item> </argument> <bookmark name="bookmarks"/> <columnsControls name="columns_controls"/> <filters name="listing_filters"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="templates" xsi:type="array"> <item name="filters" xsi:type="array"> <item name="select" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</item> <item name="template" xsi:type="string">ui/grid/filters/elements/ui-select</item> </item> </item> </item> </item> </argument> </filters> <massaction name="listing_massaction"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/grid/tree-massactions</item> <item name="indexField" xsi:type="string">id</item> </item> </argument> <action name="activate"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="type" xsi:type="string">massActivate</item> <item name="label" xsi:type="string" translate="true">Enable</item> <item name="url" xsi:type="url" path="*/*/massAction"> <param name="_current">1</param> <param name="action">activate</param> </item> </item> </argument> </action> <action name="inactivate"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="type" xsi:type="string">massInactivate</item> <item name="label" xsi:type="string" translate="true">Disable</item> <item name="url" xsi:type="url" path="*/*/massAction"> <param name="_current">1</param> <param name="action">inactivate</param> </item> </item> </argument> </action> <action name="delete"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="type" xsi:type="string">delete</item> <item name="label" xsi:type="string" translate="true">Delete</item> <item name="url" xsi:type="url" path="*/*/massAction"> <param name="_current">1</param> <param name="action">delete</param> </item> <item name="confirm" xsi:type="array"> <item name="title" xsi:type="string" translate="true">Delete items</item> <item name="message" xsi:type="string" translate="true">Are you sure you want to delete selected items?</item> </item> </item> </argument> </action> <action name="edit"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="type" xsi:type="string">edit</item> <item name="label" xsi:type="string" translate="true">Edit</item> <item name="callback" xsi:type="array"> <item name="target" xsi:type="string">editSelected</item> <item name="provider" xsi:type="string">magelearn_story_listing.magelearn_story_listing.magelearn_story_columns_editor</item> </item> </item> </argument> </action> </massaction> <paging name="listing_paging"/> </listingToolbar> <columns name="magelearn_story_columns"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="editorConfig" xsi:type="array"> <item name="indexField" xsi:type="string">id</item> <item name="enabled" xsi:type="boolean">true</item> <item name="selectProvider" xsi:type="string">magelearn_story_listing.magelearn_story_listing.magelearn_story_columns.ids</item> <item name="clientConfig" xsi:type="array"> <item name="saveUrl" xsi:type="url" path="magelearn_story/Story/inlineEdit"/> <item name="validateBeforeSave" xsi:type="boolean">false</item> </item> </item> <item name="childDefaults" xsi:type="array"> <item name="fieldAction" xsi:type="array"> <item name="provider" xsi:type="string">magelearn_story_listing.magelearn_story_listing.magelearn_story_columns_editor</item> <item name="target" xsi:type="string">startEdit</item> <item name="params" xsi:type="array"> <item name="0" xsi:type="string">${ $.$data.rowIndex }</item> <item name="1" xsi:type="boolean">true</item> </item> </item> </item> </item> </argument> <selectionsColumn name="ids"> <settings> <indexField>id</indexField> </settings> </selectionsColumn> <column name="id"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">textRange</item> <item name="sorting" xsi:type="string">asc</item> <item name="label" xsi:type="string" translate="true">ID</item> <item name="sortOrder" xsi:type="number">10</item> <item name="editor" xsi:type="array"> <item name="editorType" xsi:type="string">text</item> <item name="validation" xsi:type="array"> <item name="required" xsi:type="boolean">false</item> </item> </item> </item> </argument> </column> <column name="name"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">text</item> <item name="sorting" xsi:type="string">asc</item> <item name="label" xsi:type="string" translate="true">Name</item> <item name="sortOrder" xsi:type="number">20</item> <item name="editor" xsi:type="array"> <item name="editorType" xsi:type="string">text</item> <item name="validation" xsi:type="array"> <item name="required" xsi:type="boolean">true</item> </item> </item> </item> </argument> </column> <column name="status"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">select</item> <item name="sorting" xsi:type="string">asc</item> <item name="dataType" xsi:type="string">select</item> <item name="label" xsi:type="string" translate="true">Status</item> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/select</item> <item name="sortOrder" xsi:type="number">30</item> <item name="editor" xsi:type="array"> <item name="editorType" xsi:type="string">select</item> </item> </item> <item name="options" xsi:type="array"> <item name="disabled" xsi:type="array"> <item name="value" xsi:type="string">0</item> <item name="label" xsi:type="string" translate="true">Disabled</item> </item> <item name="enabled" xsi:type="array"> <item name="value" xsi:type="string">1</item> <item name="label" xsi:type="string" translate="true">Enabled</item> </item> </item> </argument> </column> <column name="stores" class="Magelearn\Story\Block\Adminhtml\Story\Grid\Renderer\Stores"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="bodyTmpl" xsi:type="string">ui/grid/cells/html</item> <item name="sortable" xsi:type="boolean">false</item> <item name="label" xsi:type="string" translate="true">Store Views</item> <item name="sortOrder" xsi:type="number">40</item> </item> </argument> </column> <column name="position"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">text</item> <item name="sorting" xsi:type="string">asc</item> <item name="label" xsi:type="string" translate="true">Position</item> <item name="sortOrder" xsi:type="number">50</item> <item name="editor" xsi:type="array"> <item name="editorType" xsi:type="string">text</item> </item> </item> </argument> </column> <column name="photo" class="Magelearn\Story\Ui\Component\Listing\Column\PhotoIcon"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="sortOrder" xsi:type="number">60</item> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/thumbnail</item> <item name="sortable" xsi:type="boolean">false</item> <item name="altField" xsi:type="string">name</item> <item name="has_preview" xsi:type="string">1</item> <item name="label" xsi:type="string" translate="true">Photo</item> </item> </argument> </column> <column name="created_at"> <settings> <filter>dateRange</filter> <label translate="true">Created At</label> <editor> <editorType>date</editorType> <validation> <rule name="required-entry" xsi:type="boolean">false</rule> </validation> </editor> </settings> </column> <column name="updated_at"> <settings> <filter>dateRange</filter> <label translate="true">Updated At</label> <editor> <editorType>date</editorType> <validation> <rule name="required-entry" xsi:type="boolean">false</rule> </validation> </editor> </settings> </column> <actionsColumn name="actions" class="Magelearn\Story\Ui\Component\Listing\Column\StoryActions"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="indexField" xsi:type="string">id</item> <item name="urlEntityParamName" xsi:type="string">id</item> <item name="sortOrder" xsi:type="number">90</item> <item name="resizeEnabled" xsi:type="boolean">false</item> <item name="resizeDefaultWidth" xsi:type="string">107</item> </item> </argument> </actionsColumn> </columns> </listing>
Now as per highlighted code above,
Add Block/Adminhtml/Story/Grid/Renderer/Stores.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Grid\Renderer; use Magento\Framework\Escaper; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\System\Store as SystemStore; /** * Class Stores */ class Stores extends \Magento\Store\Ui\Component\Listing\Column\Store { /** * Store constructor. * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param SystemStore $systemStore * @param Escaper $escaper * @param array|null $components * @param array|null $data * @param string $storeKey */ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, SystemStore $systemStore, Escaper $escaper, array $components = null, array $data = null, $storeKey = 'store_id' ) { parent::__construct($context, $uiComponentFactory, $systemStore, $escaper, $components, $data, $storeKey); $this->systemStore = $systemStore; } /** * Get data * * @param array $item * @return string */ protected function prepareItem(array $item) { $content = ''; if ($item['stores'] == '0' || $item['stores'] == ',0,') { return __('All Store Views'); } $data = $this->systemStore->getStoresStructure(false, explode(',', $item['stores'])); foreach ($data as $website) { $content .= $website['label'] . "<br/>"; foreach ($website['children'] as $group) { $content .= str_repeat(' ', 3) . $this->escaper->escapeHtml($group['label']) . "<br/>"; foreach ($group['children'] as $store) { $content .= str_repeat(' ', 6) . $this->escaper->escapeHtml($store['label']) . "<br/>"; } } } return $content; } }
Also add Ui/Component/Listing/Column/PhotoIcon.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Ui\Component\Listing\Column; use Magento\Backend\Model\UrlInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\View\Asset\Repository; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Listing\Columns\Column; class PhotoIcon extends Column { private $storeManager; /** * @var Repository */ private $assetRepo; /** * @var UrlInterface */ private $_backendUrl; /** * GroupIcon constructor. * * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param StoreManagerInterface $storeManager * @param Repository $assetRepo * @param UrlInterface $backendUrl * @param array $components * @param array $data */ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, StoreManagerInterface $storeManager, Repository $assetRepo, UrlInterface $backendUrl, array $components = [], array $data = [] ) { parent::__construct($context, $uiComponentFactory, $components, $data); $this->storeManager = $storeManager; $this->assetRepo = $assetRepo; $this->_backendUrl = $backendUrl; } /** * Prepare Data Source * * @param array $dataSource * @return array * @throws NoSuchEntityException */ public function prepareDataSource(array $dataSource) { if (isset($dataSource['data']['items'])) { $path = $this->storeManager->getStore()->getBaseUrl( \Magento\Framework\UrlInterface::URL_TYPE_MEDIA ) . 'magelearn/story/'; $baseImage = $this->assetRepo->getUrl('Magelearn_Story::images/no_photo.jpg'); $fieldName = $this->getData('name'); foreach ($dataSource['data']['items'] as & $item) { if ($item[$fieldName]) { $itemPath = $path . $item['id'] . '/' . $item['photo']; $item[$fieldName . '_src'] = $itemPath; $item[$fieldName . '_alt'] = $item['name']; $item[$fieldName . '_orig_src'] = $path . $item['photo']; } else { $item[$fieldName . '_src'] = $baseImage; $item[$fieldName . '_alt'] = 'Category Icon'; $item[$fieldName . '_orig_src'] = $baseImage; } $item[$fieldName . '_link'] = $this->_backendUrl->getUrl( "magelearn_story/story/edit", ['id' => $item['id']] ); } } return $dataSource; } }
Also add Ui/Component/Listing/Column/StoryActions.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Ui\Component\Listing\Column; class StoryActions extends \Magento\Ui\Component\Listing\Columns\Column { const URL_PATH_DETAILS = 'magelearn_story/story/details'; const URL_PATH_EDIT = 'magelearn_story/story/edit'; const URL_PATH_DELETE = 'magelearn_story/story/delete'; protected $urlBuilder; /** * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context * @param \Magento\Framework\View\Element\UiComponentFactory $uiComponentFactory * @param \Magento\Framework\UrlInterface $urlBuilder * @param array $components * @param array $data */ public function __construct( \Magento\Framework\View\Element\UiComponent\ContextInterface $context, \Magento\Framework\View\Element\UiComponentFactory $uiComponentFactory, \Magento\Framework\UrlInterface $urlBuilder, array $components = [], array $data = [] ) { $this->urlBuilder = $urlBuilder; parent::__construct($context, $uiComponentFactory, $components, $data); } /** * Prepare Data Source * * @param array $dataSource * @return array */ public function prepareDataSource(array $dataSource) { if (isset($dataSource['data']['items'])) { foreach ($dataSource['data']['items'] as & $item) { if (isset($item['id'])) { $item[$this->getData('name')] = [ 'edit' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_EDIT, [ 'id' => $item['id'] ] ), 'label' => __('Edit') ], 'delete' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_DELETE, [ 'id' => $item['id'] ] ), 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete %1',$item['name']), 'message' => __('Are you sure you wan\'t to delete a %1 record?', $item['name']) ] ] ]; } } } return $dataSource; } }
Now as per highlighted code in view/adminhtml/ui_component/magelearn_story_listing.xml file, we will add Controller/Adminhtml/Story/InlineEdit.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml\Story; use Magento\Backend\App\Action\Context; use Magelearn\Story\Model\StoryFactory; class InlineEdit extends \Magento\Backend\App\Action { /** * @var StoryFactory */ private $storyFactory; /** * @param \Magento\Backend\App\Action\Context $context * @param StoryFactory $storyFactory */ public function __construct( \Magento\Backend\App\Action\Context $context, StoryFactory $storyFactory ) { parent::__construct($context); $this->storyFactory = $storyFactory; } /** * Inline edit action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_JSON); $error = false; $messages = []; $postItems = $this->getRequest()->getParam('items', []); if (!($this->getRequest()->getParam('isAjax') && count($postItems))) { return $resultJson->setData([ 'messages' => [__('Please correct the data sent.')], 'error' => true, ]); } foreach ($postItems as $storyId => $storyData) { /** @var \Magelearn\Story\Model\Story $story */ $story = $this->storyFactory->create(); $story->load($storyId); $story->setData('inlineEdit', true); try { $story->addData($storyData); $story->save(); } catch (\Magento\Framework\Exception\LocalizedException $e) { $messages[] = $this->getErrorMessage($location, $e->getMessage()); $error = true; } catch (\RuntimeException $e) { $messages[] = $this->getErrorMessage($location, $e->getMessage()); $error = true; } catch (\Exception $e) { $messages[] = $this->getErrorMessage( $location, __('Something went wrong while saving the location.') ); $error = true; } } return $resultJson->setData([ 'messages' => $messages, 'error' => $error ]); } /** * Add story id to error message * * @param \Magelearn\Story\Model\Story $story * @param string $errorText * @return string */ private function getErrorMessage($story, $errorText) { return '[Story ID: ' . $story->getId() . '] ' . $errorText; } }
We will also create our Mass Action file at, Controller/Adminhtml/Story/MassAction.php
<?php namespace Magelearn\Story\Controller\Adminhtml\Story; use Magento\Backend\App\Action; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\ResultFactory; use Magento\Ui\Component\MassAction\Filter; use Magelearn\Story\Model\ResourceModel\Story\CollectionFactory; use Magento\Framework\App\Action\HttpPostActionInterface; class MassAction extends Action implements HttpPostActionInterface { /** * @var Filter */ protected $filter; /** * @var CollectionFactory */ protected $collectionFactory; /** * @param Context $context * @param Filter $filter * @param CollectionFactory $collectionFactory */ public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory) { parent::__construct($context); $this->filter = $filter; $this->collectionFactory = $collectionFactory; } public function execute() { $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setPath('*/*/'); /** @var \Magento\Ui\Component\MassAction\Filter $filter */ $this->filter->applySelectionOnTargetProvider(); // compatibility with Mass Actions on Magento 2.1.0 /** @var $collection \Magelearn\Story\Model\ResourceModel\Story\CollectionFactory */ $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); $action = $this->getRequest()->getParam('action'); if ($collectionSize && in_array($action, ['activate', 'inactivate', 'delete'])) { try { $collection->walk($action); if ($action === 'delete') { $this->messageManager->addSuccessMessage(__('You deleted the story(s).')); } else { $this->messageManager->addSuccessMessage(__('You changed the story(s).')); } return $resultRedirect; } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addErrorMessage( __('We can\'t delete story(s) right now. Please review the log and try again.').$e->getMessage() ); $this->logger->critical($e); return $resultRedirect; } } $this->messageManager->addErrorMessage(__('We can\'t find a story(s) to delete.')); return $resultRedirect; } }
Now to create a new story, we will create Controller/Adminhtml/Story/NewAction.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml\Story; class NewAction extends \Magelearn\Story\Controller\Adminhtml\Story { protected $resultForwardFactory; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory ) { $this->resultForwardFactory = $resultForwardFactory; parent::__construct($context, $coreRegistry); } /** * New action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ $resultForward = $this->resultForwardFactory->create(); return $resultForward->forward('edit'); } }
And as per highlighted code above also add Controller/Adminhtml/Story.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml; abstract class Story extends \Magento\Backend\App\Action { protected $_coreRegistry; const ADMIN_RESOURCE = 'Magelearn_Story::top_level'; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry ) { $this->_coreRegistry = $coreRegistry; parent::__construct($context); } /** * Init page * * @param \Magento\Backend\Model\View\Result\Page $resultPage * @return \Magento\Backend\Model\View\Result\Page */ public function initPage($resultPage) { $resultPage->setActiveMenu(self::ADMIN_RESOURCE) ->addBreadcrumb(__('Magelearn'), __('Magelearn')) ->addBreadcrumb(__('Story'), __('Story')); return $resultPage; } /** * Determine if authorized to perform group actions. * * @return bool */ protected function _isAllowed() { return $this->_authorization->isAllowed('Magelearn_Story::story'); } }
So, we will create Controller/Adminhtml/Story/Edit.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml\Story; class Edit extends \Magelearn\Story\Controller\Adminhtml\Story { protected $resultPageFactory; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\View\Result\PageFactory $resultPageFactory ) { $this->resultPageFactory = $resultPageFactory; parent::__construct($context, $coreRegistry); } /** * Edit action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { // 1. Get ID and create model $id = (int)$this->getRequest()->getParam('id', 0); $model = $this->_objectManager->create(\Magelearn\Story\Model\Story::class); // 2. Initial checking if ($id) { $model->load($id); if (!$model->getId()) { $this->messageManager->addErrorMessage(__('This Story no longer exists.')); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('*/*/'); } } $this->_coreRegistry->register('magelearn_story_story', $model); // 3. Build edit form /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); $this->initPage($resultPage)->addBreadcrumb( $id ? __('Edit Story') : __('New Story'), $id ? __('Edit Story') : __('New Story') ); $resultPage->getConfig()->getTitle()->prepend(__('Stories')); $resultPage->getConfig()->getTitle()->prepend($model->getId() ? __('Edit Story %1', $model->getName()) : __('New Story')); return $resultPage; } }
Now we will create our layout file to create a New story at view/adminhtml/layout/magelearn_story_new.xml file.
<?xml version="1.0" ?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="magelearn_story_story_edit"/> </page>
And add view/adminhtml/layout/magelearn_story_story_edit.xml file.
<?xml version="1.0" ?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="editor"/> <head> <css src="Magelearn_Story::css/gallery.css"/> </head> <body> <referenceContainer name="content"> <uiComponent name="magelearn_story_form"/> </referenceContainer> </body> </page>
Now, as per highlighted code above,
We will add our ui_component file at view/adminhtml/ui_component/magelearn_story_form.xml file.
<?xml version="1.0" ?> <form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="provider" xsi:type="string">magelearn_story_form.story_form_data_source</item> <item name="deps" xsi:type="string">magelearn_story_form.story_form_data_source</item> </item> <item name="label" xsi:type="string" translate="true">General Information</item> <item name="template" xsi:type="string">templates/form/collapsible</item> <item name="config" xsi:type="array"> <item name="dataScope" xsi:type="string">data</item> <item name="namespace" xsi:type="string">magelearn_story_form</item> </item> <item name="buttons" xsi:type="array"> <item name="back" xsi:type="array"> <item name="name" xsi:type="string">back</item> <item name="label" xsi:type="string" translate="true">Back</item> <item name="class" xsi:type="string">back</item> <item name="url" xsi:type="string">*/*/</item> </item> <item name="save" xsi:type="string">Magelearn\Story\Block\Adminhtml\Story\Edit\SaveButton</item> <item name="reset" xsi:type="string">Magelearn\Story\Block\Adminhtml\Story\Edit\ResetButton</item> <item name="save_and_continue" xsi:type="string">Magelearn\Story\Block\Adminhtml\Story\Edit\SaveAndContinueButton</item> <item name="delete" xsi:type="string">Magelearn\Story\Block\Adminhtml\Story\Edit\DeleteButton</item> </item> </argument> <dataSource name="story_form_data_source"> <argument name="dataProvider" xsi:type="configurableObject"> <argument name="class" xsi:type="string">Magelearn\Story\Ui\DataProvider\Form\StoryDataProvider</argument> <argument name="name" xsi:type="string">story_form_data_source</argument> <argument name="primaryFieldName" xsi:type="string">id</argument> <argument name="requestFieldName" xsi:type="string">id</argument> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="submit_url" xsi:type="url" path="*/*/save"/> </item> </argument> </argument> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/form/provider</item> </item> </argument> </dataSource> <fieldset name="general"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">General Information</item> <item name="collapsible" xsi:type="boolean">true</item> <item name="sortOrder" xsi:type="number">10</item> <item name="opened" xsi:type="boolean">true</item> </item> </argument> <field name="name"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Story Name</item> <item name="formElement" xsi:type="string">input</item> <item name="visible" xsi:type="boolean">true</item> <item name="dataType" xsi:type="string">text</item> <item name="validation" xsi:type="array"> <item name="required-entry" xsi:type="boolean">true</item> </item> </item> </argument> </field> <field name="status"> <argument name="data" xsi:type="array"> <item name="options" xsi:type="object">Magelearn\Story\Block\Adminhtml\Story\Edit\Form\Status</item> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Status</item> <item name="formElement" xsi:type="string">select</item> <item name="visible" xsi:type="boolean">true</item> <item name="dataType" xsi:type="string">select</item> <item name="validation" xsi:type="array"> <item name="required-entry" xsi:type="boolean">true</item> </item> </item> </argument> </field> <field name="stores"> <argument name="data" xsi:type="array"> <item name="options" xsi:type="object">Magelearn\Story\Ui\Component\Listing\Column\StoreOptions</item> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Store View</item> <item name="formElement" xsi:type="string">multiselect</item> <item name="visible" xsi:type="boolean">true</item> <item name="dataType" xsi:type="string">multiselect</item> <item name="validation" xsi:type="array"> <item name="required-entry" xsi:type="boolean">true</item> </item> </item> </argument> </field> <field name="position"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Position</item> <item name="formElement" xsi:type="string">input</item> <item name="visible" xsi:type="boolean">true</item> <item name="dataType" xsi:type="string">text</item> </item> </argument> </field> <field name="photo"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> <item name="formElement" xsi:type="string">fileUploader</item> <item name="elementTmpl" xsi:type="string">ui/form/element/uploader/uploader</item> <item name="label" xsi:type="string" translate="true">Story Image</item> <item name="previewTmpl" xsi:type="string">Magelearn_Story/form/element/image-preview</item> <item name="notice" xsi:type="string" translate="true"> Allowed file types : jpg jpeg gif png. Gif can have only one frame </item> <item name="validation" xsi:type="array"> <item name="required-entry" xsi:type="boolean">true</item> </item> </item> </argument> </field> <field name="description"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Description</item> <item name="formElement" xsi:type="string">wysiwyg</item> <item name="template" xsi:type="string">ui/form/field</item> <item name="wysiwyg" xsi:type="boolean">true</item> <item name="visible" xsi:type="boolean">true</item> <item name="dataType" xsi:type="string">text</item> </item> </argument> </field> </fieldset> <fieldset name="meta"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Meta Information</item> <item name="collapsible" xsi:type="boolean">true</item> <item name="sortOrder" xsi:type="number">20</item> </item> </argument> <field name="url_key"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">URL Key</item> <item name="formElement" xsi:type="string">input</item> <item name="visible" xsi:type="boolean">true</item> <item name="validation" xsi:type="array"> <item name="required-entry" xsi:type="boolean">true</item> </item> </item> </argument> </field> <field name="meta_title"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Meta Title</item> <item name="formElement" xsi:type="string">input</item> <item name="visible" xsi:type="boolean">true</item> </item> </argument> </field> <field name="meta_description"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Meta Description</item> <item name="formElement" xsi:type="string">textarea</item> <item name="visible" xsi:type="boolean">true</item> </item> </argument> </field> <field name="meta_robots"> <argument name="data" xsi:type="array"> <item name="options" xsi:type="object">Magento\Config\Model\Config\Source\Design\Robots</item> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> <item name="label" xsi:type="string" translate="true">Meta Robots</item> <item name="formElement" xsi:type="string">select</item> </item> </argument> </field> <field name="canonical_url"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Canonical</item> <item name="formElement" xsi:type="string">input</item> </item> </argument> </field> </fieldset> <fieldset name="image_gallery"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="label" xsi:type="string" translate="true">Image Gallery</item> <item name="collapsible" xsi:type="boolean">true</item> <item name="sortOrder" xsi:type="number">32</item> <item name="initializeFieldsetDataByDefault" xsi:type="boolean">true</item> </item> </argument> <field name="gallery"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">string</item> <item name="source" xsi:type="string">Gallery</item> <item name="label" xsi:type="string" translate="true">Images of Gallery</item> <item name="visible" xsi:type="boolean">true</item> <item name="formElement" xsi:type="string">fileUploader</item> <item name="previewTmpl" xsi:type="string">Magelearn_Story/form/element/gallery-image-preview</item> <item name="component" xsi:type="string">Magelearn_Story/js/form/file-uploader</item> <item name="elementTmpl" xsi:type="string">ui/form/element/uploader/uploader</item> <item name="isMultipleFiles" xsi:type="boolean">true</item> <item name="dataScope" xsi:type="string">gallery_image</item> <item name="notice" xsi:type="string" translate="true"> Allowed file types : jpg jpeg gif png. Gif can have only one frame </item> </item> </argument> </field> <field name="base_img"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="dataType" xsi:type="string">text</item> <item name="formElement" xsi:type="string">hidden</item> </item> </argument> </field> </fieldset> </form>
Now we will add our DataProvider class to provide data for edit form.
For that Add file at Ui/DataProvider/Form/StoryDataProvider.php
<?php declare(strict_types=1); namespace Magelearn\Story\Ui\DataProvider\Form; use Magelearn\Story\Model\ResourceModel\Story\Collection; use Magelearn\Story\Model\ImageProcessor; use Magelearn\Story\Model\ResourceModel\Gallery\Collection as GalleryCollection; use Magento\Framework\App\RequestInterface; class StoryDataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider { /** * @var ImageProcessor */ private $imageProcessor; /** * @var GalleryCollection */ private $galleryCollection; /** * @var RequestInterface */ private $request; public function __construct( $name, $primaryFieldName, $requestFieldName, Collection $collection, ImageProcessor $imageProcessor, GalleryCollection $galleryCollection, RequestInterface $request, array $meta = [], array $data = [] ) { parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); $this->collection = $collection; $this->imageProcessor = $imageProcessor; $this->galleryCollection = $galleryCollection; $this->request = $request; } /** * Get data * * @return array */ public function getData() { $data = parent::getData(); /** * It is need for support of several fieldsets. * For details @see \Magento\Ui\Component\Form::getDataSourceData */ if ($data['totalRecords'] > 0) { $storyId = (int)$data['items'][0]['id']; $storyModel = $this->collection->getItemById($storyId); /** @var \Magelearn\Story\Model\ResourceModel\Story $storyResource */ $storyResource = $storyModel->getResource(); $storyData = $storyModel->getData(); if ($storyModel->getPhoto()) { $photoName = $storyModel->getPhoto(); $storyData['photo'] = [ [ 'name' => $storyModel->getPhoto(), 'url' => $this->imageProcessor->getImageUrl( [ImageProcessor::ML_STORY_MEDIA_PATH, $storyData['id'], $photoName] ) ] ]; } $galleryImages = $this->galleryCollection->getImagesByStory($storyId); if (!empty($galleryImages)) { $storyData['gallery_image'] = []; foreach ($galleryImages as $image) { $imgName = $image->getData('image_name'); $imgUrlParams = [ImageProcessor::ML_STORY_GALLERIES_MEDIA_PATH, $storyData['id'], $imgName]; $imgUrl = $this->imageProcessor->getImageUrl($imgUrlParams); $imgSize = $this->imageProcessor->getImageSize($imgUrlParams); array_push( $storyData['gallery_image'], ['name' => $imgName, 'url' => $imgUrl, 'size' => $imgSize] ); if ($image->getData('is_base') == true) { $storyData['base_img'] = $imgName; } } } $data[$storyId] = $storyData; } return $data; } /** * Get data * * @return array */ public function getMeta() { $this->meta = parent::getMeta(); $storyId = (int)$this->request->getParam('id'); $this->meta['general']['children']['photo']['arguments']['data']['config']['uploaderConfig']['url'] = 'magelearn_story/file/upload/type/photo/id/' . $storyId; $this->meta['image_gallery']['children']['gallery']['arguments']['data']['config']['uploaderConfig']['url'] = 'magelearn_story/file/upload/type/gallery_image/id/' . $storyId; return $this->meta; } }
Here in getMeta() method we have provided `type` of the image upload (photo or gallery_image) which will be further used for uploaderConfig to upload the correct type of image.
And as per highlighted code in magelearn_story_form.xml file,
We will add Block/Adminhtml/Story/Edit/Form/Status.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Edit\Form; use Magento\Framework\Data\OptionSourceInterface; class Status implements OptionSourceInterface { public const DISABLED = 0; public const ENABLED = 1; /** * Get options * * @return array */ public function toOptionArray() { $optionArray = []; foreach ($this->toArray() as $value => $label) { $optionArray[] = ['value' => $value, 'label' => $label]; } return $optionArray; } /** * Get options in "key-value" format * * @return array */ public function toArray() { return [ self::DISABLED => __('Disabled'), self::ENABLED => __('Enabled') ]; } }
Also add Ui/Component/Listing/Column/StoreOptions.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Ui\Component\Listing\Column; use Magento\Store\Ui\Component\Listing\Column\Store\Options; /** * Class StoreOptions * * To use this class, add * <item name="options" xsi:type="object">Magelearn\Story\Ui\Component\Listing\Column\StoreOptions</item> * to your ui field as argument * */ class StoreOptions extends Options { /** * All Store Views value */ public const ALL_STORE_VIEWS = '0'; /** * Get options * * @return array */ public function toOptionArray() { if ($this->options !== null) { return $this->options; } $this->currentOptions['All Store Views']['label'] = __('All Store Views'); $this->currentOptions['All Store Views']['value'] = self::ALL_STORE_VIEWS; $this->generateCurrentOptions(); $this->options = array_values($this->currentOptions); return $this->options; } }
And for Photo Image Preview add view/adminhtml/web/template/form/element/image-preview.html file.
<div class="file-uploader-summary"> <div class="file-uploader-preview marker-uploader-preview"> <a attr="href: $parent.getFilePreview($file)" target="_blank"> <img class="preview-image" tabindex="0" event="load: $parent.onPreviewLoad.bind($parent)" attr=" src: $parent.getFilePreview($file), alt: $file.name"> </a> <div class="actions"> <button type="button" class="action-remove" data-role="delete-button" attr="title: $t('Delete image')" click="$parent.removeFile.bind($parent, $file)"> <span translate="'Delete image'"></span> </button> </div> </div> <div class="file-uploader-filename" text="$file.name"></div> <div class="file-uploader-meta"> <text args="$file.previewWidth"></text>x<text args="$file.previewHeight"></text> </div> </div>
And for Gallery Image Preview add view/adminhtml/web/template/form/element/gallery-image-preview.html file.
<div class="file-uploader-summary"> <div attr="class: $parent.getImageClass($file)"> <a class="image-uploader-preview-link" attr="href: $parent.getFilePreview($file)" target="_blank"> <div class="file-uploader-spinner image-uploader-spinner"></div> <img class="preview-image" tabindex="0" event="load: $parent.onPreviewLoad.bind($parent)" attr=" src: $parent.getFilePreview($file), alt: $file.name, title: $file.name"> </a> <div class="actions"> <button type="button" class="action-make-base mg-make-base-button" click="$parent.makeBase.bind($parent, $file)" data-role="button" attr="title: $t('Make base')"> <span translate="'Make base'"></span> </button> <button type="button" class="action-remove" data-role="delete-button" attr="title: $t('Delete image')" disable="$parent.disabled" click="$parent.removeFile.bind($parent, $file)"> <span translate="'Delete image'"></span> </button> </div> </div> <div class="file-uploader-filename" text="$file.name"></div> <div class="file-uploader-meta"> <text args="$file.previewWidth"></text>x<text args="$file.previewHeight"></text>, <text args="$parent.formatSize($file.size)"></text> </div> </div>
And also add our JS Component file to upload gallery images at view/adminhtml/web/js/form/file-uploader.js
define([ 'jquery', 'Magento_Ui/js/form/element/file-uploader' ], function ($, fileUploader) { return fileUploader.extend({ getImageClass: function (file) { if ($('input[name="base_img"]').val() == file.name) { return 'file-uploader-preview image-uploader-preview mg-preview-image mg-base-img'; } return 'file-uploader-preview image-uploader-preview mg-preview-image'; }, makeBase: function(file) { $('.mg-preview-image').each(function () { $(this).removeClass('mg-base-img'); }); $(event.target.closest('.mg-preview-image')).addClass('mg-base-img'); $('input[name="base_img"]').val(file.name).change(); }, }) });
Now as defined in view/adminhtml/ui_component/magelearn_story_form.xml file,
To provide different buttons HTML, We will create different block files.
Add Block/Adminhtml/Story/Edit/SaveButton.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Edit; use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; class SaveButton extends GenericButton implements ButtonProviderInterface { /** * @return array */ public function getButtonData() { return [ 'label' => __('Save Story'), 'class' => 'save primary', 'data_attribute' => [ 'mage-init' => ['button' => ['event' => 'save']], 'form-role' => 'save', ], 'sort_order' => 90, ]; } }
Now as per highlighted in above code,
Add Block/Adminhtml/Story/Edit/GenericButton.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Edit; use Magento\Backend\Block\Widget\Context; abstract class GenericButton { protected $context; /** * @param \Magento\Backend\Block\Widget\Context $context */ public function __construct(Context $context) { $this->context = $context; } /** * Return model ID * * @return int|null */ public function getModelId() { return $this->context->getRequest()->getParam('story_id'); } /** * Generate url by route and parameters * * @param string $route * @param array $params * @return string */ public function getUrl($route = '', $params = []) { return $this->context->getUrlBuilder()->getUrl($route, $params); } }
Now add Block/Adminhtml/Story/Edit/ResetButton.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Edit; use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; /** * Class ResetButton */ class ResetButton extends GenericButton implements ButtonProviderInterface { /** * @return array */ public function getButtonData() { return [ 'label' => __('Reset'), 'class' => 'reset', 'on_click' => 'location.reload();', 'sort_order' => 30, ]; } }
Now add Block/Adminhtml/Story/Edit/SaveAndContinueButton.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Edit; use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; class SaveAndContinueButton extends GenericButton implements ButtonProviderInterface { /** * @return array */ public function getButtonData() { return [ 'label' => __('Save and Continue Edit'), 'class' => 'save', 'data_attribute' => [ 'mage-init' => [ 'button' => ['event' => 'saveAndContinueEdit'], ], ], 'sort_order' => 80, ]; } }
Now add Block/Adminhtml/Story/Edit/DeleteButton.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Edit; use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; class DeleteButton extends GenericButton implements ButtonProviderInterface { /** * @return array */ public function getButtonData() { $data = []; if ($this->getModelId()) { $data = [ 'label' => __('Delete Story'), 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' ) . '\', \'' . $this->getDeleteUrl() . '\')', 'sort_order' => 20, ]; } return $data; } /** * Get URL for delete button * * @return string */ public function getDeleteUrl() { return $this->getUrl('*/*/delete', ['story_id' => $this->getModelId()]); } }
Also add Block/Adminhtml/Story/Edit/BackButton.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block\Adminhtml\Story\Edit; use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; class BackButton extends GenericButton implements ButtonProviderInterface { /** * @return array */ public function getButtonData() { return [ 'label' => __('Back'), 'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()), 'class' => 'back', 'sort_order' => 10 ]; } /** * Get URL for back (reset) button * * @return string */ public function getBackUrl() { return $this->getUrl('*/*/'); } }
Now to save the story data add file at Controller/Adminhtml/Story/Save.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml\Story; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; class Save extends \Magento\Backend\App\Action { protected $dataPersistor; /** * File system * * @var \Magento\Framework\Filesystem */ protected $filesystem; /** * File Uploader factory * * @var \Magento\MediaStorage\Model\File\UploaderFactory */ protected $fileUploaderFactory; /** * @var \Magento\Framework\Filesystem\Io\File */ protected $ioFile; /** * @var \Magelearn\Story\Model\Story */ protected $storyModel; /** * @var \Magento\Backend\Model\Session */ protected $sessionModel; /** * @var \Psr\Log\LoggerInterface */ protected $logger; /** * @var \Magento\Ui\Component\MassAction\Filter */ protected $filter; /** * @var \Magelearn\Story\Model\ResourceModel\Story\Collection */ protected $storyCollection; /** * @var DateTime */ public $date; /** * @var TimezoneInterface */ protected $timezone; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\App\Request\DataPersistorInterface $dataPersistor */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\App\Request\DataPersistorInterface $dataPersistor, \Magento\Framework\Filesystem $filesystem, \Magento\MediaStorage\Model\File\UploaderFactory $fileUploaderFactory, \Magento\Framework\Filesystem\Io\File $ioFile, \Magelearn\Story\Model\Story $storyModel, \Psr\Log\LoggerInterface $logger, \Magento\Ui\Component\MassAction\Filter $filter, \Magelearn\Story\Model\ResourceModel\Story\Collection $storyCollection, DateTime $date, TimezoneInterface $timezone ) { $this->dataPersistor = $dataPersistor; parent::__construct($context); $this->filesystem = $filesystem; $this->fileUploaderFactory = $fileUploaderFactory; $this->ioFile = $ioFile; $this->storyModel = $storyModel; $this->sessionModel = $context->getSession(); $this->logger = $logger; $this->filter = $filter; $this->storyCollection = $storyCollection; $this->date = $date; $this->timezone = $timezone; } /** * Save action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $data = $this->getRequest()->getPostValue(); if ($data) { $id = (int)$this->getRequest()->getParam('id'); $model = $this->storyModel->load($id); if (!$model->getId() && $id) { $this->messageManager->addErrorMessage(__('This Story no longer exists.')); return $resultRedirect->setPath('*/*/'); } if (isset($data['stores']) && !array_filter($data['stores'])) { $data['stores'] = ',0,'; } if (isset($data['stores']) && is_array($data['stores'])) { $data['stores'] = ',' . implode(',', array_filter($data['stores'])) . ','; } if ($model->getCreatedAt() == null) { $data['created_at'] = $this->date->date(); } $data['updated_at'] = $this->date->date(); $this->filterData($data); $this->storyModel->addData($data); $this->_prepareForSave($this->storyModel); $session = $this->sessionModel->setPageData($this->storyModel->getData()); try { $this->storyModel->save(); $this->messageManager->addSuccessMessage(__('You saved the Story.')); $this->dataPersistor->clear('magelearn_story_story'); $session->setPageData(false); if ($this->getRequest()->getParam('back')) { return $resultRedirect->setPath('*/*/edit', ['id' => $model->getId()]); } return $resultRedirect->setPath('*/*/'); } catch (LocalizedException $e) { $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addExceptionMessage($e, __('Something went wrong while saving the Story.')); } $this->dataPersistor->set('magelearn_story_story', $data); return $resultRedirect->setPath('*/*/edit', ['id' => $this->getRequest()->getParam('id')]); } return $resultRedirect->setPath('*/*/'); } /** * @param array $data */ private function filterData(&$data) { if (isset($data['photo']) && is_array($data['photo'])) { if (isset($data['photo'][0]['name'])) { $data['photo'] = $data['photo'][0]['name']; } } else { $data['photo'] = null; } } protected function _prepareForSave($model) { //upload images $data = $this->getRequest()->getPost(); $path = $this->filesystem->getDirectoryRead( DirectoryList::MEDIA )->getAbsolutePath( 'magelearn/story/' ); $imagesTypes = ['store', 'photo']; foreach ($imagesTypes as $type) { $field = $type . '_img'; $files = $this->getRequest()->getFiles(); $isRemove = isset($data['remove_' . $field]); $fileData = $this->getRequest()->getFiles($field); $hasNew = !empty($fileData['name']); try { // remove the old file if ($isRemove || $hasNew) { $oldName = isset($data['old_' . $field]) ? $data['old_' . $field] : ''; if ($oldName) { $this->ioFile->rm($path . $oldName); $model->setData($field, ''); } } // upload a new if any if (!$isRemove && $hasNew) { //find the first available name $storyId = $model->getId(); $newName = $storyId . preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $files[$field]['name']); if (substr($newName, 0, 1) == '.') { $newName = 'label' . $newName; } $uploader = $this->fileUploaderFactory->create(['fileId' => $field]); $uploader->setAllowedExtensions(['jpg', 'jpeg', 'gif', 'png']); $uploader->setAllowRenameFiles(true); $uploader->save($path, $newName); $model->setData($field, $newName); } } catch (\Exception $e) { if ($e->getCode() != \Magento\MediaStorage\Model\File\Uploader::TMP_NAME_EMPTY) { $this->logger->critical($e); } } } return true; } }
And to Delete the story add Controller/Adminhtml/Story/Delete.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Adminhtml\Story; class Delete extends \Magelearn\Story\Controller\Adminhtml\Story { /** * Delete action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); // check if we know what should be deleted $id = $this->getRequest()->getParam('id'); if ($id) { try { // init model and delete $model = $this->_objectManager->create(\Magelearn\Story\Model\Story::class); $model->load($id); $model->delete(); // display success message $this->messageManager->addSuccessMessage(__('You deleted the Story.')); // go to grid return $resultRedirect->setPath('*/*/'); } catch (\Exception $e) { // display error message $this->messageManager->addErrorMessage($e->getMessage()); // go back to edit form return $resultRedirect->setPath('*/*/edit', ['id' => $id]); } } // display error message $this->messageManager->addErrorMessage(__('We can\'t find a Story to delete.')); // go to grid return $resultRedirect->setPath('*/*/'); } }
Now to provide the proper link on header/footer we will create our front-end files.
Add etc/frontend/events.xml file.
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <event name="layout_render_before"> <observer name="magelearn_story_layout_render" instance="Magelearn\Story\Observer\LayoutRender" /> </event> </config>
We will also add view/frontend/layout/default.xml file to provide the footer link.
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="footer_links"> <block class="Magelearn\Story\Block\Link" ifconfig="mlstory/general/add_to_footer_menu" name="mlstory_footer_link"/> </referenceBlock> </body> </page>
Now we will add our observer file at Observer/LayoutRender.php
<?php declare(strict_types=1); namespace Magelearn\Story\Observer; use Magelearn\Story\Model\ConfigProvider; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\View\LayoutInterface; class LayoutRender implements ObserverInterface { public const HEADER_LINKS = 'header.links'; public const TOP_LINKS = 'top.links'; /** * @var ConfigProvider */ private $configProvider; /** * @var LayoutInterface */ private $layout; public function __construct( ConfigProvider $configProvider, LayoutInterface $layout ) { $this->configProvider = $configProvider; $this->layout = $layout; } /** * @param Observer $observer */ public function execute(Observer $observer) { if ($this->configProvider->isAddLinkToToolbar()) { $parent = null; if ($this->layout->hasElement(self::HEADER_LINKS)) { $parent = self::HEADER_LINKS; } elseif ($this->layout->hasElement(self::TOP_LINKS)) { $parent = self::TOP_LINKS; // Compatibility with Magento_Blank theme } if ($parent) { $this->layout->addBlock( \Magelearn\Story\Block\Link::class, 'mlstory_top_link', $parent ); } } } }
Now as per highlighted code above add Model/ConfigProvider.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Model; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\ScopeInterface; /** * Scope config Provider model */ class ConfigProvider { /** * xpath prefix of module */ public const PATH_PREFIX = 'mlstory'; /**#@+ * Constants defined for xpath of system configuration */ public const XPATH_ENABLE_PAGES = 'general/enable_pages'; public const XPATH_LABEL = 'general/label'; public const XPATH_ADD_LINK = 'general/add_to_toolbar_menu'; public const META_TITLE = 'story/main_settings/meta_title'; public const META_DESCRIPTION = 'story/main_settings/meta_description'; public const XPATH_PAGINATION_LIMIT = 'story/main_settings/pagination_limit'; public const XPATH_URL = 'story/main_settings/url'; public const XPATH_DESCRIPTION_LIMIT = 'story/main_settings/description_limit'; /**#@-*/ /** * @var ScopeConfigInterface */ private $scopeConfig; /** * ConfigProvider constructor. * * @param ScopeConfigInterface $scopeConfig */ public function __construct( ScopeConfigInterface $scopeConfig ) { $this->scopeConfig = $scopeConfig; } /** * An alias for scope config with default scope type SCOPE_STORE * * @param string $key * @param string|null $scopeCode * @param string $scopeType * * @return string|null */ public function getValue($key, $scopeCode = null, $scopeType = ScopeInterface::SCOPE_STORE) { return $this->scopeConfig->getValue(self::PATH_PREFIX . '/' . $key, $scopeType, $scopeCode); } /** * @param string|null $scopeCode * * @return string */ public function getMetaTitle($scopeCode = null) { return $this->getValue(self::META_TITLE, $scopeCode); } /** * @param string|null $scopeCode * * @return string */ public function getMetaDescription($scopeCode = null) { return $this->getValue(self::META_DESCRIPTION, $scopeCode); } /** * @param string|null $scopeCode * * @return int */ public function getPaginationLimit($scopeCode = null) { return (int)$this->getValue(self::XPATH_PAGINATION_LIMIT, $scopeCode); } /** * @param string|null $scopeCode * * @return string */ public function getUrl($scopeCode = null) { return $this->getValue(self::XPATH_URL, $scopeCode); } /** * @param string|null $scopeCode * * @return bool */ public function getEnablePages($scopeCode = null) { return (bool)$this->getValue(self::XPATH_ENABLE_PAGES, $scopeCode); } /** * @param string|null $scopeCode * * @return int */ public function getDescriptionLimit($scopeCode = null) { return (int)$this->getValue(self::XPATH_DESCRIPTION_LIMIT, $scopeCode); } /** * @param string|null $scopeCode * * @return string */ public function getLabel($scopeCode = null) { $label = $this->getValue(self::XPATH_LABEL, $scopeCode); return $label ? $label : __('Story Listing')->getText(); } /** * @param null $scopeCode * * @return bool */ public function isAddLinkToToolbar($scopeCode = null) { return (bool)$this->getValue(self::XPATH_ADD_LINK, $scopeCode); } }
Now as per highlighted code in Observer/LayoutRender.php file,
we will add Block/Link.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block; use Magelearn\Story\Model\ConfigProvider; use Magento\Framework\View\Element\Template\Context; use Magento\Framework\App\DefaultPathInterface; class Link extends \Magento\Framework\View\Element\Html\Link\Current { /** * @var ConfigProvider */ private $configProvider; public function __construct( Context $context, ConfigProvider $configProvider, DefaultPathInterface $defaultPath, array $data = [] ) { parent::__construct($context, $defaultPath, $data); $this->configProvider = $configProvider; } /** * @return string */ public function getPath() { if (!$this->hasData('path')) { $this->setData('path', $this->configProvider->getUrl()); } return $this->getData('path'); } /** * @return string */ public function getLabel() { return $this->configProvider->getLabel(); } /** * @return bool */ public function isCurrent() { return false; } }
Now to provide the custom routes at frontend,
First we will add etc/frontend/di.xml file.
<?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="Magento\Framework\App\RouterList"> <arguments> <argument name="routerList" xsi:type="array"> <item name="magelearn_story_root" xsi:type="array"> <item name="class" xsi:type="string">Magelearn\Story\Controller\Router</item> <item name="disable" xsi:type="boolean">false</item> <item name="sortOrder" xsi:type="string">60</item> </item> </argument> </arguments> </type> </config>
And then we will add our router file at Controller/Router.php
<?php declare(strict_types=1); namespace Magelearn\Story\Controller; use Magento\Framework\Module\Manager; use Magento\Store\Model\Store; use Magelearn\Story\Model\ResourceModel\Story; use Magelearn\Story\Model\ConfigProvider; class Router implements \Magento\Framework\App\RouterInterface { public const STORY_CONTROLLER_PATH = 'mlstory'; /** * @var \Magento\Framework\App\ActionFactory */ protected $actionFactory; /** * @var \Magento\Framework\App\Config\ScopeConfigInterface */ protected $scopeConfig; /** * @var Story */ private $storyResource; /** * @var ConfigProvider */ private $configProvider; /** * @var \Magento\Framework\App\RequestInterface|\Magento\Framework\App\Request\Http */ private $request; /** * @var \Magento\Store\Model\StoreManagerInterface */ private $storeManager; public function __construct( \Magento\Framework\App\ActionFactory $actionFactory, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Store\Model\StoreManagerInterface $storeManager, Story $storyResource, ConfigProvider $configProvider ) { $this->actionFactory = $actionFactory; $this->scopeConfig = $scopeConfig; $this->storyResource = $storyResource; $this->storeManager = $storeManager; $this->configProvider = $configProvider; } public function match(\Magento\Framework\App\RequestInterface $request) { $this->request = $request; $storyPage = $this->configProvider->getUrl(); $identifier = trim($this->request->getPathInfo(), '/'); $request->setRouteName('mlstory'); if (strpos($identifier, self::STORY_CONTROLLER_PATH) !== false && !$this->request->isAjax()) { $this->request->setModuleName('mlstory')->setControllerName('index')->setActionName('index'); return $this->actionFactory->create(\Magelearn\Story\Controller\Index\Index::class); } $identifier = current(explode("/", $identifier)); $identifierPart = trim($request->getPathInfo(), '/'); $parts = explode('/', $identifierPart); // Check if the URL matches our story view pattern if (count($parts) === 3 && $parts[0] === 'story' && $parts[1] === 'view') { $urlKey = $parts[2]; $stores = [Store::DEFAULT_STORE_ID, $this->storeManager->getStore(true)->getId()]; if ($storyId = $this->storyResource->matchStoryUrl($urlKey, $stores)) { $this->request->setModuleName('mlstory') ->setControllerName('story') ->setActionName('view') ->setParam('id', $storyId) ->setParam('url_key', $urlKey); $this->request->setAlias(\Magento\Framework\Url::REWRITE_REQUEST_PATH_ALIAS, $identifier); $this->request->setDispatched(true); return $this->actionFactory->create(\Magelearn\Story\Controller\Story\View::class); } } if ($identifier == $storyPage) { $this->request->setDispatched(true); $this->request->setModuleName('mlstory')->setControllerName('index')->setActionName('index'); $this->request->setAlias(\Magento\Framework\Url::REWRITE_REQUEST_PATH_ALIAS, $identifier); return $this->actionFactory->create(\Magelearn\Story\Controller\Index\Index::class); } else { return null; } return $this->actionFactory->create(\Magento\Framework\App\Action\Forward::class); } /** * @return string */ private function getUrlKey() { return urldecode(trim( str_replace($this->configProvider->getUrl(), '', $this->request->getPathInfo()), '/' )); } }
As per the code highlighted in Controller/Router.php file,
We will add our Controller and layout file.
Now add Controller/Index/Index.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Index; /** * Class Index */ class Index extends \Magento\Framework\App\Action\Action { /** * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { return $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE); } }
Also add view/frontend/layout/mlstory_index_index.xml file.
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <head> <css src="Magelearn_Story::css/story.css"/> </head> <body> <referenceContainer name="main"> <block class="Magelearn\Story\Block\Story" name="magelearn.story.center" template="Magelearn_Story::center.phtml" output="1"> <arguments> <argument name="cache_lifetime" xsi:type="number">86400</argument> </arguments> </block> </referenceContainer> </body> </page>
Now Add our block file to provide data on front-end phtml file.
Add file at Block/Story.php file.
<?php declare(strict_types=1); namespace Magelearn\Story\Block; use Magelearn\Story\Helper\Data; use Magelearn\Story\Model\BaseImageStory; use Magelearn\Story\Model\ConfigProvider; use Magelearn\Story\Model\ImageProcessor; use Magelearn\Story\Model\Story as StoryModel; use Magelearn\Story\Model\ResourceModel\Story\Collection as StoryCollection; use Magelearn\Story\Model\ResourceModel\Story\CollectionFactory; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Filesystem\Io\File; use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\Template\Context; use Magento\Framework\Escaper; use Magento\Framework\UrlInterface; use Magento\Widget\Block\BlockInterface; use Magento\Cms\Model\Template\FilterProvider; class Story extends Template implements IdentityInterface { /** * Path to template file in theme. * * @var string */ protected $_template = 'Magelearn_Story::center.phtml'; /** * @var Registry */ protected $coreRegistry; /** * @var \Magento\Framework\Filesystem */ protected $filesystem; /** * @var File */ protected $ioFile; /** * @var EncoderInterface */ protected $jsonEncoder; /** * @var Data */ public $dataHelper; /** * @var ConfigProvider */ public $configProvider; /** * @var StoryCollection */ private $storyCollection; /** * @var bool */ private $isStoryCollectionPrepared = false; /** * @var ImageProcessor */ private $imageProcessor; /** * @var CollectionFactory */ private $storyCollectionFactory; /** * Instance of pager block * * @var \Magento\Catalog\Block\Product\Widget\Html\Pager */ private $pager; /** * @var BaseImageStory */ private $baseImageStory; /** * @var Escaper */ private $escaper; /** * @var UrlInterface */ private $urlBuilder; /** * @var FilterProvider */ private $filterProvider; protected $logger; public function __construct( Context $context, Registry $coreRegistry, EncoderInterface $jsonEncoder, File $ioFile, Data $dataHelper, ConfigProvider $configProvider, ImageProcessor $imageProcessor, CollectionFactory $storyCollectionFactory, BaseImageStory $baseImageStory, FilterProvider $filterProvider, Escaper $escaper, UrlInterface $urlBuilder, \Psr\Log\LoggerInterface $logger, array $data = [] ) { $this->coreRegistry = $coreRegistry; $this->filesystem = $context->getFilesystem(); $this->jsonEncoder = $jsonEncoder; $this->ioFile = $ioFile; parent::__construct($context, $data); $this->dataHelper = $dataHelper; $this->configProvider = $configProvider; $this->storyCollectionFactory = $storyCollectionFactory; $this->imageProcessor = $imageProcessor; $this->baseImageStory = $baseImageStory; $this->filterProvider = $filterProvider; $this->escaper = $escaper; $this->urlBuilder = $urlBuilder; $this->logger = $logger; } /** * Return title of SStory * * @param StoryModel $story * * @return string */ public function getStoryTitle($story) { if ($story->getUrlKey() && $this->configProvider->getEnablePages()) { return '<a class="mlstory-link" href="' . $this->getStoryUrl($story) . '" title="' . $this->escaper->escapeHtml($story->getName()) . '" target="_blank">' . $this->escaper->escapeHtml($story->getName()) . '</a>'; } else { return '<div class="mlstory-title">' . $this->escaper->escapeHtml($story->getName()) . '</div>'; } } /** * Return main image url * * @param StoryModel $story * * @return string */ public function getStoryImage($story) { return $this->baseImageStory->getPhotoImageUrl($story); } /** * Get full description of story * * @return string */ public function getStoryDescription($story, bool $useFilterProcessor = false) { $descriptionLimit = $this->configProvider->getDescriptionLimit(); $description = $story->getDescription() ?? ''; if ($useFilterProcessor) { $description = $this->filterProvider->getPageFilter()->filter($description); } $description = strip_tags( preg_replace('#(<style.*?>).*?(</style>)#', '$1$2', $description) ); if (strlen($description) < $descriptionLimit) { return '<div class="mlstory-description">' . $description . '</div>'; } if ($descriptionLimit) { if (preg_match('/^(.{' . ($descriptionLimit) . '}.*?)\b/isu', $description, $matches)) { $description = $matches[1] . '...'; } if ($this->configProvider->getEnablePages()) { $description .= '<a href="' . $this->getStoryUrl($story) . '" title="read more" target="_blank"> ' . __('Read More') . '</a>'; } } return '<div class="mlstory-description">' . $description . '</div>'; } /** * Get Story url * * @return string */ private function getStoryUrl($story) { return $this->escaper->escapeUrl( $this->urlBuilder->getUrl('story/view/' . $story->getUrlKey()) ); } /** * @return StoryCollection */ public function getStoryCollection() { if (!$this->isStoryCollectionPrepared) { $this->getClearStoryCollection(); $this->storyCollection->joinMainImage(); foreach ($this->storyCollection as $story) { $story->setTemplatesHtml(); } $this->isStoryCollectionPrepared = true; } return $this->storyCollection; } public function getClearStoryCollection(): StoryCollection { if (!$this->storyCollection) { $this->storyCollection = $this->storyCollectionFactory->create(); $this->storyCollection->applyDefaultFilters(); //$this->storyCollection->getStoryData(); $this->storyCollection->setCurPage((int) $this->getRequest()->getParam('p', 1)); $this->storyCollection->setPageSize($this->configProvider->getPaginationLimit()); } return $this->storyCollection; } public function getMlStoreMediaUrl() { $store_url = $this->_storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA); $store_url = $store_url . 'magelearn/story/'; return $store_url; } /** * Add metadata to page header * * @return $this */ protected function _prepareLayout() { if ($this->getNameInLayout()) { if ($title = $this->configProvider->getMetaTitle()) { $this->pageConfig->getTitle()->set($title); } if ($description = $this->configProvider->getMetaDescription()) { $this->pageConfig->setDescription($description); } $this->getPagerHtml(); if ($this->pager && !$this->pager->isFirstPage()) { $this->addPrevNext( $this->getUrl('mlstory', ['p' => $this->pager->getCurrentPage() - 1]), ['rel' => 'prev'] ); } if ($this->pager && $this->pager->getCurrentPage() < $this->pager->getLastPageNum()) { $this->addPrevNext( $this->getUrl('mlstory', ['p' => $this->pager->getCurrentPage() + 1]), ['rel' => 'next'] ); } } return parent::_prepareLayout(); } /** * Add prev/next pages * * @param string $url * @param array $attributes * */ protected function addPrevNext($url, $attributes) { $this->pageConfig->addRemotePageAsset( $url, 'link_rel', ['attributes' => $attributes] ); } /** * Return Pager for story page * * @return string */ public function getPagerHtml() { if ($this->getLayout()->getBlock('magelearn.story.pager')) { $this->pager = $this->getLayout()->getBlock('magelearn.story.pager'); return $this->pager->toHtml(); } if (!$this->pager) { $this->pager = $this->getLayout()->createBlock( Pager::class, 'magelearn.story.pager' ); if ($this->pager) { $this->pager->setUseContainer( false )->setShowPerPage( false )->setShowAmounts( false )->setFrameLength( $this->_scopeConfig->getValue( 'design/pagination/pagination_frame', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) )->setJump( $this->_scopeConfig->getValue( 'design/pagination/pagination_frame_skip', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) )->setLimit( $this->configProvider->getPaginationLimit() )->setCollection( $this->getClearStoryCollection() )->setTemplate( 'Magelearn_Story::pager.phtml' ); return $this->pager->toHtml(); } } return ''; } /** * Return identifiers for produced content * * @return array */ public function getIdentities() { return [StoryModel::CACHE_TAG]; } /** * @return string[] */ public function getCacheKeyInfo() { $cacheKeyInfo = parent::getCacheKeyInfo(); if ($this->configProvider->getPaginationLimit()) { $cacheKeyInfo = array_merge( $cacheKeyInfo, [implode('-', $this->getClearStoryCollection()->getIdsOnPage())] ); } return $cacheKeyInfo; } }
Now we will add our pager Block and html files as per the code defined in Block/Story.php file.
Add file at Block/Pager.php
<?php declare(strict_types=1); namespace Magelearn\Story\Block; class Pager extends \Magento\Theme\Block\Html\Pager { /** * Return correct URL for ajax request * * @param array $params * @return string */ public function getPagerUrl($params = []) { $ajaxUrl = rtrim($this->_urlBuilder->getUrl('mlstory'), '/'); if ($query = $this->getRequest()->getParam('query')) { $params['query'] = $query; } // Remove page parameter if it's 1 or not set if (isset($params['p']) && ($params['p'] == 1 || $params['p'] == '1')) { unset($params['p']); } // Return base URL without any parameters if empty if (empty($params)) { return $ajaxUrl; } $queryString = http_build_query($params); return !empty($queryString) ? $ajaxUrl . '?' . $queryString : $ajaxUrl; } }
Also add file at view/frontend/templates/pager.phtml
<?php /** * @var \Magento\Theme\Block\Html\Pager $block * @var \Magento\Framework\Escaper $escaper */ ?> <?php if ($block->getCollection()->getSize()): ?> <?php if ($block->getUseContainer()): ?> <div class="pager"> <?php endif ?> <?php if ($block->getShowAmounts()): ?> <p class="toolbar-amount"> <span class="toolbar-number"> <?php if ($block->getLastPageNum()>1): ?> <?= $escaper->escapeHtml( __('Items %1 to %2 of %3 total', $block->getFirstNum(), $block->getLastNum(), $block->getTotalNum()) ) ?> <?php elseif ($block->getTotalNum() == 1): ?> <?= $escaper->escapeHtml(__('%1 Item', $block->getTotalNum())) ?> <?php else: ?> <?= $escaper->escapeHtml(__('%1 Item(s)', $block->getTotalNum())) ?> <?php endif; ?> </span> </p> <?php endif ?> <?php if ($block->getLastPageNum()>1): ?> <div class="pages"> <strong class="label pages-label" id="paging-label"><?= $escaper->escapeHtml(__('Page')) ?></strong> <ul class="items pages-items" aria-labelledby="paging-label"> <?php if (!$block->isFirstPage()): ?> <li class="item pages-item-previous"> <?php $text = $block->getAnchorTextForPrevious() ? $block->getAnchorTextForPrevious() : '';?> <a class="<?= $escaper->escapeHtmlAttr($text ? 'link ' : 'action ') ?> previous" href="<?= $escaper->escapeUrl($block->getPreviousPageUrl()) ?>" rel="nofollow" title="<?= $escaper->escapeHtmlAttr($text ? $text : __('Previous')) ?>"> <span class="label"><?= $escaper->escapeHtml(__('Page')) ?></span> <span><?= $escaper->escapeHtml($text ? $text : __('Previous')) ?></span> </a> </li> <?php endif;?> <?php if ($block->canShowFirst()): ?> <li class="item"> <a class="page first" href="<?= $escaper->escapeUrl($block->getFirstPageUrl()) ?>" rel="nofollow"> <span class="label"><?= $escaper->escapeHtml(__('Page')) ?></span> <span>1</span> </a> </li> <?php endif;?> <?php if ($block->canShowPreviousJump()): ?> <li class="item"> <a class="page previous jump" title="" href="<?= $escaper->escapeUrl($block->getPreviousJumpUrl()) ?>" rel="nofollow"> <span>...</span> </a> </li> <?php endif;?> <?php foreach ($block->getFramePages() as $_page): ?> <?php if ($block->isPageCurrent($_page)): ?> <li class="item current"> <strong class="page"> <span class="label"><?= $escaper->escapeHtml(__('You\'re currently reading page')) ?></span> <span><?= $escaper->escapeHtml($_page) ?></span> </strong> </li> <?php else: ?> <li class="item"> <a href="<?= $escaper->escapeUrl($block->getPageUrl($_page)) ?>" class="page" rel="nofollow"> <span class="label"><?= $escaper->escapeHtml(__('Page')) ?></span> <span><?= $escaper->escapeHtml($_page) ?></span> </a> </li> <?php endif;?> <?php endforeach;?> <?php if ($block->canShowNextJump()): ?> <li class="item"> <a class="page next jump" title="" href="<?= $escaper->escapeUrl($block->getNextJumpUrl()) ?>" rel="nofollow"> <span>...</span> </a> </li> <?php endif;?> <?php if ($block->canShowLast()): ?> <li class="item"> <a class="page last" href="<?= $escaper->escapeUrl($block->getLastPageUrl()) ?>" rel="nofollow"> <span class="label"><?= $escaper->escapeHtml(__('Page')) ?></span> <span><?= $escaper->escapeHtml($block->getLastPageNum()) ?></span> </a> </li> <?php endif;?> <?php if (!$block->isLastPage()): ?> <li class="item pages-item-next"> <?php $text = $block->getAnchorTextForNext() ? $block->getAnchorTextForNext() : '';?> <a class="<?= /* @noEscape */ $text ? 'link ' : 'action ' ?> next" href="<?= $escaper->escapeUrl($block->getNextPageUrl()) ?>" rel="nofollow" title="<?= $escaper->escapeHtmlAttr($text ? $text : __('Next')) ?>"> <span class="label"><?= $escaper->escapeHtml(__('Page')) ?></span> <span><?= $escaper->escapeHtml($text ? $text : __('Next')) ?></span> </a> </li> <?php endif;?> </ul> </div> <?php endif; ?> <?php if ($block->isShowPerPage()): ?> <div class="limiter"> <strong class="limiter-label"><?= $escaper->escapeHtml(__('Show')) ?></strong> <select id="limiter" data-mage-init='{"redirectUrl": {"event":"change"}}' class="limiter-options"> <?php foreach ($block->getAvailableLimit() as $_key => $_limit): ?> <option value="<?= $escaper->escapeUrl($block->getLimitUrl($_key)) ?>" <?php if ($block->isLimitCurrent($_key)): ?> selected="selected"<?php endif ?>> <?= $escaper->escapeHtml($_limit) ?> </option> <?php endforeach; ?> </select> <span class="limiter-text"><?= $escaper->escapeHtml(__('per page')) ?></span> </div> <?php endif ?> <?php if ($block->getUseContainer()): ?> </div> <?php endif ?> <?php endif ?>
Now as per highlighted in view/frontend/layout/mlstory_index_index.xml,
We will add our template file at view/frontend/templates/center.phtml
<?php /** * @var \Magelearn\Story\Block\Story $block * @var \Magelearn\Story\Model\Story $story */ $stories = $block->getStoryCollection(); ?> <div class="magelearn-story-list"> <?php if ($stories->count()): ?> <div class="story-container story-grid"> <?php foreach ($stories as $story): ?> <div class="story-item" data-mlid="<?= (int)$story['id']; ?>"> <div class="story-card"> <?php if ($photoImage = $story->getPhoto()):?> <div class="story-image"> <div class="story-image-wrapper"> <!-- Main Image --> <img class="main-photo" src="<?= $story->getPhoto() ?>" alt="<?= $block->escapeHtml($story->getName()) ?>" /> <!-- Hover Image: Check if photo_url exists --> <?php if ($hoverImage = $story->getPhotoUrl()): ?> <img class="hover-photo" src="<?= $hoverImage ?>" alt="<?= $block->escapeHtml($story->getName()) ?>" /> <?php endif; ?> </div> </div> <?php endif;?> <div class="story-content"> <h2 class="story-title"> <?= $block->getStoryTitle($story) ?> </h2> <div class="story-description"> <?= /** @escapeNotVerified */ ($block->getStoryDescription($story, true)); ?> </div> </div> </div> </div> <?php endforeach; ?> </div> <?php if ($pager = $block->getPagerHtml()): ?> <div class="story-pagination"><?= /** @escapeNotVerified */ $pager; ?></div> <?php endif; ?> <?php else: ?> <p class="no-stories">No stories found.</p> <?php endif; ?> </div>
Now as per defined in Block/Story.php file at getStoryUrl() function,
We have added URL route for story view page as 'story/view/'
Accordingly that, we will create our controller and layout files.
Add file at Controller/Story/View.php
<?php declare(strict_types=1); namespace Magelearn\Story\Controller\Story; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magelearn\Story\Model\Story; use Magento\Framework\Registry; use Magento\Framework\Controller\ResultFactory; /** * Class View */ class View extends Action { /** * @var Story */ private $storyModel; /** * @var Registry */ private $coreRegistry; public function __construct( Context $context, Story $storyModel, Registry $coreRegistry ) { parent::__construct($context); $this->storyModel = $storyModel; $this->coreRegistry = $coreRegistry; } /** * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { $urlKey = $this->getRequest()->getParam('url_key'); if ($storyId = (int)$this->_request->getParam('id')) { $story = $this->storyModel->load($storyId); } if (!$storyId) { return $this->resultFactory->create(ResultFactory::TYPE_FORWARD); } $this->coreRegistry->register('mlstory_current_story', $story); $this->coreRegistry->register('mlstory_current_story_id', $story->getId()); return $this->resultFactory->create(ResultFactory::TYPE_PAGE); } }
Also add layout file at view/frontend/layout/mlstory_story_view.xml
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <head> <css src="Magelearn_Story::css/storyview.css"/> <css src="Magelearn_Story::css/fancybox.css"/> <css src="Magelearn_Story::css/slick.css"/> <css src="Magelearn_Story::css/slick-theme.css"/> </head> <body> <referenceContainer name="main"> <block class="Magelearn\Story\Block\View\Story" name="story" template="Magelearn_Story::pages/story_view.phtml"/> </referenceContainer> </body> </page>
Now as per highlighted code above, we will add Block and template file.
Add file at Block/View/Story.php
<?php declare(strict_types=1); namespace Magelearn\Story\Block\View; use Magelearn\Story\Model\ConfigProvider; use Magelearn\Story\Model\ImageProcessor; use Magelearn\Story\Model\Story as storyModel; use Magelearn\Story\Model\ResourceModel\Gallery\Collection; use Magento\Directory\Model\RegionFactory; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Registry; use Magento\Framework\View\Element\Template; use Magento\Cms\Model\Template\FilterProvider; /** * Story front block. */ class Story extends Template implements IdentityInterface { /** * @var Registry */ private $coreRegistry; /** * @var ConfigProvider */ public $configProvider; /** * @var storyModel */ private $storyModel; /** * @var \Magelearn\Story\Helper\Data */ public $dataHelper; /** * @var Collection */ private $galleryCollection; /** * @var ImageProcessor */ private $imageProcessor; /** * @var RegionFactory */ private $regionFactory; /** * @var FilterProvider */ private $filterProvider; public function __construct( Template\Context $context, Registry $coreRegistry, ConfigProvider $configProvider, storyModel $storyModel, Collection $galleryCollection, ImageProcessor $imageProcessor, RegionFactory $regionFactory, \Magelearn\Story\Helper\Data $dataHelper, FilterProvider $filterProvider, array $data = [] ) { parent::__construct($context, $data); $this->coreRegistry = $coreRegistry; $this->configProvider = $configProvider; $this->storyModel = $storyModel; $this->galleryCollection = $galleryCollection; $this->imageProcessor = $imageProcessor; $this->regionFactory = $regionFactory; $this->dataHelper = $dataHelper; $this->filterProvider = $filterProvider; } public function getCacheLifetime() { return null; } /** * @return storyModel|bool */ public function getCurrentStory() { if ($this->getStoryId()) { try { $this->storyModel->load($this->getStoryId()); return $this->storyModel; //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (\Exception $e) { } } return false; } /** * Get full description of story * * @return string */ public function getStoryDescription($story) { $description = ''; if ($story->getDescription()) { $description = $story->getDescription(); } return $this->filterProvider->getPageFilter()->filter($description); } /** * @return array */ public function getStoryGallery() { $storyId = $this->getStoryId(); $storyImages = $this->galleryCollection->getImagesByStory($storyId); $result = []; foreach ($storyImages as $image) { array_push( $result, [ 'name' => $image->getData('image_name'), 'is_base' => (bool)$image->getData('is_base'), 'path' => $this->imageProcessor->getImageUrl( [ImageProcessor::ML_STORY_GALLERIES_MEDIA_PATH, $storyId, $image->getData('image_name')] ) ] ); } return $result; } public function getStoryPhoto($imagePath) { $storyId = $this->getStoryId(); return $this->imageProcessor->getImageUrl( [ImageProcessor::ML_STORY_MEDIA_PATH, $storyId, $imagePath] ); } /** * @return int */ public function getStoryId() { if (!$this->hasData('story_id')) { $this->setData('story_id', $this->coreRegistry->registry('mlstory_current_story_id')); } return (int)$this->getData('story_id'); } /** * Add metadata to page * * @return $this */ protected function _prepareLayout() { $story = $this->getCurrentStory(); if ($story) { if ($description = $story->getMetaTitle()) { $this->pageConfig->getTitle()->set($story->getMetaTitle()); } /** @var \Magento\Theme\Block\Html\Title $headingBlock */ if ($headingBlock = $this->getLayout()->getBlock('page.main.title')) { $headingBlock->setPageTitle($story->getName()); } if ($description = $story->getMetaDescription()) { $this->pageConfig->setDescription($description); } if ($metaRobots = $story->getMetaRobots()) { $this->pageConfig->setRobots($metaRobots); } if ($canonical = $story->getCanonicalUrl()) { $this->pageConfig->addRemotePageAsset( $canonical, 'canonical', ['attributes' => ['rel' => 'canonical']] ); } } $breadcrumbsBlock = $this->getLayout()->getBlock('breadcrumbs'); if ($story && $breadcrumbsBlock) { $breadcrumbsBlock->addCrumb( 'mlstory', [ 'label' => $this->configProvider->getLabel(), 'title' => $this->configProvider->getLabel(), 'link' => $this->_urlBuilder->getUrl($this->configProvider->getUrl()) ] ); $breadcrumbsBlock->addCrumb( 'story_page', [ 'label' => $story->getName(), 'title' => $story->getName() ] ); } return parent::_prepareLayout(); } /** * Return identifiers for produced content * * @return array */ public function getIdentities() { return [storyModel::CACHE_TAG . '_' . $this->getStoryId()]; } /** * @return array */ public function getCacheKeyInfo() { return parent::getCacheKeyInfo() + ['l_id' => $this->getStoryId()]; } }
And as per highlighted code above add file at Helper/Data.php
<?php declare(strict_types=1); namespace Magelearn\Story\Helper; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; use Magento\Store\Model\StoreManagerInterface; class Data extends AbstractHelper { /** * Store manager * * @var StoreManagerInterface */ protected $storeManager; public function __construct( StoreManagerInterface $storeManager, Context $context ) { parent::__construct($context); $this->storeManager = $storeManager; } /** * @param $name * * @return string */ public function getImageUrl($name) { $path = $this->storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA); return $path . 'magelearn/story/'. $name; } public function compressHtml($html) { return preg_replace( '#(?ix)(?>[^\S ]\s*|\s{2,})#', //remove break lines ' ', preg_replace('/<!--(?!\s*ko\s|\s*\/ko)[^>]*-->/', '', $html) //remove html comments ); } }
And also add view/frontend/templates/pages/story_view.phtml file.
<?php /** @var \Magelearn\Story\Model\Story $story */ /** @var \Magelearn\Story\Block\View\Story $block */ $story = $block->getCurrentStory(); $galleryImages = $block->getStoryGallery(); ?> <?php if ($story->getPhoto()): ?> <div class="story-main-section"> <div class="main-story-photo"> <img src="<?= $block->escapeHtml($block->getStoryPhoto($story->getPhoto())) ?>" alt="<?= $block->escapeHtml($story->getName() . ' - Main Photo') ?>" loading="lazy"> </div> <div class="story-description"> <?= /** @escapeNotVerified */ ($block->getStoryDescription($story)) ?> </div> </div> <?php endif; ?> <?php if (!empty($galleryImages)): ?> <div class="story-gallery-section"> <h2 class="gallery-title"><?= $block->escapeHtml(__('Image Gallery')) ?></h2> <div class="gallery-wrapper" id="story-gallery"> <div class="gallery-slider"> <?php foreach ($galleryImages as $index => $image): ?> <div class="gallery-slide"> <a class="story-gallery-item" data-fancybox="gallery" data-src="<?= $block->escapeHtml($image['path']) ?>" data-caption="<?= $block->escapeHtml($story->getName() . ' - Image ' . ($index + 1)) ?>"> <img src="<?= $block->escapeHtml($image['path']) ?>" alt="<?= $block->escapeHtml($story->getName() . ' - Image ' . ($index + 1)) ?>" loading="lazy" onerror="this.onerror=null; this.src='<?= $block->escapeUrl($block->getViewFileUrl('Magelearn_Story::images/placeholder.jpg')) ?>';"> </a> </div> <?php endforeach; ?> </div> <div class="gallery-slider-nav"> <?php foreach ($galleryImages as $index => $image): ?> <div class="gallery-nav-slide"> <img src="<?= $block->escapeHtml($image['path']) ?>" alt="<?= $block->escapeHtml($story->getName() . ' - Thumbnail ' . ($index + 1)) ?>" loading="lazy" onerror="this.onerror=null; this.src='<?= $block->escapeUrl($block->getViewFileUrl('Magelearn_Story::images/placeholder.jpg')) ?>';"> </div> <?php endforeach; ?> </div> </div> </div> <script type="text/x-magento-init"> { "#story-gallery": { "Magelearn_Story/js/gallery-init": {} } } </script> <?php else: ?> <div class="no-gallery-message"> <?= $block->escapeHtml(__('No images available for this story.')) ?> </div> <?php endif; ?>
Now at frontend, to add Fancybox and Slick carousel,
We will define Our JS paths at requirejs-config.js
Add file at view/frontend/requirejs-config.js
var config = { map: { "*": { 'fancybox': 'Magelearn_Story/js/plugins/fancybox/fancybox.umd' } }, paths: { 'fancybox': 'Magelearn_Story/js/plugins/fancybox/fancybox.umd', 'slick': 'Magelearn_Story/js/plugins/slick/slick.min' }, shim: { 'slick': { deps: ['jquery'] } } };
Now as per the defined path above add CSS and JS file at view/frontend/web/css and view/frontend/web/js
Also as per highlighted code in view/frontend/templates/pages/story_view.phtml add JS file at view/frontend/web/js/gallery-init.js to initialize Slick slider and Fancybox for gallery images.
define([ 'jquery', 'Magelearn_Story/js/story-gallery', 'Magelearn_Story/js/slick-slider' ], function($, storyGallery, slickSlider) { 'use strict'; return function (config) { $(document).ready(function() { // Initialize Fancybox storyGallery({ options: { groupAll: true, animated: true, showClass: "f-fadeIn", hideClass: "f-fadeOut", Toolbar: { display: { left: ["prev", "next"], middle: ["zoomIn", "zoomOut", "toggle1to1", "slideshowStart", "slideshowStop"], right: ["close"] } }, Carousel: { infinite: true, friction: 0.8 } } }); // Initialize Slick Slider slickSlider(); }); }; });
Add view/frontend/web/js/story-gallery.js fancybox gallery initialization
define([ 'jquery', 'fancybox' ], function ($, fancybox) { 'use strict'; return function (config) { $(document).ready(function () { try { if (typeof window.Fancybox !== 'undefined') { window.Fancybox.bind('[data-fancybox]', config.options); } else if (fancybox && fancybox.Fancybox) { fancybox.Fancybox.bind('[data-fancybox]', config.options); } else if (fancybox && typeof fancybox.bind === 'function') { fancybox.bind('[data-fancybox]', config.options); } else { console.error('Fancybox not properly loaded'); } } catch (error) { console.error('Fancybox initialization error:', error); } }); }; });
And view/frontend/web/js/slick-slider.js for slick slider initialization.
0 Comments On "Effortless Multiple Image Upload with Slick Carousel & Fancybox Integration in Magento 2"