├── Block └── Adminhtml │ └── Button │ ├── Back.php │ ├── Delete.php │ ├── Reset.php │ └── Save.php ├── Console └── Command │ └── Deploy.php ├── Controller └── Adminhtml │ ├── Delete.php │ ├── Edit.php │ ├── Heartbeat │ └── Index.php │ ├── Index.php │ ├── InlineEdit.php │ ├── MassDelete.php │ ├── NewAction.php │ ├── Save.php │ └── Upload.php ├── Generator ├── ListRepo.php ├── NameMatcher.php ├── Repo.php ├── UiCollectionProvider.php └── UiManager.php ├── LICENSE.txt ├── Model ├── FileChecker.php ├── FileInfo.php ├── ResourceModel │ ├── AbstractModel.php │ ├── Collection │ │ ├── AbstractCollection.php │ │ └── StoreAwareAbstractCollection.php │ ├── Relation │ │ └── Store │ │ │ ├── ReadHandler.php │ │ │ └── SaveHandler.php │ ├── Store.php │ └── StoreAwareAbstractModel.php └── Uploader.php ├── README.md ├── Source ├── Catalog │ ├── ProductAttribute.php │ ├── ProductAttributeOptions.php │ └── ProductAttributeSet.php ├── Options.php └── StoreView.php ├── Test └── Unit │ ├── Block │ └── Adminhtml │ │ └── Button │ │ ├── BackTest.php │ │ ├── DeleteTest.php │ │ ├── ResetTest.php │ │ └── SaveTest.php │ ├── Console │ └── Command │ │ └── DeployTest.php │ ├── Controller │ ├── DeleteTest.php │ ├── EditTest.php │ ├── Heartbeat │ │ └── IndexTest.php │ ├── IndexTest.php │ ├── InlineEditTest.php │ ├── MassDeleteTest.php │ ├── NewActionTest.php │ ├── SaveTest.php │ └── UploadTest.php │ ├── Generator │ ├── ListRepoTest.php │ ├── NameMatcherTest.php │ ├── RepoTest.php │ ├── UiCollectionProviderTest.php │ └── UiManagerTest.php │ ├── Model │ ├── FileCheckerTest.php │ ├── FileInfoTest.php │ ├── ResourceModel │ │ ├── AbstractModelTest.php │ │ ├── Collection │ │ │ ├── AbstractCollectionTest.php │ │ │ └── StoreAwareAbstractCollectionTest.php │ │ ├── Relation │ │ │ └── Store │ │ │ │ ├── ReadHandlerTest.php │ │ │ │ └── SaveHandlerTest.php │ │ ├── StoreAwareAbstractModelTest.php │ │ └── StoreTest.php │ └── UploaderTest.php │ ├── Source │ ├── Catalog │ │ ├── ProductAttributeOptionsTest.php │ │ ├── ProductAttributeSetTest.php │ │ └── ProductAttributeTest.php │ ├── OptionsTest.php │ └── StoreViewTest.php │ ├── Ui │ ├── Component │ │ └── Listing │ │ │ ├── ActionsColumnTest.php │ │ │ └── ImageTest.php │ ├── EntityUiConfigTest.php │ ├── Form │ │ ├── DataModifier │ │ │ ├── CompositeDataModifierTest.php │ │ │ ├── DataModifierTest.php │ │ │ ├── MultiselectTest.php │ │ │ ├── NullModifierTest.php │ │ │ └── UploadTest.php │ │ └── DataProviderTest.php │ └── SaveDataProcessor │ │ ├── CompositeProcessorTest.php │ │ ├── DateTest.php │ │ ├── DynamicRowsTest.php │ │ ├── MultiselectTest.php │ │ ├── NullProcessorTest.php │ │ └── UploadTest.php │ └── ViewModel │ ├── Formatter │ ├── DateTest.php │ ├── FileTest.php │ ├── ImageTest.php │ ├── OptionTest.php │ ├── TextTest.php │ └── WysiwygTest.php │ ├── FormatterTest.php │ └── HeartbeatTest.php ├── Ui ├── CollectionProviderInterface.php ├── Component │ └── Listing │ │ ├── ActionsColumn.php │ │ └── Image.php ├── EntityUiConfig.php ├── EntityUiManagerInterface.php ├── Form │ ├── DataModifier │ │ ├── CompositeDataModifier.php │ │ ├── DynamicRows.php │ │ ├── Multiselect.php │ │ ├── NullModifier.php │ │ └── Upload.php │ ├── DataModifierInterface.php │ └── DataProvider.php ├── SaveDataProcessor │ ├── CompositeProcessor.php │ ├── Date.php │ ├── DynamicRows.php │ ├── Multiselect.php │ ├── NullProcessor.php │ └── Upload.php └── SaveDataProcessorInterface.php ├── ViewModel ├── Formatter.php ├── Formatter │ ├── Date.php │ ├── File.php │ ├── FormatterInterface.php │ ├── Image.php │ ├── Options.php │ ├── Text.php │ └── Wysiwyg.php └── Heartbeat.php ├── composer.json ├── etc ├── adminhtml │ └── routes.xml ├── crud │ └── di.xml ├── di.xml └── module.xml ├── i18n └── en_US.csv ├── registration.php └── view └── adminhtml ├── templates └── heartbeat.phtml └── web ├── js └── heartbeat.js └── template └── preview.html /Block/Adminhtml/Button/Back.php: -------------------------------------------------------------------------------- 1 | url = $url; 47 | $this->uiConfig = $uiConfig; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function getButtonData() 54 | { 55 | return [ 56 | 'label' => $this->getLabel(), 57 | 'on_click' => sprintf("location.href = '%s';", $this->url->getUrl('*/*/')), 58 | 'class' => 'back', 59 | 'sort_order' => 10 60 | ]; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | private function getLabel() 67 | { 68 | return $this->uiConfig ? $this->uiConfig->getBackLabel() : __('Back')->render(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Block/Adminhtml/Button/Delete.php: -------------------------------------------------------------------------------- 1 | request = $request; 64 | $this->entityUiManager = $entityUiManager; 65 | $this->uiConfig = $uiConfig; 66 | $this->url = $url; 67 | } 68 | 69 | /** 70 | * @inheritDoc 71 | */ 72 | public function getButtonData() 73 | { 74 | $data = []; 75 | if ($this->getEntityId()) { 76 | $data = [ 77 | 'label' => $this->uiConfig->getDeleteLabel(), 78 | 'class' => 'delete', 79 | 'on_click' => 'deleteConfirm(\'' . 80 | $this->uiConfig->getDeleteMessage() . '\', \'' . $this->getDeleteUrl() . '\', {"data": {}})', 81 | 'sort_order' => 20, 82 | ]; 83 | } 84 | return $data; 85 | } 86 | 87 | /** 88 | * @return int|null 89 | */ 90 | private function getEntityId(): ?int 91 | { 92 | try { 93 | return $this->entityUiManager->get( 94 | (int)$this->request->getParam($this->uiConfig->getRequestParamName(), 0) 95 | )->getId(); 96 | } catch (NoSuchEntityException $e) { 97 | return null; 98 | } 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | private function getDeleteUrl(): string 105 | { 106 | return $this->url->getUrl( 107 | '*/*/delete', 108 | [$this->uiConfig->getRequestParamName() => $this->getEntityId()] 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Block/Adminhtml/Button/Reset.php: -------------------------------------------------------------------------------- 1 | __('Reset'), 37 | 'class' => 'reset', 38 | 'on_click' => 'location.reload();', 39 | 'sort_order' => 30 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Console/Command/Deploy.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 58 | $this->ioFile = $ioFile; 59 | $this->directoryList = $directoryList; 60 | parent::__construct($name); 61 | } 62 | 63 | /** 64 | * configure command 65 | */ 66 | protected function configure() 67 | { 68 | $this->setName('umc:crud:deploy'); 69 | $this->setDescription('Copies the required di.xml file for Umc_Crud module to app/etc'); 70 | $this->addOption("force", "-f", InputOption::VALUE_NONE, "Force deploy config"); 71 | } 72 | 73 | /** 74 | * @param InputInterface $input 75 | * @param OutputInterface $output 76 | * @return int|null|void 77 | * @throws \Magento\Framework\Exception\FileSystemException 78 | */ 79 | protected function execute(InputInterface $input, OutputInterface $output) 80 | { 81 | if ($output->isVerbose()) { 82 | $output->writeln('Copying CRUD generators config to root etc folder'); 83 | } 84 | $source = $this->reader->getModuleDir('etc', 'Umc_Crud') . '/' . self::DI_FOLDER . '/di.xml'; 85 | $destination = $this->directoryList->getPath('etc') . '/crud'; 86 | $destinationFile = $destination . '/di.xml'; 87 | 88 | $isForce = $input->getOption('force'); 89 | if (!$isForce && $this->ioFile->fileExists($destinationFile)) { 90 | $output->writeln($destinationFile . " already exists. Use -f to overwrite it."); 91 | } else { 92 | $this->ioFile->checkAndCreateFolder($destination); 93 | $this->ioFile->cp($source, $destinationFile); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Delete.php: -------------------------------------------------------------------------------- 1 | uiConfig = $uiConfig; 53 | $this->uiManager = $uiManager; 54 | parent::__construct($context); 55 | } 56 | 57 | /** 58 | * @return \Magento\Framework\Controller\Result\Redirect 59 | */ 60 | public function execute() 61 | { 62 | $paramName = $this->uiConfig->getRequestParamName(); 63 | /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ 64 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 65 | $id = (int)$this->getRequest()->getParam($paramName); 66 | if ($id) { 67 | try { 68 | $this->uiManager->delete($id); 69 | $this->messageManager->addSuccessMessage($this->uiConfig->getDeleteSuccessMessage()); 70 | $resultRedirect->setPath('*/*/'); 71 | return $resultRedirect; 72 | } catch (NoSuchEntityException $e) { 73 | $this->messageManager->addErrorMessage($this->uiConfig->getDeleteMissingEntityMessage()); 74 | $resultRedirect->setPath('*/*/'); 75 | return $resultRedirect; 76 | } catch (LocalizedException $e) { 77 | $this->messageManager->addErrorMessage($e->getMessage()); 78 | $resultRedirect->setPath('*/*/edit', [$paramName => $id]); 79 | return $resultRedirect; 80 | } catch (\Exception $e) { 81 | $this->messageManager->addErrorMessage($this->uiConfig->getGeneralDeleteErrorMessage()); 82 | $resultRedirect->setPath('*/*/edit', [$paramName => $id]); 83 | return $resultRedirect; 84 | } 85 | } 86 | $this->messageManager->addErrorMessage($this->uiConfig->getDeleteMissingEntityMessage()); 87 | $resultRedirect->setPath('*/*/'); 88 | return $resultRedirect; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Edit.php: -------------------------------------------------------------------------------- 1 | entityUiManager = $entityUiManager; 51 | $this->uiConfig = $uiConfig; 52 | parent::__construct($context); 53 | } 54 | 55 | /** 56 | * @return \Magento\Backend\Model\View\Result\Page 57 | */ 58 | public function execute() 59 | { 60 | $id = (int)$this->getRequest()->getParam($this->uiConfig->getRequestParamName()); 61 | $entity = $this->entityUiManager->get($id); 62 | /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ 63 | $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); 64 | $activeMenu = $this->uiConfig->getMenuItem(); 65 | if ($activeMenu) { 66 | $resultPage->setActiveMenu($activeMenu); 67 | } 68 | $resultPage->getConfig()->getTitle()->prepend($this->uiConfig->getListPageTitle()); 69 | if (!$entity->getId()) { 70 | $resultPage->getConfig()->getTitle()->prepend($this->uiConfig->getNewLabel()); 71 | } else { 72 | $resultPage->getConfig()->getTitle()->prepend($entity->getData($this->uiConfig->getNameAttribute())); 73 | } 74 | return $resultPage; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Heartbeat/Index.php: -------------------------------------------------------------------------------- 1 | resultFactory->create(ResultFactory::TYPE_JSON); 38 | $response->setData([]); 39 | return $response; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Index.php: -------------------------------------------------------------------------------- 1 | uiConfig = $uiConfig; 46 | } 47 | 48 | /** 49 | * @return \Magento\Backend\Model\View\Result\Page 50 | */ 51 | public function execute() 52 | { 53 | /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ 54 | $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); 55 | $listMenuItem = $this->uiConfig->getMenuItem(); 56 | if ($listMenuItem) { 57 | $resultPage->setActiveMenu($listMenuItem); 58 | } 59 | $pageTitle = $this->uiConfig->getListPageTitle(); 60 | if ($pageTitle) { 61 | $resultPage->getConfig()->getTitle()->prepend($pageTitle); 62 | } 63 | return $resultPage; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Controller/Adminhtml/InlineEdit.php: -------------------------------------------------------------------------------- 1 | dataProcessor = $dataProcessor; 53 | $this->entityUiManager = $entityUiManager; 54 | parent::__construct($context); 55 | } 56 | 57 | /** 58 | * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\Result\Json 59 | * |\Magento\Framework\Controller\ResultInterface 60 | * @throws \Magento\Framework\Exception\NoSuchEntityException 61 | */ 62 | public function execute() 63 | { 64 | /** @var \Magento\Framework\Controller\Result\Json $resultJson */ 65 | $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); 66 | $error = false; 67 | $messages = []; 68 | 69 | if ($this->getRequest()->getParam('isAjax')) { 70 | $postItems = $this->getRequest()->getParam('items', []); 71 | if (!count($postItems)) { 72 | $messages[] = __('Please correct the data sent.'); 73 | $error = true; 74 | } else { 75 | foreach (array_keys($postItems) as $id) { 76 | try { 77 | $entity = $this->entityUiManager->get($id); 78 | $newData = $this->dataProcessor->modifyData($postItems[$id]); 79 | // phpcs:disable Magento2.Performance.ForeachArrayMerge 80 | $entity->setData(array_merge($entity->getData(), $newData)); 81 | // phpcs:enable 82 | $this->entityUiManager->save($entity); 83 | } catch (\Exception $e) { 84 | $messages[] = '[' . __('Error') . ': ' . $id . '] ' . $e->getMessage(); 85 | $error = true; 86 | } 87 | } 88 | } 89 | } 90 | $resultJson->setData([ 91 | 'messages' => $messages, 92 | 'error' => $error 93 | ]); 94 | return $resultJson; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Controller/Adminhtml/MassDelete.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 69 | $this->collectionProvider = $collectionProvider; 70 | $this->uiConfig = $uiConfig; 71 | $this->uiManager = $uiManager; 72 | parent::__construct($context); 73 | } 74 | 75 | /** 76 | * execute action 77 | * 78 | * @return \Magento\Framework\Controller\Result\Redirect 79 | */ 80 | public function execute() 81 | { 82 | try { 83 | $collection = $this->filter->getCollection($this->collectionProvider->getCollection()); 84 | $collectionSize = $collection->getSize(); 85 | foreach ($collection as $entity) { 86 | $this->uiManager->delete((int)$entity->getId()); 87 | } 88 | $this->messageManager->addSuccessMessage( 89 | $this->uiConfig->getMassDeleteSuccessMessage($collectionSize) 90 | ); 91 | } catch (LocalizedException $e) { 92 | $this->messageManager->addErrorMessage($e->getMessage()); 93 | } catch (\Exception $e) { 94 | $this->messageManager->addErrorMessage($this->uiConfig->getMassDeleteErrorMessage()); 95 | } 96 | /** @var \Magento\Framework\Controller\Result\Redirect $redirectResult */ 97 | $redirectResult = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 98 | $redirectResult->setPath('*/*/index'); 99 | return $redirectResult; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Controller/Adminhtml/NewAction.php: -------------------------------------------------------------------------------- 1 | resultFactory->create(ResultFactory::TYPE_FORWARD); 37 | $forward->forward('edit'); 38 | return $forward; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Controller/Adminhtml/Upload.php: -------------------------------------------------------------------------------- 1 | uploader = $uploader; 47 | parent::__construct($context); 48 | } 49 | 50 | /** 51 | * @return \Magento\Framework\Controller\ResultInterface 52 | */ 53 | public function execute() 54 | { 55 | try { 56 | $result = $this->uploader->saveFileToTmpDir($this->getFieldName()); 57 | } catch (\Exception $e) { 58 | $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; 59 | } 60 | $response = $this->resultFactory->create(ResultFactory::TYPE_JSON); 61 | $response->setData($result); 62 | return $response; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | protected function getFieldName() 69 | { 70 | return $this->getRequest()->getParam('field'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Marius Strajeru 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Model/FileChecker.php: -------------------------------------------------------------------------------- 1 | file = $file; 40 | } 41 | 42 | /** 43 | * @param $destinationFile 44 | * @param int $sparseLevel 45 | * @return string 46 | */ 47 | public function getNewFileName($destinationFile, $sparseLevel = 2) 48 | { 49 | $fileInfo = $this->file->getPathInfo($destinationFile); 50 | if ($this->file->fileExists($destinationFile)) { 51 | $index = 1; 52 | $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension']; 53 | while ($this->file->fileExists($fileInfo['dirname'] . '/' . $baseName)) { 54 | $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension']; 55 | $index++; 56 | } 57 | return $baseName; 58 | } else { 59 | $prefix = $sparseLevel > 0 ? '/' : ''; 60 | $fileName = $fileInfo['filename']; 61 | for ($i = 0; $i < $sparseLevel; $i++) { 62 | $prefix .= ($fileName[$i] ?? '_') . '/'; 63 | } 64 | return $prefix . $fileInfo['basename']; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Model/ResourceModel/AbstractModel.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 61 | $this->metadataPool = $metadataPool; 62 | $this->interfaceClass = $interfaceClass; 63 | parent::__construct($context, $connectionName); 64 | } 65 | 66 | /** 67 | * @param AbstractModel $object 68 | * @return $this 69 | * @throws \Exception 70 | */ 71 | public function save(\Magento\Framework\Model\AbstractModel $object) 72 | { 73 | $this->entityManager->save($object); 74 | return $this; 75 | } 76 | 77 | /** 78 | * @param \Magento\Framework\Model\AbstractModel $object 79 | * @return $this|AbstractDb 80 | * @throws \Exception 81 | */ 82 | public function delete(\Magento\Framework\Model\AbstractModel $object) 83 | { 84 | $this->entityManager->delete($object); 85 | return $this; 86 | } 87 | 88 | /** 89 | * @return false|\Magento\Framework\DB\Adapter\AdapterInterface 90 | * @throws \Exception 91 | */ 92 | public function getConnection() 93 | { 94 | return $this->metadataPool->getMetadata($this->interfaceClass)->getEntityConnection(); 95 | } 96 | 97 | /** 98 | * @param \Magento\Framework\Model\AbstractModel $object 99 | * @param mixed $value 100 | * @param null $field 101 | * @return $this|AbstractDb 102 | * @throws LocalizedException 103 | */ 104 | public function load(\Magento\Framework\Model\AbstractModel $object, $value, $field = null) 105 | { 106 | $this->entityManager->load($object, $value); 107 | return $this; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Model/ResourceModel/Relation/Store/ReadHandler.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 46 | $this->storeIdField = $storeIdField; 47 | } 48 | 49 | /** 50 | * @param object $entity 51 | * @param array $arguments 52 | * @return bool|object 53 | * @throws \Magento\Framework\Exception\LocalizedException 54 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 55 | */ 56 | public function execute($entity, $arguments = []) 57 | { 58 | if ($entity->getId()) { 59 | $stores = $this->resource->lookupStoreIds((int)$entity->getId()); 60 | $entity->setData($this->storeIdField, $stores); 61 | } 62 | return $entity; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Model/ResourceModel/Relation/Store/SaveHandler.php: -------------------------------------------------------------------------------- 1 | metadataPool = $metadataPool; 68 | $this->resource = $resource; 69 | $this->entityType = $entityType; 70 | $this->storeTable = $storeTable; 71 | $this->storeIdField = $storeIdField; 72 | } 73 | 74 | /** 75 | * @param object $entity 76 | * @param array $arguments 77 | * @return object 78 | * @throws \Exception 79 | */ 80 | public function execute($entity, $arguments = []) 81 | { 82 | $entityMetadata = $this->metadataPool->getMetadata($this->entityType); 83 | $linkField = $entityMetadata->getLinkField(); 84 | 85 | $connection = $this->resource->getConnection(); 86 | 87 | $oldStores = $this->resource->lookupStoreIds((int)$entity->getId()); 88 | $newStores = (array)$entity->getData($this->storeIdField); 89 | 90 | $table = $connection->getTableName($this->storeTable); 91 | 92 | $delete = array_diff($oldStores, $newStores); 93 | if ($delete) { 94 | $where = [ 95 | $linkField . ' = ?' => (int)$entity->getData($linkField), 96 | $this->storeIdField . ' IN (?)' => $delete, 97 | ]; 98 | $connection->delete($table, $where); 99 | } 100 | 101 | $insert = array_diff($newStores, $oldStores); 102 | if ($insert) { 103 | $data = []; 104 | foreach ($insert as $storeId) { 105 | $data[] = [ 106 | $linkField => (int)$entity->getData($linkField), 107 | $this->storeIdField => (int)$storeId, 108 | ]; 109 | } 110 | $connection->insertMultiple($table, $data); 111 | } 112 | return $entity; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Model/ResourceModel/Store.php: -------------------------------------------------------------------------------- 1 | getColumnValues($linkField); 43 | if (count($linkedIds)) { 44 | $connection = $collection->getConnection(); 45 | $select = $connection->select()->from(['entity_store' => $collection->getTable($tableName)]) 46 | ->where('entity_store.' . $linkField . ' IN (?)', $linkedIds); 47 | $result = $connection->fetchAll($select); 48 | if ($result) { 49 | $storesData = []; 50 | foreach ($result as $storeData) { 51 | $storesData[$storeData[$linkField]][] = $storeData[$storeIdField]; 52 | } 53 | foreach ($collection as $item) { 54 | $linkedId = $item->getData($linkField); 55 | if (!isset($storesData[$linkedId])) { 56 | continue; 57 | } 58 | $item->setData($storeIdField, $storesData[$linkedId]); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * @param AbstractCollection $collection 66 | * @param $store 67 | * @param string $storeField 68 | * @param bool $withAdmin 69 | */ 70 | public function addStoreFilter(AbstractCollection $collection, $store, $storeField = 'store_id', $withAdmin = true) 71 | { 72 | if (!is_array($store)) { 73 | $store = [$store]; 74 | } 75 | if ($withAdmin) { 76 | $store[] = \Magento\Store\Model\Store::DEFAULT_STORE_ID; 77 | } 78 | $collection->addFilter($storeField, ['in' => $store], 'public'); 79 | } 80 | 81 | /** 82 | * @param AbstractCollection $collection 83 | * @param string $tableName 84 | * @param string $linkField 85 | */ 86 | public function joinStoreRelationTable(AbstractCollection $collection, string $tableName, string $linkField) 87 | { 88 | $collection->getSelect()->join( 89 | ['store_table' => $collection->getTable($tableName)], 90 | 'main_table.' . $linkField . ' = store_table.' . $linkField, 91 | [] 92 | )->group( 93 | 'main_table.' . $linkField 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Umc_Crud Magento 2 module 2 | 3 | ## Purpose: 4 | 5 | This module is intended for Magento 2 developers, in order to reduce the boilerplate code when creating a CRUD extension. 6 | 7 | ## Compatibility 8 | - 2.4.6 9 | - 2.4.5 10 | - 2.4.4 11 | - 2.4.3 12 | - 2.4.2 13 | - 2.4.1 14 | - 2.4.0 15 | - 2.3.5 16 | - 2.3.4 17 | - probably works for versions before 2.3.4, but it's not guaranteed. 18 | ## What it does: 19 | It provides a set of classes and interfaces that can be configured, composed or prerenced in order to avoid writing the same code over and over again. 20 | 21 | Example: 22 | 23 | (Almost) every `Save` controller for an entity does the following: 24 | 25 | - retrieves the data from POST. 26 | - may or may not transform the data received via POST 27 | - creates a new entity or retrieves a requested entity from the database 28 | - assigns the data to the entity from the point above 29 | - persists the entity 30 | - redirects "somewhere" with a success or an error message. 31 | 32 | And the only variables here are 33 | - the entity being added / modified 34 | - the way the data is processed before attaching it to the entity 35 | 36 | This module provides a general admin `Save` controller that has as dependencies a set of other classes / interfaces that have only one of the responsibilities above 37 | - an Entity manager responsible for retrieving the data from db or instantiating a new entity 38 | - a data processor (interface) that processes the data 39 | - an entity config class will contain the details about the entity being processed. 40 | - side objects: a data persistor (which is basically the session) to save the data submitted in case there is an error and you need to redirect back to the form with the previously submitted data prefilled. 41 | 42 | All of these can be configured via `di.xml` for each entity you want to manage. 43 | 44 | This module also adds a few more code generators (similar to the core ones for factory or proxy for example) that will autogenerate repository classes and a few others. 45 | 46 | # Target audience 47 | 48 | - this module is intended for experienced Magento 2 developers that are tired of writing the same thing over and over again. 49 | - this is not intended for junior developers. 50 | - In order to use this you have to have good knowledge of ... 51 | - how a magento CRUD module works 52 | - what is a virtual type 53 | - how DI works in Magento 54 | 55 | ## Advantages in using this module 56 | - less code to write, which means less code to test and less code that can malfunction 57 | - your copy/paste analyzer will stop complaining you have classes that look the same. 58 | - decrease development time. (hopefully) 59 | - you will have a standard way of writing all your CRUD modules (no matter if it's good or bad, at least it is consistent) 60 | - This covers most of the cases you will encounter in your development process. If one of your cases is not covered by this module you can choose not to extend or compose the classes in this module and use your own. 61 | 62 | 63 | ## Disadvantages of using this module 64 | - there is a lot more configuration you need to write (YEAH....xmls). 65 | - makes debugging a little harder. 66 | - adds a new abstractization layer ... or 7. Just kidding. it's 1. 67 | - you add a new dependency to your project and all your CRUD module will depend on this module. 68 | 69 | # Installation: 70 | - Via composer (recommended) 71 | - `composer require "umc/module-crud=*"` 72 | - Manual install 73 | - download a copy from `https://github.com/UltimateModuleCreator/umc-crud` and all the files in `app/code/Umc/Crud`. 74 | 75 | After installation 76 | - run `php bin/magento setup:upgrade [--keep-generated]` 77 | - check if this file exists `app/etc/crud/di.xml`. If it does not exist, run the command `bin/magento umc:crud:deploy`. If you get an error you can copy it from `vendor/umc/module-crud/etc/crud/di.xml` to `app/etc/crud/di.xml`. 78 | 79 | # Documentation 80 | 81 | For more details about how this extension should work, visit https://github.com/UltimateModuleCreator/umc-crud/wiki 82 | 83 | # Donations 84 | 85 | If you really like this and get the hang of it and it saves you a lot of development time, consider a small (or large - I won't mind) donation via PayPal 86 | -------------------------------------------------------------------------------- /Source/Catalog/ProductAttribute.php: -------------------------------------------------------------------------------- 1 | attributeRepository = $attributeRepository; 67 | $this->searchCriteriaBuilder = $searchCriteriaBuilder; 68 | $this->sortOrderBuilder = $sortOrderBuilder; 69 | $this->filters = $filters; 70 | } 71 | 72 | /** 73 | * @return array 74 | */ 75 | public function toOptionArray() 76 | { 77 | if ($this->options === null) { 78 | $this->sortOrderBuilder->setAscendingDirection(); 79 | $this->sortOrderBuilder->setField(ProductAttributeInterface::FRONTEND_LABEL); 80 | $sortOrder = $this->sortOrderBuilder->create(); 81 | $this->searchCriteriaBuilder->addSortOrder($sortOrder); 82 | $this->searchCriteriaBuilder->addFilter(ProductAttributeInterface::FRONTEND_LABEL, '', 'neq'); 83 | foreach ($this->getValidFilters() as $filter) { 84 | $this->searchCriteriaBuilder->addFilter( 85 | $filter['key'], 86 | $filter['value'], 87 | $filter['condition'] ?? 'eq' 88 | ); 89 | } 90 | $this->options = array_map( 91 | function (ProductAttributeInterface $attribute) { 92 | return [ 93 | 'label' => $attribute->getDefaultFrontendLabel(), 94 | 'value' => $attribute->getAttributeCode() 95 | ]; 96 | }, 97 | $this->attributeRepository->getList($this->searchCriteriaBuilder->create())->getItems() 98 | ); 99 | } 100 | return $this->options; 101 | } 102 | 103 | /** 104 | * @return array 105 | */ 106 | private function getValidFilters() 107 | { 108 | return array_filter( 109 | $this->filters, 110 | function ($filter) { 111 | return array_key_exists('key', $filter) && array_key_exists('value', $filter); 112 | } 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Source/Catalog/ProductAttributeOptions.php: -------------------------------------------------------------------------------- 1 | attributeRepository = $attributeRepository; 52 | $this->attributeCode = $attributeCode; 53 | } 54 | 55 | /** 56 | * @return array 57 | */ 58 | public function toOptionArray() 59 | { 60 | if ($this->options === null) { 61 | try { 62 | $attribute = $this->attributeRepository->get($this->attributeCode); 63 | $this->options = array_map( 64 | function (AttributeOptionInterface $option) { 65 | return [ 66 | 'value' => $option->getValue(), 67 | 'label' => $option->getLabel() 68 | ]; 69 | }, 70 | $attribute->getOptions() 71 | ); 72 | } catch (NoSuchEntityException $e) { 73 | $this->options = []; 74 | } 75 | } 76 | return $this->options; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Source/Catalog/ProductAttributeSet.php: -------------------------------------------------------------------------------- 1 | attributeSetRepository = $attributeSetRepository; 60 | $this->searchCriteriaBuilder = $searchCriteriaBuilder; 61 | $this->sortOrderBuilder = $sortOrderBuilder; 62 | } 63 | 64 | /** 65 | * to option array 66 | * 67 | * @return array 68 | */ 69 | public function toOptionArray() 70 | { 71 | if ($this->options === null) { 72 | $this->options = []; 73 | $this->sortOrderBuilder->setField('attribute_set_name'); 74 | $this->sortOrderBuilder->setAscendingDirection(); 75 | $this->searchCriteriaBuilder->addSortOrder( 76 | $this->sortOrderBuilder->create() 77 | ); 78 | $attributeSets = $this->attributeSetRepository->getList( 79 | $this->searchCriteriaBuilder->create() 80 | )->getItems(); 81 | foreach ($attributeSets as $set) { 82 | $this->options[] = [ 83 | 'label' => $set->getAttributeSetName(), 84 | 'value' => $set->getAttributeSetId() 85 | ]; 86 | } 87 | } 88 | return $this->options; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Source/Options.php: -------------------------------------------------------------------------------- 1 | options = $options; 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | public function toOptionArray() 50 | { 51 | if ($this->processed === null) { 52 | $filteredOptions = array_filter( 53 | $this->options, 54 | function ($option) { 55 | if (!is_array($option)) { 56 | return false; 57 | } 58 | return array_key_exists('label', $option) 59 | && array_key_exists('value', $option) 60 | && (!array_key_exists('disabled', $option) || !$option['disabled']); 61 | } 62 | ); 63 | $this->processed = array_values( 64 | array_map( 65 | function ($option) { 66 | return [ 67 | 'label' => $option['label'], 68 | 'value' => $option['value'] 69 | ]; 70 | }, 71 | $filteredOptions 72 | ) 73 | ); 74 | } 75 | return $this->processed; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Source/StoreView.php: -------------------------------------------------------------------------------- 1 | options !== null) { 42 | return $this->options; 43 | } 44 | 45 | $this->currentOptions['All Store Views']['label'] = __('All Store Views'); 46 | $this->currentOptions['All Store Views']['value'] = self::ALL_STORE_VIEWS; 47 | 48 | $this->generateCurrentOptions(); 49 | 50 | $this->options = array_values($this->currentOptions); 51 | 52 | return $this->options; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Test/Unit/Block/Adminhtml/Button/BackTest.php: -------------------------------------------------------------------------------- 1 | url = $this->createMock(UrlInterface::class); 47 | $this->uiConfig = $this->createMock(EntityUiConfig::class); 48 | } 49 | 50 | /** 51 | * @covers \Umc\Crud\Block\Adminhtml\Button\Back::getButtonData 52 | * @covers \Umc\Crud\Block\Adminhtml\Button\Back::getLabel 53 | * @covers \Umc\Crud\Block\Adminhtml\Button\Back::__construct 54 | */ 55 | public function testGetButtonData() 56 | { 57 | $back = new Back($this->url, $this->uiConfig); 58 | $this->uiConfig->method('getBackLabel')->willReturn('Back to list'); 59 | $this->url->method('getUrl')->willReturn('url'); 60 | $result = $back->getButtonData(); 61 | $this->assertEquals('Back to list', $result['label']); 62 | $this->assertEquals("location.href = 'url';", $result['on_click']); 63 | } 64 | 65 | /** 66 | * @covers \Umc\Crud\Block\Adminhtml\Button\Back::getButtonData 67 | * @covers \Umc\Crud\Block\Adminhtml\Button\Back::getLabel 68 | * @covers \Umc\Crud\Block\Adminhtml\Button\Back::__construct 69 | */ 70 | public function testGetButtonDataNoUiConfig() 71 | { 72 | $back = new Back($this->url); 73 | $this->url->method('getUrl')->willReturn('url'); 74 | $result = $back->getButtonData(); 75 | $this->assertEquals('Back', $result['label']); 76 | $this->assertEquals("location.href = 'url';", $result['on_click']); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Test/Unit/Block/Adminhtml/Button/DeleteTest.php: -------------------------------------------------------------------------------- 1 | request = $this->createMock(RequestInterface::class); 63 | $this->entityUiManager = $this->createMock(EntityUiManagerInterface::class); 64 | $this->uiConfig = $this->createMock(EntityUiConfig::class); 65 | $this->url = $this->createMock(UrlInterface::class); 66 | $this->delete = new Delete( 67 | $this->request, 68 | $this->entityUiManager, 69 | $this->uiConfig, 70 | $this->url 71 | ); 72 | } 73 | 74 | /** 75 | * @covers \Umc\Crud\Block\Adminhtml\Button\Delete::getButtonData 76 | * @covers \Umc\Crud\Block\Adminhtml\Button\Delete::getDeleteUrl 77 | * @covers \Umc\Crud\Block\Adminhtml\Button\Delete::getEntityId 78 | * @covers \Umc\Crud\Block\Adminhtml\Button\Delete::__construct 79 | */ 80 | public function testGetButtonData() 81 | { 82 | $entity = $this->createMock(AbstractModel::class); 83 | $entity->method('getId')->willReturn(1); 84 | $this->entityUiManager->method('get')->willReturn($entity); 85 | $this->url->expects($this->once())->method('getUrl')->willReturn('url'); 86 | $result = $this->delete->getButtonData(); 87 | $this->assertArrayHasKey('label', $result); 88 | $this->assertArrayHasKey('on_click', $result); 89 | } 90 | 91 | /** 92 | * @covers \Umc\Crud\Block\Adminhtml\Button\Delete::getButtonData 93 | * @covers \Umc\Crud\Block\Adminhtml\Button\Delete::getEntityId 94 | * @covers \Umc\Crud\Block\Adminhtml\Button\Delete::__construct 95 | */ 96 | public function testGetButtonNoEntity() 97 | { 98 | $this->entityUiManager->method('get')->willThrowException( 99 | $this->createMock(NoSuchEntityException::class) 100 | ); 101 | $this->url->expects($this->never())->method('getUrl'); 102 | $this->assertEquals([], $this->delete->getButtonData()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Test/Unit/Block/Adminhtml/Button/ResetTest.php: -------------------------------------------------------------------------------- 1 | getButtonData(); 36 | $this->assertEquals(__('Reset'), $result['label']); 37 | $this->assertEquals("location.reload();", $result['on_click']); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Test/Unit/Controller/Heartbeat/IndexTest.php: -------------------------------------------------------------------------------- 1 | createMock(Context::class); 38 | $resultFactory = $this->createMock(ResultFactory::class); 39 | $jsonResult = $this->createMock(Json::class); 40 | $context->expects($this->once())->method('getResultFactory')->willReturn($resultFactory); 41 | $resultFactory->expects($this->once())->method('create')->willReturn($jsonResult); 42 | $jsonResult->expects($this->once())->method('setData')->with([]); 43 | $index = new Index($context); 44 | $this->assertEquals($jsonResult, $index->execute()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Test/Unit/Controller/IndexTest.php: -------------------------------------------------------------------------------- 1 | context = $this->createMock(Context::class); 71 | $this->uiConfig = $this->createMock(EntityUiConfig::class); 72 | $this->resultPage = $this->createMock(Page::class); 73 | $this->pageConfig = $this->createMock(Config::class); 74 | $this->pageTitle = $this->createMock(Title::class); 75 | $this->resultFactory = $this->createMock(ResultFactory::class); 76 | $this->context->method('getResultFactory')->willReturn($this->resultFactory); 77 | $this->pageConfig->method('getTitle')->willReturn($this->pageTitle); 78 | $this->resultFactory->method('create')->willReturn($this->resultPage); 79 | $this->resultPage->method('getConfig')->willReturn($this->pageConfig); 80 | $this->index = new Index( 81 | $this->context, 82 | $this->uiConfig 83 | ); 84 | } 85 | 86 | /** 87 | * @covers \Umc\Crud\Controller\Adminhtml\Index::execute 88 | * @covers \Umc\Crud\Controller\Adminhtml\Index::_isAllowed 89 | * @covers \Umc\Crud\Controller\Adminhtml\Index::__construct 90 | */ 91 | public function testExecute() 92 | { 93 | $this->uiConfig->expects($this->once())->method('getMenuItem')->willReturn('SelectedMenu'); 94 | $this->uiConfig->expects($this->once())->method('getListPageTitle')->willReturn('PageTitle'); 95 | $this->resultPage->expects($this->once())->method('setActiveMenu')->with('SelectedMenu'); 96 | $this->pageTitle->expects($this->once())->method('prepend')->with('PageTitle'); 97 | $this->assertEquals($this->resultPage, $this->index->execute()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Test/Unit/Controller/NewActionTest.php: -------------------------------------------------------------------------------- 1 | createMock(Context::class); 38 | $resultFactory = $this->createMock(ResultFactory::class); 39 | $forward = $this->createMock(Forward::class); 40 | $context->expects($this->once())->method('getResultFactory')->willReturn($resultFactory); 41 | $resultFactory->expects($this->once())->method('create')->willReturn($forward); 42 | $newAction = new NewAction($context); 43 | $forward->expects($this->once())->method('forward')->with('edit'); 44 | $this->assertEquals($forward, $newAction->execute()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Test/Unit/Controller/UploadTest.php: -------------------------------------------------------------------------------- 1 | context = $this->createMock(Context::class); 66 | $this->uploader = $this->createMock(Uploader::class); 67 | $this->request = $this->createMock(RequestInterface::class); 68 | $this->resultFactory = $this->createMock(ResultFactory::class); 69 | $this->resultJson = $this->createMock(Json::class); 70 | $this->resultFactory->method('create')->willReturn($this->resultJson); 71 | $this->context->method('getRequest')->willReturn($this->request); 72 | $this->context->method('getResultFactory')->willReturn($this->resultFactory); 73 | $this->upload = new Upload( 74 | $this->context, 75 | $this->uploader 76 | ); 77 | } 78 | 79 | /** 80 | * @covers \Umc\Crud\Controller\Adminhtml\Upload::execute 81 | * @covers \Umc\Crud\Controller\Adminhtml\Upload::getFieldName 82 | * @covers \Umc\Crud\Controller\Adminhtml\Upload::__construct 83 | */ 84 | public function testExecute() 85 | { 86 | $this->uploader->method('saveFileToTmpDir')->willReturn(['result']); 87 | $this->resultJson->expects($this->once())->method('setData')->with(['result']); 88 | $this->assertEquals($this->resultJson, $this->upload->execute()); 89 | } 90 | 91 | /** 92 | * @covers \Umc\Crud\Controller\Adminhtml\Upload::execute 93 | * @covers \Umc\Crud\Controller\Adminhtml\Upload::getFieldName 94 | * @covers \Umc\Crud\Controller\Adminhtml\Upload::__construct 95 | */ 96 | public function testExecuteWithException() 97 | { 98 | $this->uploader->method('saveFileToTmpDir')->willThrowException(new \Exception()); 99 | $this->assertEquals($this->resultJson, $this->upload->execute()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Test/Unit/Generator/RepoTest.php: -------------------------------------------------------------------------------- 1 | nameMatcher = $this->createMock(NameMatcher::class); 67 | $this->ioObject = $this->createMock(Io::class); 68 | $this->classGenerator = $this->createMock(CodeGeneratorInterface::class); 69 | $this->definedClasses = $this->createMock(DefinedClasses::class); 70 | $this->parameter = $this->createMock(ReflectionParameter::class); 71 | $this->repo = new Repo( 72 | $this->nameMatcher, 73 | DataObject::class, 74 | '\Result\Class', 75 | $this->ioObject, 76 | $this->classGenerator, 77 | $this->definedClasses 78 | ); 79 | } 80 | 81 | /** 82 | * @covers \Umc\Crud\Generator\Repo 83 | */ 84 | public function testGenerate() 85 | { 86 | $this->ioObject->expects($this->once())->method('generateResultFileName')->willReturn('filename.php'); 87 | $this->nameMatcher->expects($this->any())->method('getRepositoryInterfaceName'); 88 | $this->nameMatcher->expects($this->any())->method('getInterfaceName'); 89 | $this->nameMatcher->expects($this->any())->method('getInterfaceFactoryClass'); 90 | $this->nameMatcher->expects($this->any())->method('getResourceClassName'); 91 | $this->definedClasses->method('isClassLoadable')->willReturn(true); 92 | $this->ioObject->method('makeResultFileDirectory')->willReturn(true); 93 | $this->ioObject->method('fileExists')->willReturn(true); 94 | $this->classGenerator->expects($this->once())->method('setName')->willReturnSelf(); 95 | $this->classGenerator->expects($this->once())->method('addProperties')->willReturnSelf(); 96 | $this->classGenerator->expects($this->once())->method('addMethods')->willReturnSelf(); 97 | $this->classGenerator->expects($this->once())->method('setClassDocBlock')->willReturnSelf(); 98 | $this->classGenerator->expects($this->once())->method('generate')->willReturn('generated code'); 99 | $this->assertEquals('filename.php', $this->repo->generate()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Test/Unit/Generator/UiCollectionProviderTest.php: -------------------------------------------------------------------------------- 1 | nameMatcher = $this->createMock(NameMatcher::class); 67 | $this->ioObject = $this->createMock(Io::class); 68 | $this->classGenerator = $this->createMock(CodeGeneratorInterface::class); 69 | $this->definedClasses = $this->createMock(DefinedClasses::class); 70 | $this->parameter = $this->createMock(ReflectionParameter::class); 71 | $this->uiCollectionProvider = new UiCollectionProvider( 72 | $this->nameMatcher, 73 | DataObject::class, 74 | '\Result\Class', 75 | $this->ioObject, 76 | $this->classGenerator, 77 | $this->definedClasses 78 | ); 79 | } 80 | 81 | /** 82 | * @covers \Umc\Crud\Generator\UiCollectionProvider 83 | */ 84 | public function testGenerate() 85 | { 86 | $this->ioObject->expects($this->once())->method('generateResultFileName')->willReturn('filename.php'); 87 | $this->nameMatcher->expects($this->any())->method('getCollectionFactoryClass'); 88 | $this->definedClasses->method('isClassLoadable')->willReturn(true); 89 | $this->ioObject->method('makeResultFileDirectory')->willReturn(true); 90 | $this->ioObject->method('fileExists')->willReturn(true); 91 | $this->classGenerator->expects($this->once())->method('setName')->willReturnSelf(); 92 | $this->classGenerator->expects($this->once())->method('addProperties')->willReturnSelf(); 93 | $this->classGenerator->expects($this->once())->method('addMethods')->willReturnSelf(); 94 | $this->classGenerator->expects($this->once())->method('setClassDocBlock')->willReturnSelf(); 95 | $this->classGenerator->expects($this->once())->method('generate')->willReturn('generated code'); 96 | $this->assertEquals('filename.php', $this->uiCollectionProvider->generate()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Test/Unit/Generator/UiManagerTest.php: -------------------------------------------------------------------------------- 1 | nameMatcher = $this->createMock(NameMatcher::class); 67 | $this->ioObject = $this->createMock(Io::class); 68 | $this->classGenerator = $this->createMock(CodeGeneratorInterface::class); 69 | $this->definedClasses = $this->createMock(DefinedClasses::class); 70 | $this->parameter = $this->createMock(ReflectionParameter::class); 71 | $this->uiManager = new UiManager( 72 | $this->nameMatcher, 73 | DataObject::class, 74 | '\Result\Class', 75 | $this->ioObject, 76 | $this->classGenerator, 77 | $this->definedClasses 78 | ); 79 | } 80 | 81 | /** 82 | * @covers \Umc\Crud\Generator\UiManager 83 | */ 84 | public function testGenerate() 85 | { 86 | $this->ioObject->expects($this->once())->method('generateResultFileName')->willReturn('filename.php'); 87 | $this->nameMatcher->expects($this->any())->method('getInterfaceName'); 88 | $this->definedClasses->method('isClassLoadable')->willReturn(true); 89 | $this->ioObject->method('makeResultFileDirectory')->willReturn(true); 90 | $this->ioObject->method('fileExists')->willReturn(true); 91 | $this->classGenerator->expects($this->once())->method('setName')->willReturnSelf(); 92 | $this->classGenerator->expects($this->once())->method('addProperties')->willReturnSelf(); 93 | $this->classGenerator->expects($this->once())->method('addMethods')->willReturnSelf(); 94 | $this->classGenerator->expects($this->once())->method('setClassDocBlock')->willReturnSelf(); 95 | $this->classGenerator->expects($this->once())->method('generate')->willReturn('generated code'); 96 | $this->assertEquals('filename.php', $this->uiManager->generate()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Test/Unit/Model/FileCheckerTest.php: -------------------------------------------------------------------------------- 1 | file = $this->createMock(File::class); 46 | $this->fileChecker = new FileChecker( 47 | $this->file 48 | ); 49 | } 50 | 51 | /** 52 | * @covers \Umc\Crud\Model\FileChecker::getNewFileName 53 | * @covers \Umc\Crud\Model\FileChecker::__construct 54 | */ 55 | public function testGetNewFileName() 56 | { 57 | $this->file->method('getPathInfo')->willReturn([ 58 | 'filename' => 'file', 59 | 'extension' => 'ext', 60 | 'basename' => 'file.ext', 61 | 'dirname' => 'dir' 62 | ]); 63 | $this->file->method('fileExists')->willReturn(false); 64 | $this->assertEquals('file.ext', $this->fileChecker->getNewFileName('file', 0)); 65 | $this->assertEquals('/f/i/file.ext', $this->fileChecker->getNewFileName('file')); 66 | $this->assertEquals('/f/i/l/e/_/_/file.ext', $this->fileChecker->getNewFileName('file', 6)); 67 | } 68 | 69 | /** 70 | * @covers \Umc\Crud\Model\FileChecker::getNewFileName 71 | * @covers \Umc\Crud\Model\FileChecker::__construct 72 | */ 73 | public function testGetNewFileNameFileExists() 74 | { 75 | $this->file->method('getPathInfo')->willReturn([ 76 | 'filename' => 'file', 77 | 'extension' => 'ext', 78 | 'basename' => 'file.ext', 79 | 'dirname' => 'dir' 80 | ]); 81 | $this->file->method('fileExists')->willReturnOnConsecutiveCalls(true, true, false); 82 | $this->assertEquals('file_1.ext', $this->fileChecker->getNewFileName('file', 0)); 83 | } 84 | 85 | /** 86 | * @covers \Umc\Crud\Model\FileChecker::getNewFileName 87 | * @covers \Umc\Crud\Model\FileChecker::__construct 88 | */ 89 | public function testGetNewFileNameFileExistsThreeLevels() 90 | { 91 | $this->file->method('getPathInfo')->willReturn([ 92 | 'filename' => 'file', 93 | 'extension' => 'ext', 94 | 'basename' => 'file.ext', 95 | 'dirname' => 'dir' 96 | ]); 97 | $this->file->method('fileExists')->willReturnOnConsecutiveCalls(true, true, true, true, false); 98 | $this->assertEquals('file_3.ext', $this->fileChecker->getNewFileName('file', 0)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Test/Unit/Model/ResourceModel/Relation/Store/ReadHandlerTest.php: -------------------------------------------------------------------------------- 1 | resource = $this->createMock(StoreAwareAbstractModel::class); 47 | $this->readHandler = new ReadHandler($this->resource); 48 | } 49 | 50 | /** 51 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\ReadHandler::execute 52 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\ReadHandler::__construct 53 | */ 54 | public function testExecute() 55 | { 56 | $entity = $this->createMock(AbstractModel::class); 57 | $entity->method('getId')->willReturn(1); 58 | $this->resource->expects($this->once())->method('lookupStoreIds')->willReturn([1, 3]); 59 | $entity->expects($this->once())->method('setData')->with('store_id', [1, 3]); 60 | $this->assertEquals($entity, $this->readHandler->execute($entity)); 61 | } 62 | 63 | /** 64 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\ReadHandler::execute 65 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\ReadHandler::__construct 66 | */ 67 | public function testExecuteNoId() 68 | { 69 | $entity = $this->createMock(AbstractModel::class); 70 | $entity->method('getId')->willReturn(null); 71 | $this->resource->expects($this->never())->method('lookupStoreIds'); 72 | $entity->expects($this->never())->method('setData'); 73 | $this->assertEquals($entity, $this->readHandler->execute($entity)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Test/Unit/Model/ResourceModel/Relation/Store/SaveHandlerTest.php: -------------------------------------------------------------------------------- 1 | metadataPool = $this->createMock(MetadataPool::class); 66 | $this->resource = $this->createMock(StoreAwareAbstractModel::class); 67 | $this->metadata = $this->createMock(EntityMetadataInterface::class); 68 | $this->metadataPool->method('getMetadata')->willReturn($this->metadata); 69 | $this->connection = $this->createMock(AdapterInterface::class); 70 | $this->resource->method('getConnection')->willReturn($this->connection); 71 | $this->entity = $this->createMock(AbstractModel::class); 72 | $this->saveHandler = new SaveHandler( 73 | $this->metadataPool, 74 | $this->resource, 75 | 'entityType', 76 | 'store_table', 77 | 'store_id' 78 | ); 79 | } 80 | 81 | /** 82 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\SaveHandler::execute 83 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\SaveHandler::__construct 84 | */ 85 | public function testExecute() 86 | { 87 | $this->metadata->method('getLinkField')->willReturn('entity_id'); 88 | $this->resource->method('lookupStoreIds')->willReturn([1, 2, 3]); 89 | $this->entity->method('getData')->willReturnMap([ 90 | ['store_id', null, [1, 2, 4, 5]], 91 | ['entity_id', null, 1] 92 | ]); 93 | $this->connection->expects($this->once())->method('delete'); 94 | $this->connection->expects($this->once())->method('insertMultiple'); 95 | $this->saveHandler->execute($this->entity); 96 | } 97 | 98 | /** 99 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\SaveHandler::execute 100 | * @covers \Umc\Crud\Model\ResourceModel\Relation\Store\SaveHandler::__construct 101 | */ 102 | public function testExecuteNoInsert() 103 | { 104 | $this->resource->method('lookupStoreIds')->willReturn([1, 2, 3]); 105 | $this->entity->method('getData')->willReturnMap([ 106 | ['store_id', null, [1, 2]], 107 | ['entity_id', null, 1] 108 | ]); 109 | $this->connection->expects($this->once())->method('delete'); 110 | $this->connection->expects($this->never())->method('insertMultiple'); 111 | $this->saveHandler->execute($this->entity); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Test/Unit/Model/ResourceModel/StoreTest.php: -------------------------------------------------------------------------------- 1 | collection = $om->getCollectionMock( 59 | AbstractCollection::class, 60 | [ 61 | $this->getItemMock('1'), 62 | $this->getItemMock('2'), 63 | $this->getItemMock('3') 64 | ] 65 | ); 66 | $this->connection = $this->createMock(AdapterInterface::class); 67 | $this->collection->method('getConnection')->willReturn($this->connection); 68 | $this->select = $this->createMock(Select::class); 69 | $this->store = new Store(); 70 | } 71 | 72 | /** 73 | * @covers \Umc\Crud\Model\ResourceModel\Store::addStoresToCollection 74 | */ 75 | public function testAddStoresToCollection() 76 | { 77 | $this->collection->method('getColumnValues')->willReturn([1, 2, 3]); 78 | $this->connection->method('select')->willReturn($this->select); 79 | $this->select->expects($this->once())->method('from')->willReturnSelf(); 80 | $this->select->expects($this->once())->method('where')->willReturnSelf(); 81 | $this->connection->method('fetchAll')->willReturn([ 82 | [ 83 | 'store_id' => 1, 84 | 'linked_id' => 1 85 | ], 86 | [ 87 | 'store_id' => 2, 88 | 'linked_id' => 1 89 | ], 90 | [ 91 | 'store_id' => 2, 92 | 'linked_id' => 2 93 | ], 94 | ]); 95 | $this->store->addStoresToCollection($this->collection, 'table', 'linked_id'); 96 | } 97 | 98 | /** 99 | * @covers \Umc\Crud\Model\ResourceModel\Store::addStoreFilter 100 | */ 101 | public function testAddStoreFilter() 102 | { 103 | $this->collection->expects($this->once())->method('addFilter')->with('store_id', ['in' => [1, 0]], 'public'); 104 | $this->store->addStoreFilter($this->collection, 1); 105 | } 106 | 107 | /** 108 | * @covers \Umc\Crud\Model\ResourceModel\Store::joinStoreRelationTable 109 | */ 110 | public function testJoinStoreRelationTable() 111 | { 112 | $this->collection->method('getSelect')->willReturn($this->select); 113 | $this->select->expects($this->once())->method('join')->willReturnSelf(); 114 | $this->select->expects($this->once())->method('group'); 115 | $this->store->joinStoreRelationTable($this->collection, 'table', 'link_id'); 116 | } 117 | 118 | /** 119 | * @param $linkedField 120 | * @return MockObject 121 | */ 122 | private function getItemMock($linkedField) 123 | { 124 | $mock = $this->createMock(AbstractModel::class); 125 | $mock->method('getData')->willReturn($linkedField); 126 | return $mock; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Test/Unit/Source/Catalog/ProductAttributeOptionsTest.php: -------------------------------------------------------------------------------- 1 | attributeRepository = $this->createMock(ProductAttributeRepositoryInterface::class); 53 | $this->attribute = $this->createMock(ProductAttributeInterface::class); 54 | $this->productAttributeOptions = new ProductAttributeOptions( 55 | $this->attributeRepository, 56 | 'attributeCode' 57 | ); 58 | } 59 | 60 | /** 61 | * @covers \Umc\Crud\Source\Catalog\ProductAttributeOptions::toOptionArray 62 | * @covers \Umc\Crud\Source\Catalog\ProductAttributeOptions::__construct 63 | */ 64 | public function testToOptionArray() 65 | { 66 | $this->attributeRepository->expects($this->once())->method('get')->willReturn($this->attribute); 67 | $this->attribute->expects($this->once())->method('getOptions')->willReturn([ 68 | $this->getOptionMock(1, 'label1'), 69 | $this->getOptionMock(2, 'label2'), 70 | ]); 71 | $expected = [ 72 | [ 73 | 'label' => 'label1', 74 | 'value' => 1 75 | ], 76 | [ 77 | 'label' => 'label2', 78 | 'value' => 2 79 | ] 80 | ]; 81 | $this->assertEquals($expected, $this->productAttributeOptions->toOptionArray()); 82 | //call twice to test memoizing 83 | $this->assertEquals($expected, $this->productAttributeOptions->toOptionArray()); 84 | } 85 | 86 | /** 87 | * @covers \Umc\Crud\Source\Catalog\ProductAttributeOptions::toOptionArray 88 | * @covers \Umc\Crud\Source\Catalog\ProductAttributeOptions::__construct 89 | */ 90 | public function testToOptionArrayNoAttribute() 91 | { 92 | $this->attributeRepository->expects($this->once())->method('get')->willThrowException( 93 | $this->createMock(NoSuchEntityException::class) 94 | ); 95 | $this->attribute->expects($this->never())->method('getOptions'); 96 | $this->assertEquals([], $this->productAttributeOptions->toOptionArray()); 97 | //call twice to test memoizing 98 | $this->assertEquals([], $this->productAttributeOptions->toOptionArray()); 99 | } 100 | 101 | /** 102 | * @param $value 103 | * @param $label 104 | * @return MockObject 105 | */ 106 | private function getOptionMock($value, $label) 107 | { 108 | $mock = $this->createMock(AttributeOptionInterface::class); 109 | $mock->method('getValue')->willReturn($value); 110 | $mock->method('getLabel')->willReturn($label); 111 | return $mock; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Test/Unit/Source/OptionsTest.php: -------------------------------------------------------------------------------- 1 | 'label1', 38 | 'value' => 'value1' 39 | ], 40 | [ 41 | 'label' => 'label2', 42 | 'value' => 'value2', 43 | 'disabled' => true 44 | ], 45 | [ 46 | 'value' => 'value3', 47 | 'disabled' => false 48 | ], 49 | [ 50 | 'label' => 'label4', 51 | ], 52 | [ 53 | 'label' => 'label5', 54 | 'value' => 'value5', 55 | 'disabled' => false 56 | ], 57 | 'dummy' 58 | ]; 59 | $expected = [ 60 | [ 61 | 'label' => 'label1', 62 | 'value' => 'value1' 63 | ], 64 | [ 65 | 'label' => 'label5', 66 | 'value' => 'value5' 67 | ], 68 | ]; 69 | $options = new Options($input); 70 | $this->assertEquals($expected, $options->toOptionArray()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Test/Unit/Source/StoreViewTest.php: -------------------------------------------------------------------------------- 1 | systemStore = $this->createMock(Store::class); 49 | $escaper = $this->createMock(Escaper::class); 50 | $this->storeView = new StoreView( 51 | $this->systemStore, 52 | $escaper 53 | ); 54 | $escaper->method('escapeJs')->willReturnArgument(0); 55 | $escaper->method('escapeHtml')->willReturnArgument(0); 56 | } 57 | 58 | /** 59 | * @covers \Umc\Crud\Source\StoreView 60 | */ 61 | public function testToOptionArray() 62 | { 63 | $this->systemStore->expects($this->once())->method('getWebsiteCollection')->willReturn([ 64 | $this->getWebsiteMock(1, 'website1'), 65 | $this->getWebsiteMock(2, 'website2') 66 | ]); 67 | $this->systemStore->expects($this->once())->method('getGroupCollection')->willReturn([ 68 | $this->getGroupMock(11, 1, 'group1'), 69 | $this->getGroupMock(12, 1, 'group2'), 70 | ]); 71 | $this->systemStore->expects($this->once())->method('getStoreCollection')->willReturn([ 72 | $this->getStoreViewMock(1, 11, 'storeview1'), 73 | $this->getStoreViewMock(2, 12, 'storeview2'), 74 | ]); 75 | $this->storeView->toOptionArray(); 76 | //call twice to test memoizing 77 | $result = $this->storeView->toOptionArray(); 78 | $this->assertEquals(2, count($result)); 79 | $this->assertEquals(0, $result[0]['value']); 80 | $this->assertEquals(2, count($result[1]['value'])); 81 | } 82 | 83 | /** 84 | * @param $id 85 | * @param $name 86 | * @return MockObject 87 | */ 88 | private function getWebsiteMock($id, $name) 89 | { 90 | $mock = $this->createMock(Website::class); 91 | $mock->method('getId')->willReturn($id); 92 | $mock->method('getName')->willReturn($name); 93 | return $mock; 94 | } 95 | 96 | /** 97 | * @param $id 98 | * @param $websiteId 99 | * @param $name 100 | * @return MockObject 101 | */ 102 | private function getGroupMock($id, $websiteId, $name) 103 | { 104 | $mock = $this->createMock(Group::class); 105 | $mock->method('getId')->willReturn($id); 106 | $mock->method('getWebsiteId')->willReturn($websiteId); 107 | $mock->method('getName')->willReturn($name); 108 | return $mock; 109 | } 110 | 111 | /** 112 | * @param $id 113 | * @param $groupId 114 | * @param $name 115 | * @return MockObject 116 | */ 117 | private function getStoreViewMock($id, $groupId, $name) 118 | { 119 | $mock = $this->createMock(\Magento\Store\Model\Store::class); 120 | $mock->method('getId')->willReturn($id); 121 | $mock->method('getGroupId')->willReturn($groupId); 122 | $mock->method('getName')->willReturn($name); 123 | return $mock; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Test/Unit/Ui/Form/DataModifier/CompositeDataModifierTest.php: -------------------------------------------------------------------------------- 1 | createMock(AbstractModel::class); 38 | $processor1 = $this->createMock(DataModifierInterface::class); 39 | $processor1->method('modifyData')->willReturnCallback( 40 | function (AbstractModel $model, array $data) { 41 | $data['element1'] = ($data['element1'] ?? '') . '_processed1'; 42 | return $data; 43 | } 44 | ); 45 | $processor2 = $this->createMock(DataModifierInterface::class); 46 | $processor2->method('modifyData')->willReturnCallback( 47 | function (AbstractModel $model, array $data) { 48 | $data['element1'] = ($data['element1'] ?? '') . '_processed2'; 49 | $data['element2'] = ($data['element2'] ?? '') . '_processed2'; 50 | return $data; 51 | } 52 | ); 53 | $compositeProcessor = new CompositeDataModifier([$processor1, $processor2]); 54 | $data = [ 55 | 'element1' => 'value1', 56 | 'element2' => 'value2', 57 | 'element3' => 'value3' 58 | ]; 59 | $expected = [ 60 | 'element1' => 'value1_processed1_processed2', 61 | 'element2' => 'value2_processed2', 62 | 'element3' => 'value3' 63 | ]; 64 | $this->assertEquals($expected, $compositeProcessor->modifyData($model, $data)); 65 | } 66 | 67 | /** 68 | * @covers \Umc\Crud\Ui\Form\DataModifier\CompositeDataModifier::__construct 69 | */ 70 | public function testGetConstructor() 71 | { 72 | $this->expectException(\InvalidArgumentException::class); 73 | new CompositeDataModifier(['string value']); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Test/Unit/Ui/Form/DataModifier/DataModifierTest.php: -------------------------------------------------------------------------------- 1 | serializer = $this->createMock(Json::class); 51 | $this->model = $this->createMock(AbstractModel::class); 52 | $this->dynamicRows = new DynamicRows( 53 | $this->serializer, 54 | ['field1', 'field2', 'field3'] 55 | ); 56 | } 57 | 58 | /** 59 | * @covers \Umc\Crud\Ui\Form\DataModifier\DynamicRows::modifyData 60 | * @covers \Umc\Crud\Ui\Form\DataModifier\DynamicRows::__construct 61 | */ 62 | public function testModifyData() 63 | { 64 | $data = [ 65 | 'field1' => 'value1', 66 | 'field2' => ['value2'], 67 | 'dummy' => 'dummy' 68 | ]; 69 | $this->serializer->expects($this->once())->method('unserialize')->willReturnCallback(function ($item) { 70 | return [$item]; 71 | }); 72 | $expected = [ 73 | 'field1' => ['value1'], 74 | 'field2' => ['value2'], 75 | 'dummy' => 'dummy' 76 | ]; 77 | $this->assertEquals($expected, $this->dynamicRows->modifyData($this->model, $data)); 78 | } 79 | 80 | /** 81 | * @covers \Umc\Crud\Ui\Form\DataModifier\DynamicRows::modifyData 82 | * @covers \Umc\Crud\Ui\Form\DataModifier\DynamicRows::__construct 83 | */ 84 | public function testModifyDataWithUnserializeError() 85 | { 86 | $data = [ 87 | 'field1' => 'value1', 88 | 'field2' => ['value2'], 89 | 'dummy' => 'dummy' 90 | ]; 91 | $this->serializer->expects($this->once())->method('unserialize')->willThrowException(new \Exception()); 92 | $expected = [ 93 | 'field1' => [], 94 | 'field2' => ['value2'], 95 | 'dummy' => 'dummy' 96 | ]; 97 | $this->assertEquals($expected, $this->dynamicRows->modifyData($this->model, $data)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Test/Unit/Ui/Form/DataModifier/MultiselectTest.php: -------------------------------------------------------------------------------- 1 | createMock(AbstractModel::class); 36 | $modifier = new Multiselect(['field1', 'field2', 'field3', 'field4']); 37 | $data = [ 38 | 'field1' => '1,2,3', 39 | 'field2' => [3, 4, 5], 40 | 'field3' => null, 41 | 'dummy' => 'dummy' 42 | ]; 43 | $expected = [ 44 | 'field1' => [1, 2, 3], 45 | 'field2' => [3, 4, 5], 46 | 'field3' => [], 47 | 'dummy' => 'dummy' 48 | ]; 49 | $this->assertEquals($expected, $modifier->modifyData($model, $data)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Test/Unit/Ui/Form/DataModifier/NullModifierTest.php: -------------------------------------------------------------------------------- 1 | createMock(AbstractModel::class); 36 | $data = ['dummy']; 37 | $this->assertEquals($data, (new NullModifier())->modifyData($model, $data)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Test/Unit/Ui/SaveDataProcessor/CompositeProcessorTest.php: -------------------------------------------------------------------------------- 1 | createMock(SaveDataProcessorInterface::class); 37 | $processor1->method('modifyData')->willReturnCallback(function (array $data) { 38 | $data['element1'] = ($data['element1'] ?? '') . '_processed1'; 39 | return $data; 40 | }); 41 | $processor2 = $this->createMock(SaveDataProcessorInterface::class); 42 | $processor2->method('modifyData')->willReturnCallback(function (array $data) { 43 | $data['element1'] = ($data['element1'] ?? '') . '_processed2'; 44 | $data['element2'] = ($data['element2'] ?? '') . '_processed2'; 45 | return $data; 46 | }); 47 | $compositeProcessor = new CompositeProcessor([$processor1, $processor2]); 48 | $data = [ 49 | 'element1' => 'value1', 50 | 'element2' => 'value2', 51 | 'element3' => 'value3' 52 | ]; 53 | $expected = [ 54 | 'element1' => 'value1_processed1_processed2', 55 | 'element2' => 'value2_processed2', 56 | 'element3' => 'value3' 57 | ]; 58 | $this->assertEquals($expected, $compositeProcessor->modifyData($data)); 59 | } 60 | 61 | /** 62 | * @covers \Umc\Crud\Ui\SaveDataProcessor\CompositeProcessor::__construct 63 | */ 64 | public function testGetConstructor() 65 | { 66 | $this->expectException(\InvalidArgumentException::class); 67 | new CompositeProcessor(['string value']); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Test/Unit/Ui/SaveDataProcessor/DateTest.php: -------------------------------------------------------------------------------- 1 | filterFactory = $this->createMock(\Magento\Framework\Filter\FilterInputFactory::class); 54 | $this->dateFilter = $this->createMock(DateFilter::class); 55 | $this->filter = $this->createMock(\Magento\Framework\Filter\FilterInput::class); 56 | $this->date = new Date( 57 | ['field1', 'field2', 'field3'], 58 | $this->filterFactory, 59 | $this->dateFilter 60 | ); 61 | } 62 | 63 | /** 64 | * @covers \Umc\Crud\Ui\SaveDataProcessor\Date 65 | */ 66 | public function testModifyData() 67 | { 68 | $data = [ 69 | 'field1' => '2020-01-01', 70 | 'field2' => '2021-01-01', 71 | ]; 72 | $expectedFactoryData = [ 73 | 'filterRules' => [ 74 | 'field1' => $this->dateFilter, 75 | 'field2' => $this->dateFilter 76 | ], 77 | 'validatorRules' => [], 78 | 'data' => $data 79 | ]; 80 | $this->filterFactory->expects($this->once())->method('create') 81 | ->with($expectedFactoryData) 82 | ->willReturn($this->filter); 83 | $this->filter->expects($this->once())->method('getUnescaped')->willReturn(['result']); 84 | $this->assertEquals(['result'], $this->date->modifyData($data)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Test/Unit/Ui/SaveDataProcessor/DynamicRowsTest.php: -------------------------------------------------------------------------------- 1 | serializer = $this->createMock(Json::class); 41 | } 42 | 43 | /** 44 | * @covers \Umc\Crud\Ui\SaveDataProcessor\DynamicRows::modifyData 45 | * @covers \Umc\Crud\Ui\SaveDataProcessor\DynamicRows::__construct 46 | */ 47 | public function testModifyData() 48 | { 49 | $dynamicRows = new DynamicRows( 50 | $this->serializer, 51 | ['field1', 'field2', 'field3'], 52 | false 53 | ); 54 | $data = [ 55 | 'field1' => [1, 2, 3], 56 | 'field2' => 'string', 57 | 'field4' => ['not_processed'] 58 | ]; 59 | $this->serializer->expects($this->once())->method('serialize')->willReturnCallback( 60 | function (array $item) { 61 | $item['serialized'] = 1; 62 | return $item; 63 | } 64 | ); 65 | $expected = [ 66 | 'field1' => [ 67 | 0 => 1, 68 | 1 => 2, 69 | 2 => 3, 70 | 'serialized' => 1 71 | ], 72 | 'field2' => 'string', 73 | 'field4' => ['not_processed'] 74 | ]; 75 | $this->assertEquals($expected, $dynamicRows->modifyData($data)); 76 | } 77 | 78 | /** 79 | * @covers \Umc\Crud\Ui\SaveDataProcessor\DynamicRows::modifyData 80 | * @covers \Umc\Crud\Ui\SaveDataProcessor\DynamicRows::__construct 81 | */ 82 | public function testModifyDataStrictMode() 83 | { 84 | $dynamicRows = new DynamicRows( 85 | $this->serializer, 86 | ['field1', 'field2', 'field3'], 87 | true 88 | ); 89 | $data = [ 90 | 'field1' => [1, 2, 3], 91 | 'field2' => 'string', 92 | 'field4' => ['not_processed'] 93 | ]; 94 | $this->serializer->expects($this->exactly(2))->method('serialize')->willReturnCallback( 95 | function (array $item) { 96 | $item['serialized'] = 1; 97 | return $item; 98 | } 99 | ); 100 | $expected = [ 101 | 'field1' => [ 102 | 0 => 1, 103 | 1 => 2, 104 | 2 => 3, 105 | 'serialized' => 1 106 | ], 107 | 'field2' => 'string', 108 | 'field3' => ['serialized' => 1], 109 | 'field4' => ['not_processed'] 110 | ]; 111 | $this->assertEquals($expected, $dynamicRows->modifyData($data)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Test/Unit/Ui/SaveDataProcessor/MultiselectTest.php: -------------------------------------------------------------------------------- 1 | [1, 2, 3], 37 | 'field2' => '4,5,6', 38 | 'dummy' => 'dummy' 39 | ]; 40 | $expected = [ 41 | 'field1' => '1,2,3', 42 | 'field2' => '4,5,6', 43 | 'dummy' => 'dummy' 44 | ]; 45 | $this->assertEquals($expected, $modifier->modifyData($data)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Test/Unit/Ui/SaveDataProcessor/NullProcessorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($data, (new NullProcessor())->modifyData($data)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Test/Unit/ViewModel/Formatter/DateTest.php: -------------------------------------------------------------------------------- 1 | localeDate = $this->createMock(TimezoneInterface::class); 46 | $this->date = new Date($this->localeDate); 47 | } 48 | 49 | /** 50 | * @covers \Umc\Crud\ViewModel\Formatter\Date::formatHtml 51 | * @covers \Umc\Crud\ViewModel\Formatter\Date::__construct 52 | */ 53 | public function testFormatHtml() 54 | { 55 | $this->localeDate->expects($this->once())->method('formatDateTime') 56 | ->with(new \DateTime('1984-04-04'), \IntlDateFormatter::LONG, \IntlDateFormatter::NONE, null, null) 57 | ->willReturn('formatted'); 58 | $this->assertEquals('formatted', $this->date->formatHtml('1984-04-04')); 59 | } 60 | 61 | /** 62 | * @covers \Umc\Crud\ViewModel\Formatter\Date::formatHtml 63 | * @covers \Umc\Crud\ViewModel\Formatter\Date::__construct 64 | */ 65 | public function testFormatHtmlWithParams() 66 | { 67 | $this->localeDate->expects($this->once())->method('formatDateTime') 68 | ->with(new \DateTime('1984-04-04'), \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT, null, null) 69 | ->willReturn('formatted'); 70 | $this->assertEquals( 71 | 'formatted', 72 | $this->date->formatHtml( 73 | '1984-04-04', 74 | [ 75 | 'format' => \IntlDateFormatter::SHORT, 76 | 'show_time' => true 77 | ] 78 | ) 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Test/Unit/ViewModel/Formatter/FileTest.php: -------------------------------------------------------------------------------- 1 | fileInfoFactory = $this->createMock(FileInfoFactory::class); 66 | $this->filesystem = $this->createMock(Filesystem::class); 67 | $this->storeManager = $this->createMock(StoreManagerInterface::class); 68 | $this->fileInfo = $this->createMock(FileInfo::class); 69 | $this->store = $this->createMock(Store::class); 70 | $this->file = new File( 71 | $this->fileInfoFactory, 72 | $this->filesystem, 73 | $this->storeManager 74 | ); 75 | } 76 | 77 | /** 78 | * @covers \Umc\Crud\ViewModel\Formatter\File::formatHtml 79 | * @covers \Umc\Crud\ViewModel\Formatter\File::getFileInfo 80 | * @covers \Umc\Crud\ViewModel\Formatter\File::__construct 81 | */ 82 | public function testFormatHtmlWithWrongPath() 83 | { 84 | $this->fileInfoFactory->expects($this->once())->method('create')->willReturn($this->fileInfo); 85 | $this->fileInfo->method('getFilePath')->willReturn(''); 86 | $this->storeManager->expects($this->never())->method('getStore'); 87 | $this->assertEquals('', $this->file->formatHtml('value')); 88 | } 89 | 90 | /** 91 | * @covers \Umc\Crud\ViewModel\Formatter\File::formatHtml 92 | * @covers \Umc\Crud\ViewModel\Formatter\File::getFileInfo 93 | * @covers \Umc\Crud\ViewModel\Formatter\File::__construct 94 | */ 95 | public function testFormatHtml() 96 | { 97 | $this->fileInfoFactory->expects($this->once())->method('create')->willReturn($this->fileInfo); 98 | $this->fileInfo->method('getFilePath')->willReturn('/path'); 99 | $this->storeManager->expects($this->once())->method('getStore')->willReturn($this->store); 100 | $this->store->method('getBaseUrl')->willReturn('base/'); 101 | $this->assertEquals('base/path', $this->file->formatHtml('value')); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Test/Unit/ViewModel/Formatter/TextTest.php: -------------------------------------------------------------------------------- 1 | escaper = $this->createMock(Escaper::class); 46 | $this->text = new Text($this->escaper); 47 | } 48 | 49 | /** 50 | * @covers \Umc\Crud\ViewModel\Formatter\Text::formatHtml 51 | * @covers \Umc\Crud\ViewModel\Formatter\Text::__construct 52 | */ 53 | public function testFormatHtml() 54 | { 55 | $this->escaper->expects($this->once())->method('escapeHtml')->willReturn('escaped'); 56 | $this->assertEquals('escaped', $this->text->formatHtml('value')); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Test/Unit/ViewModel/Formatter/WysiwygTest.php: -------------------------------------------------------------------------------- 1 | filter = $this->createMock(\Laminas\Filter\FilterInterface::class); 45 | $this->wysiwyg = new Wysiwyg($this->filter); 46 | } 47 | 48 | /** 49 | * @covers \Umc\Crud\ViewModel\Formatter\Wysiwyg::formatHtml 50 | * @covers \Umc\Crud\ViewModel\Formatter\Wysiwyg::__construct 51 | */ 52 | public function testFormatHtml() 53 | { 54 | $this->filter->expects($this->once())->method('filter')->willReturn('filtered'); 55 | $this->assertEquals('filtered', $this->wysiwyg->formatHtml('value')); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Test/Unit/ViewModel/FormatterTest.php: -------------------------------------------------------------------------------- 1 | escaper = $this->createMock(Escaper::class); 42 | } 43 | 44 | /** 45 | * @covers \Umc\Crud\ViewModel\Formatter::formatHtml 46 | * @covers \Umc\Crud\ViewModel\Formatter::getFormatter 47 | * @covers \Umc\Crud\ViewModel\Formatter::__construct 48 | */ 49 | public function testFormatHtml() 50 | { 51 | $formatter1 = $this->getFormatterMock(); 52 | $formatter2 = $this->getFormatterMock(); 53 | $formatter = new Formatter( 54 | [ 55 | 'type1' => $formatter1, 56 | 'type2' => $formatter2, 57 | ], 58 | $this->escaper 59 | ); 60 | $formatter1->expects($this->once())->method('formatHtml')->willReturn('formatted'); 61 | $this->assertEquals('formatted', $formatter->formatHtml('value', ['type' => 'type1'])); 62 | } 63 | 64 | /** 65 | * @covers \Umc\Crud\ViewModel\Formatter::formatHtml 66 | * @covers \Umc\Crud\ViewModel\Formatter::getFormatter 67 | * @covers \Umc\Crud\ViewModel\Formatter::__construct 68 | */ 69 | public function testFormatHtmlNoArgument() 70 | { 71 | $formatter1 = $this->getFormatterMock(); 72 | $formatter2 = $this->getFormatterMock(); 73 | $formatter = new Formatter( 74 | [ 75 | 'type1' => $formatter1, 76 | 'type2' => $formatter2, 77 | ], 78 | $this->escaper 79 | ); 80 | $formatter1->expects($this->never())->method('formatHtml'); 81 | $this->escaper->expects($this->once())->method('escapeHtml')->willReturn('formatted'); 82 | $this->assertEquals('formatted', $formatter->formatHtml('value', [])); 83 | } 84 | 85 | /** 86 | * @covers \Umc\Crud\ViewModel\Formatter::formatHtml 87 | * @covers \Umc\Crud\ViewModel\Formatter::getFormatter 88 | * @covers \Umc\Crud\ViewModel\Formatter::__construct 89 | */ 90 | public function testFormatHtmlNoTypeConfigured() 91 | { 92 | $formatter1 = $this->getFormatterMock(); 93 | $formatter2 = $this->getFormatterMock(); 94 | $formatter = new Formatter( 95 | [ 96 | 'type1' => $formatter1, 97 | 'type2' => $formatter2, 98 | ], 99 | $this->escaper 100 | ); 101 | $this->expectException(\InvalidArgumentException::class); 102 | $formatter->formatHtml('value', ['type' => 'type3']); 103 | } 104 | 105 | /** 106 | * @covers \Umc\Crud\ViewModel\Formatter::__construct 107 | */ 108 | public function testConstruct() 109 | { 110 | $this->expectException(\InvalidArgumentException::class); 111 | new Formatter( 112 | [ 113 | 'type1' => $this->getFormatterMock(), 114 | 'type2' => 'string', 115 | ], 116 | $this->escaper 117 | ); 118 | } 119 | 120 | /** 121 | * @return Formatter'FormatterInterface | MockObject 122 | * @throws \ReflectionException 123 | */ 124 | private function getFormatterMock() 125 | { 126 | $formatter = $this->createMock(Formatter\FormatterInterface::class); 127 | return $formatter; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Test/Unit/ViewModel/HeartbeatTest.php: -------------------------------------------------------------------------------- 1 | url = $this->createMock(UrlInterface::class); 46 | $this->heartbeat = new Heartbeat( 47 | $this->url 48 | ); 49 | } 50 | 51 | /** 52 | * @covers \Umc\Crud\ViewModel\Heartbeat 53 | */ 54 | public function testGetUrl() 55 | { 56 | $this->url->expects($this->once())->method('getUrl')->with('crud/heartbeat/index', null)->willReturn('url'); 57 | $this->assertEquals('url', $this->heartbeat->getUrl()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Ui/CollectionProviderInterface.php: -------------------------------------------------------------------------------- 1 | urlBuilder = $urlBuilder; 59 | $this->uiConfig = $uiConfig; 60 | parent::__construct($context, $uiComponentFactory, $components, $data); 61 | } 62 | 63 | /** 64 | * Prepare Data Source 65 | * 66 | * @param array $dataSource 67 | * @return array 68 | */ 69 | public function prepareDataSource(array $dataSource) 70 | { 71 | $param = $this->uiConfig->getRequestParamName(); 72 | $nameAttribute = $this->uiConfig->getNameAttribute(); 73 | foreach ($dataSource['data']['items'] as & $item) { 74 | $params = [$param => $item[$param] ?? null]; 75 | $item[$this->getData('name')] = [ 76 | 'edit' => [ 77 | 'href' => $this->urlBuilder->getUrl($this->uiConfig->getEditUrlPath(), $params), 78 | 'label' => __('Edit')->render() 79 | ], 80 | 'delete' => [ 81 | 'href' => $this->urlBuilder->getUrl($this->uiConfig->getDeleteUrlPath(), $params), 82 | 'label' => __('Delete'), 83 | 'confirm' => [ 84 | 'title' => __('Delete %1', $item[$nameAttribute] ?? '')->render(), 85 | 'message' => $this->uiConfig->getDeleteMessage() 86 | ], 87 | 'post' => true 88 | ] 89 | ]; 90 | } 91 | return $dataSource; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Ui/Component/Listing/Image.php: -------------------------------------------------------------------------------- 1 | storeManager = $storeManager; 63 | $this->uiConfig = $uiConfig; 64 | $this->fileInfo = $fileInfo; 65 | parent::__construct($context, $uiComponentFactory, $components, $data); 66 | } 67 | 68 | /** 69 | * @param array $dataSource 70 | * @return array 71 | * @throws \Magento\Framework\Exception\NoSuchEntityException 72 | */ 73 | public function prepareDataSource(array $dataSource) 74 | { 75 | if (isset($dataSource['data']['items'])) { 76 | $fieldName = $this->getData('name'); 77 | foreach ($dataSource['data']['items'] as & $item) { 78 | $url = $this->getUrl($item[$fieldName] ?? ''); 79 | $item[$fieldName . '_src'] = $url; 80 | $item[$fieldName . '_alt'] = $this->getAlt($item) ?: ''; 81 | $item[$fieldName . '_orig_src'] = $url; 82 | $item[$fieldName . '_link'] = $this->getEditUrl($item); 83 | } 84 | } 85 | return $dataSource; 86 | } 87 | 88 | /** 89 | * @param $value 90 | * @return string 91 | * @throws \Magento\Framework\Exception\FileSystemException 92 | * @throws \Magento\Framework\Exception\NoSuchEntityException 93 | */ 94 | private function getUrl($value) 95 | { 96 | if (!$value) { 97 | return ''; 98 | } 99 | if ($this->fileInfo->isBeginsWithMediaDirectoryPath($value)) { 100 | return $value; 101 | } 102 | $store = $this->storeManager->getStore(); 103 | $mediaBaseUrl = $store->getBaseUrl( 104 | \Magento\Framework\UrlInterface::URL_TYPE_MEDIA 105 | ); 106 | return $mediaBaseUrl . ltrim($this->fileInfo->getBaseFilePath(), '/') . '/' . ltrim($value, '/'); 107 | } 108 | 109 | /** 110 | * @param $item 111 | * @return string 112 | */ 113 | private function getEditUrl($item) 114 | { 115 | $base = $this->uiConfig->getEditUrlPath(); 116 | $idParam = $this->uiConfig->getRequestParamName(); 117 | $params = [$idParam => $item[$idParam] ?? null]; 118 | return $this->context->getUrl($base, $params); 119 | } 120 | 121 | /** 122 | * @param array $row 123 | * 124 | * @return null|string 125 | */ 126 | private function getAlt($row) 127 | { 128 | $altField = $this->uiConfig->getNameAttribute(); 129 | return $altField && isset($row[$altField]) ? $row[$altField] : null; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Ui/EntityUiManagerInterface.php: -------------------------------------------------------------------------------- 1 | modifiers = $modifiers; 48 | } 49 | 50 | /** 51 | * @param AbstractModel $model 52 | * @param array $data 53 | * @return array 54 | */ 55 | public function modifyData(AbstractModel $model, array $data): array 56 | { 57 | foreach ($this->modifiers as $modifier) { 58 | $data = $modifier->modifyData($model, $data); 59 | } 60 | return $data; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Ui/Form/DataModifier/DynamicRows.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 47 | $this->fields = $fields; 48 | } 49 | 50 | /** 51 | * @param AbstractModel $model 52 | * @param array $data 53 | * @return array 54 | */ 55 | public function modifyData(AbstractModel $model, array $data): array 56 | { 57 | foreach ($this->fields as $field) { 58 | if (array_key_exists($field, $data) && !is_array($data[$field])) { 59 | try { 60 | $data[$field] = $this->serializer->unserialize($data[$field]); 61 | } catch (\Exception $e) { 62 | $data[$field] = []; 63 | } 64 | } 65 | } 66 | return $data; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Ui/Form/DataModifier/Multiselect.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 41 | } 42 | 43 | /** 44 | * @param AbstractModel $model 45 | * @param array $data 46 | * @return array 47 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 48 | */ 49 | public function modifyData(AbstractModel $model, array $data): array 50 | { 51 | foreach ($this->fields as $field) { 52 | if (!array_key_exists($field, $data)) { 53 | continue; 54 | } 55 | if ($data[$field] === null) { 56 | $data[$field] = []; 57 | } 58 | if (is_string($data[$field])) { 59 | $data[$field] = explode(',', $data[$field]); 60 | } 61 | } 62 | return $data; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Ui/Form/DataModifier/NullModifier.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 70 | $this->uploader = $uploader; 71 | $this->logger = $logger; 72 | $this->fileInfo = $fileInfo; 73 | $this->storeManager = $storeManager; 74 | } 75 | 76 | /** 77 | * @param AbstractModel $model 78 | * @param array $data 79 | * @return array 80 | * @throws \Magento\Framework\Exception\FileSystemException 81 | * @throws \Magento\Framework\Exception\LocalizedException 82 | */ 83 | public function modifyData(AbstractModel $model, array $data): array 84 | { 85 | foreach ($this->fields as $field) { 86 | $value = $data[$field] ?? ''; 87 | if ($value) { 88 | if ($this->fileInfo->isExist($value)) { 89 | $stat = $this->fileInfo->getStat($value); 90 | $mime = $this->fileInfo->getMimeType($value); 91 | $beginsWithMediaDirectory = $this->fileInfo->isBeginsWithMediaDirectoryPath($value); 92 | $url = ($beginsWithMediaDirectory) ? $value : $this->getUrl($value); 93 | $data[$field] = [ 94 | 0 => [ 95 | 'name' => $value, 96 | 'url' => $url, 97 | 'size' => isset($stat) ? $stat['size'] : 0, 98 | 'type' => $mime 99 | ] 100 | ]; 101 | } 102 | } 103 | } 104 | return $data; 105 | } 106 | 107 | /** 108 | * @param $file 109 | * @return bool|string 110 | * @throws \Magento\Framework\Exception\LocalizedException 111 | * @throws \Magento\Framework\Exception\NoSuchEntityException 112 | */ 113 | public function getUrl($file) 114 | { 115 | if (is_string($file)) { 116 | $store = $this->storeManager->getStore(); 117 | $mediaBaseUrl = $store->getBaseUrl( 118 | \Magento\Framework\UrlInterface::URL_TYPE_MEDIA 119 | ); 120 | return $mediaBaseUrl . ltrim($this->fileInfo->getBaseFilePath(), '/') . '/' . ltrim($file, '/'); 121 | } else { 122 | throw new \Magento\Framework\Exception\LocalizedException( 123 | __('Something went wrong while getting the file url.') 124 | ); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Ui/Form/DataModifierInterface.php: -------------------------------------------------------------------------------- 1 | dataPersistor = $dataPersistor; 71 | $this->uiConfig = $uiConfig; 72 | $this->dataModifier = $dataModifier; 73 | parent::__construct($name, $uiConfig->getRequestParamName(), $uiConfig->getRequestParamName(), $meta, $data); 74 | $this->collection = $collectionProvider->getCollection(); 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | public function getData() 81 | { 82 | if (isset($this->loadedData)) { 83 | return $this->loadedData; 84 | } 85 | /** @var AbstractModel $entity */ 86 | foreach ($this->collection as $entity) { 87 | $this->loadedData[$entity->getId()] = $this->dataModifier->modifyData($entity, $entity->getData()); 88 | } 89 | $persistorKey = $this->uiConfig->getPersistoryKey(); 90 | $data = $this->dataPersistor->get($persistorKey); 91 | if (!empty($data)) { 92 | $entity = $this->collection->getNewEmptyItem(); 93 | $entity->setData($data); 94 | $this->loadedData[$entity->getId()] = $entity->getData(); 95 | $this->dataPersistor->clear($persistorKey); 96 | } 97 | return $this->loadedData; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Ui/SaveDataProcessor/CompositeProcessor.php: -------------------------------------------------------------------------------- 1 | modifiers = $modifiers; 47 | } 48 | 49 | public function modifyData(array $data): array 50 | { 51 | foreach ($this->modifiers as $modifier) { 52 | $data = $modifier->modifyData($data); 53 | } 54 | return $data; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Ui/SaveDataProcessor/Date.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 54 | $this->filterFactory = $filterFactory; 55 | $this->dateFilter = $dateFilter; 56 | } 57 | 58 | /** 59 | * @param array $data 60 | * @return array 61 | */ 62 | public function modifyData(array $data): array 63 | { 64 | $filterRules = []; 65 | foreach ($this->fields as $dateField) { 66 | if (!array_key_exists($dateField, $data)) { 67 | continue; 68 | } 69 | if (!empty($data[$dateField])) { 70 | $filterRules[$dateField] = $this->dateFilter; 71 | } 72 | } 73 | /** @var \Magento\Framework\Filter\FilterInput $filter */ 74 | $filter = $this->filterFactory->create([ 75 | 'filterRules' => $filterRules, 76 | 'validatorRules' => [], 77 | 'data' => $data 78 | ]); 79 | return $filter->getUnescaped(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Ui/SaveDataProcessor/DynamicRows.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 51 | $this->fields = $fields; 52 | $this->strict = $strict; 53 | } 54 | 55 | /** 56 | * @param array $data 57 | * @return array 58 | */ 59 | public function modifyData(array $data): array 60 | { 61 | foreach ($this->fields as $field) { 62 | if (!array_key_exists($field, $data) && $this->strict) { 63 | $data[$field] = []; 64 | } 65 | if (array_key_exists($field, $data) && is_array($data[$field])) { 66 | $data[$field] = $this->serializer->serialize($data[$field]); 67 | } 68 | } 69 | return $data; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Ui/SaveDataProcessor/Multiselect.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 40 | } 41 | 42 | /** 43 | * @param array $data 44 | * @return array 45 | */ 46 | public function modifyData(array $data): array 47 | { 48 | foreach ($this->fields as $field) { 49 | if (!array_key_exists($field, $data)) { 50 | continue; 51 | } 52 | $value = $data[$field] ?? []; 53 | if (is_array($value)) { 54 | $data[$field] = implode(',', $value); 55 | } 56 | } 57 | return $data; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Ui/SaveDataProcessor/NullProcessor.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 76 | $this->uploader = $uploader; 77 | $this->fileInfo = $fileInfo; 78 | $this->filesystem = $filesystem; 79 | $this->logger = $logger; 80 | $this->strict = $strict; 81 | } 82 | 83 | /** 84 | * @param $value 85 | * @return bool 86 | */ 87 | private function isTmpFileAvailable($value) 88 | { 89 | return is_array($value) && isset($value[0]['tmp_name']); 90 | } 91 | 92 | /** 93 | * @param $value 94 | * @return string 95 | */ 96 | private function getUploadedImageName($value) 97 | { 98 | return (is_array($value) && isset($value[0]['file'])) ? $value[0]['file'] : ''; 99 | } 100 | 101 | /** 102 | * @param array $data 103 | * @return array 104 | */ 105 | public function modifyData(array $data): array 106 | { 107 | foreach ($this->fields as $field) { 108 | if (!array_key_exists($field, $data)) { 109 | if ($this->strict) { 110 | $data[$field] = ''; 111 | } 112 | continue; 113 | } 114 | $value = $data[$field] ?? ''; 115 | if ($this->isTmpFileAvailable($value) && $imageName = $this->getUploadedImageName($value)) { 116 | try { 117 | $data[$field] = $this->uploader->moveFileFromTmp($imageName); 118 | } catch (\Exception $e) { 119 | $this->logger->critical($e); 120 | } 121 | } else { 122 | if ($this->fileResidesOutsideUploadDir($value)) { 123 | // phpcs:ignore Magento2.Functions.DiscouragedFunction 124 | $value[0]['name'] = parse_url($value[0]['url'], PHP_URL_PATH); 125 | } 126 | $data[$field] = $value[0]['name'] ?? ''; 127 | } 128 | } 129 | return $data; 130 | } 131 | 132 | /** 133 | * @param $value 134 | * @return bool 135 | */ 136 | private function fileResidesOutsideUploadDir($value) 137 | { 138 | if (!is_array($value) || !isset($value[0]['url'])) { 139 | return false; 140 | } 141 | $fileUrl = ltrim($value[0]['url'], '/'); 142 | $filePath = $this->fileInfo->getFilePath($fileUrl); 143 | $baseMediaDir = $this->filesystem->getUri(DirectoryList::MEDIA); 144 | return $baseMediaDir && strpos($filePath, $baseMediaDir) !== false; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Ui/SaveDataProcessorInterface.php: -------------------------------------------------------------------------------- 1 | formatterMap = $formatterMap; 51 | $this->escaper = $escaper; 52 | } 53 | 54 | /** 55 | * @param $value 56 | * @param array $arguments 57 | * @return string 58 | */ 59 | public function formatHtml($value, $arguments = []): string 60 | { 61 | $type = $arguments['type'] ?? null; 62 | return $type === null 63 | ? $this->escaper->escapeHtml($value) 64 | : $this->getFormatter($type)->formatHtml($value, $arguments); 65 | } 66 | 67 | /** 68 | * @param $type 69 | * @return FormatterInterface|null 70 | */ 71 | private function getFormatter($type) 72 | { 73 | $formatter = $this->formatterMap[$type] ?? null; 74 | if ($formatter === null) { 75 | throw new \InvalidArgumentException("Missing formatter for type {$type}"); 76 | } 77 | return $formatter; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ViewModel/Formatter/Date.php: -------------------------------------------------------------------------------- 1 | localeDate = $localeDate; 52 | } 53 | 54 | /** 55 | * @param $value 56 | * @param array $arguments 57 | * @return string 58 | */ 59 | public function formatHtml($value, $arguments = []): string 60 | { 61 | $format = $arguments[self::FORMAT] ?? self::DEFAULT_FORMAT; 62 | $showTime = $arguments[self::SHOW_TIME] ?? false; 63 | $timezone = $arguments[self::TIMEZONE] ?? null; 64 | $value = $value instanceof \DateTimeInterface ? $value : new \DateTime($value); 65 | return $this->localeDate->formatDateTime( 66 | $value, 67 | $format, 68 | $showTime ? $format : \IntlDateFormatter::NONE, 69 | null, 70 | $timezone 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ViewModel/Formatter/File.php: -------------------------------------------------------------------------------- 1 | fileInfoFactory = $fileInfoFactory; 60 | $this->filesystem = $filesystem; 61 | $this->storeManager = $storeManager; 62 | } 63 | 64 | /** 65 | * @param $path 66 | * @return FileInfo 67 | */ 68 | private function getFileInfo($path) 69 | { 70 | if (!array_key_exists($path, $this->fileInfoCache)) { 71 | $this->fileInfoCache[$path] = $this->fileInfoFactory->create(['filePath' => $path]); 72 | } 73 | return $this->fileInfoCache[$path]; 74 | } 75 | 76 | /** 77 | * @param $value 78 | * @param array $arguments 79 | * @return string 80 | * @throws \Exception 81 | */ 82 | public function formatHtml($value, $arguments = []): string 83 | { 84 | $path = $arguments['path'] ?? ''; 85 | $fileInfo = $this->getFileInfo($path); 86 | $filePath = $fileInfo->getFilePath((string)$value); 87 | if (!$filePath) { 88 | return ''; 89 | } 90 | $store = $this->storeManager->getStore(); 91 | $mediaBaseUrl = $store->getBaseUrl( 92 | \Magento\Framework\UrlInterface::URL_TYPE_MEDIA 93 | ); 94 | return $mediaBaseUrl . trim($filePath, '/'); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ViewModel/Formatter/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | objectManager = $objectManager; 51 | $this->escaper = $escaper; 52 | } 53 | 54 | /** 55 | * @param $value 56 | * @param array $arguments 57 | * @return string 58 | */ 59 | public function formatHtml($value, $arguments = []): string 60 | { 61 | $source = $this->getSource($arguments); 62 | $options = $source ? $source->toOptionArray() : $this->getOptions($arguments); 63 | $value = is_array($value) ? $value : [$value]; 64 | $texts = array_map( 65 | function ($item) { 66 | return $this->escaper->escapeHtml($item['label'] ?? ''); 67 | }, 68 | array_filter( 69 | $options, 70 | function ($item) use ($value) { 71 | return isset($item['value']) && in_array($item['value'], $value); 72 | } 73 | ) 74 | ); 75 | return count($texts) > 0 76 | ? implode(', ', $texts) 77 | : (isset($arguments['default']) ? (string)$arguments['default'] : ''); 78 | } 79 | 80 | /** 81 | * @param array $arguments 82 | * @return OptionSourceInterface|null 83 | */ 84 | private function getSource(array $arguments): ?OptionSourceInterface 85 | { 86 | $sourceClass = $arguments['source'] ?? null; 87 | if (!$sourceClass) { 88 | return null; 89 | } 90 | if (!array_key_exists($sourceClass, $this->sources)) { 91 | $instance = $this->objectManager->get($sourceClass); 92 | if (!($instance instanceof OptionSourceInterface)) { 93 | throw new \InvalidArgumentException( 94 | "Source model for options formatter should implement " . OptionSourceInterface::class 95 | ); 96 | } 97 | $this->sources[$sourceClass] = $instance; 98 | } 99 | return $this->sources[$sourceClass]; 100 | } 101 | 102 | /** 103 | * @param array $arguments 104 | * @return array|mixed 105 | */ 106 | private function getOptions(array $arguments): array 107 | { 108 | return $arguments['options'] ?? []; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ViewModel/Formatter/Text.php: -------------------------------------------------------------------------------- 1 | escaper = $escaper; 40 | } 41 | 42 | /** 43 | * @param $value 44 | * @param array $arguments 45 | * @return string 46 | */ 47 | public function formatHtml($value, $arguments = []): string 48 | { 49 | return $this->escaper->escapeHtml($value); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ViewModel/Formatter/Wysiwyg.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 40 | } 41 | 42 | /** 43 | * @param $value 44 | * @param array $arguments 45 | * @return string 46 | * @throws \Laminas\Filter\Exception\ExceptionInterface 47 | */ 48 | public function formatHtml($value, $arguments = []): string 49 | { 50 | return $this->filter->filter($value); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ViewModel/Heartbeat.php: -------------------------------------------------------------------------------- 1 | url = $url; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getUrl() 47 | { 48 | return $this->url->getUrl('crud/heartbeat/index'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "umc/module-crud", 3 | "description": "Magento 2 module for reducing the CRUD boilerplate", 4 | "require": { 5 | "php": "~7.1.3||~7.2.0||~7.3.0||~7.4.0||~8.0.0||~8.1.0||~8.2.0||~8.3.0||~8.4.0", 6 | "magento/module-backend": "*", 7 | "magento/module-config": "*", 8 | "magento/module-ui": "*", 9 | "magento/framework": "*" 10 | }, 11 | "type": "magento2-module", 12 | "license": [ 13 | "MIT" 14 | ], 15 | "autoload": { 16 | "files": [ 17 | "registration.php" 18 | ], 19 | "psr-4": { 20 | "Umc\\Crud\\": "" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /etc/crud/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | \Umc\Crud\Generator\UiManager 25 | \Umc\Crud\Generator\ListRepo 26 | \Umc\Crud\Generator\Repo 27 | \Umc\Crud\Generator\UiCollectionProvider 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | Umc\Crud\Console\Command\Deploy 25 | 26 | 27 | 28 | 29 | 30 | Magento\Framework\Module\Dir\Reader\Proxy 31 | Magento\Framework\Filesystem\DirectoryList\Proxy 32 | Magento\Framework\Filesystem\Io\File\Proxy 33 | 34 | 35 | 36 | 37 | Magento\Widget\Model\Template\Filter 38 | 39 | 40 | 41 | 42 | 43 | Umc\Crud\ViewModel\Formatter\Date 44 | Umc\Crud\ViewModel\Formatter\Text 45 | Umc\Crud\ViewModel\Formatter\Wysiwyg 46 | Umc\Crud\ViewModel\Formatter\Options 47 | Umc\Crud\ViewModel\Formatter\Image 48 | Umc\Crud\ViewModel\Formatter\File 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /i18n/en_US.csv: -------------------------------------------------------------------------------- 1 | Back,Back 2 | Reset,Reset 3 | "Please correct the data sent.","Please correct the data sent." 4 | Error,Error 5 | "Something went wrong while saving the file(s).","Something went wrong while saving the file(s)." 6 | "File can not be saved to the destination folder.","File can not be saved to the destination folder." 7 | "All Store Views","All Store Views" 8 | Edit,Edit 9 | Delete,Delete 10 | "Delete %1","Delete %1" 11 | Save,Save 12 | "Save & Duplicate","Save & Duplicate" 13 | "Save & close","Save & close" 14 | "Are you sure you want to delete the item?","Are you sure you want to delete the item?" 15 | "Delete ""${ $.$data.%1 }""","Delete ""${ $.$data.%1 }""" 16 | "Item was deleted successfully","Item was deleted successfully" 17 | "Item for delete was not found","Item for delete was not found" 18 | "There was a problem deleting the item.","There was a problem deleting the item." 19 | "Item was saved successfully.","Item was saved successfully." 20 | "There was a problem saving the item.","There was a problem saving the item." 21 | "Item was duplicated successfully.","Item was duplicated successfully." 22 | "%1 items were successfully deleted","%1 items were successfully deleted" 23 | "There was a problem deleting the items","There was a problem deleting the items" 24 | "Add new item","Add new item" 25 | "Something went wrong while getting the file url.","Something went wrong while getting the file url." 26 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | getData('heartbeat'); ?> 22 | 23 | 32 | 19 |
20 |
21 | 24 | 31 | 32 | 33 |
34 | 42 |
43 |
44 | 45 |
46 | 47 |
48 | 49 | x, 50 | 51 | 52 |
53 |
54 | --------------------------------------------------------------------------------