-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactored Source For ImportExport module, related to #7469 #9480
Changes from all commits
d948d42
8b8bcae
8c86c17
0a898be
9b542f6
43df7ef
ff04479
2488330
2088f55
99d6a17
71c5288
2ff1d0f
e8d21fc
1de2b68
f654f06
b1826bc
e5c6327
6b93516
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -267,7 +267,7 @@ protected function _getSourceAdapter($sourceFile) | |
return \Magento\ImportExport\Model\Import\Adapter::findAdapterFor( | ||
$sourceFile, | ||
$this->_filesystem->getDirectoryWrite(DirectoryList::ROOT), | ||
$this->getData(self::FIELD_FIELD_SEPARATOR) | ||
$this->getSourceAdapterOptions() | ||
); | ||
} | ||
|
||
|
@@ -785,4 +785,16 @@ public function getDeletedItemsCount() | |
{ | ||
return $this->_getEntityAdapter()->getDeletedItemsCount(); | ||
} | ||
|
||
/** | ||
* Returns options for source adapter | ||
* | ||
* @return array | ||
*/ | ||
protected function getSourceAdapterOptions() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't recommend to use Inheritance Based API, so all the methods should be whether public or private |
||
{ | ||
return [ | ||
'delimiter' => $this->getData(self::FIELD_FIELD_SEPARATOR) ?: ',' | ||
]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,11 @@ | |
*/ | ||
namespace Magento\ImportExport\Model\Import; | ||
|
||
use Magento\Framework\App\ObjectManager; | ||
use Magento\Framework\Filesystem\Directory\Write; | ||
use Magento\ImportExport\Model\Import\Source\FileFactory; | ||
use Magento\ImportExport\Model\Import\Source\FileParser\CorruptedFileException; | ||
use Magento\ImportExport\Model\Import\Source\FileParser\UnsupportedPathException; | ||
|
||
/** | ||
* Import adapter model | ||
|
@@ -14,6 +18,15 @@ | |
*/ | ||
class Adapter | ||
{ | ||
/** @var FileFactory */ | ||
private $fileSourceFactory; | ||
|
||
public function __construct(FileFactory $fileSourceFactory) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc block absent for this constructor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you see as a comment for a constructor? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DocBlock is not just comment, but specification of all input types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Violation of BC policies, all the parameters should be optional (nullable) |
||
{ | ||
$this->fileSourceFactory = $fileSourceFactory; | ||
} | ||
|
||
|
||
/** | ||
* Adapter factory. Checks for availability, loads and create instance of import adapter object. | ||
* | ||
|
@@ -25,29 +38,12 @@ class Adapter | |
* @return AbstractSource | ||
* | ||
* @throws \Magento\Framework\Exception\LocalizedException | ||
* @deprecated | ||
* @see Adapter::createSourceByPath() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better to avoid mixing up Deprecated logic and Newly created one in one class. Inject newly created factory as DI injection to Adapter class. |
||
*/ | ||
public static function factory($type, $directory, $source, $options = null) | ||
{ | ||
if (!is_string($type) || !$type) { | ||
throw new \Magento\Framework\Exception\LocalizedException( | ||
__('The adapter type must be a non-empty string.') | ||
); | ||
} | ||
$adapterClass = 'Magento\ImportExport\Model\Import\Source\\' . ucfirst(strtolower($type)); | ||
|
||
if (!class_exists($adapterClass)) { | ||
throw new \Magento\Framework\Exception\LocalizedException( | ||
__('\'%1\' file extension is not supported', $type) | ||
); | ||
} | ||
$adapter = new $adapterClass($source, $directory, $options); | ||
|
||
if (!$adapter instanceof AbstractSource) { | ||
throw new \Magento\Framework\Exception\LocalizedException( | ||
__('Adapter must be an instance of \Magento\ImportExport\Model\Import\AbstractSource') | ||
); | ||
} | ||
return $adapter; | ||
return self::createBackwardCompatibleInstance()->createSourceByPath($source, $options); | ||
} | ||
|
||
/** | ||
|
@@ -58,9 +54,72 @@ public static function factory($type, $directory, $source, $options = null) | |
* @param mixed $options OPTIONAL Adapter constructor options | ||
* | ||
* @return AbstractSource | ||
* @deprecated | ||
* @see Adapter::createSourceByPath() | ||
*/ | ||
public static function findAdapterFor($source, $directory, $options = null) | ||
{ | ||
return self::factory(pathinfo($source, PATHINFO_EXTENSION), $directory, $source, $options); | ||
return self::createBackwardCompatibleInstance()->createSourceByPath($source, $options); | ||
} | ||
|
||
|
||
|
||
/** | ||
* Finds source for import by file path | ||
* | ||
* @param $path | ||
* @param $options | ||
* | ||
* @return AbstractSource | ||
*/ | ||
public function createSourceByPath($path, $options = []) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be a responsibility of new API entity |
||
{ | ||
try { | ||
$options = $this->mapBackwardCompatibleFileParserOption($options); | ||
$parser = $this->fileSourceFactory->createFromFilePath($path, $options); | ||
} catch (UnsupportedPathException $e) { | ||
$this->throwUnsupportedFileException($path); | ||
} catch (CorruptedFileException $e) { | ||
$this->throwUnsupportedFileException($path); | ||
} | ||
|
||
return $parser; | ||
} | ||
|
||
|
||
private function extractFileExtension($path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better to use PHP core function for these purposes than to implement one by yourself $path_parts = pathinfo('/www/htdocs/inc/lib.inc.php');
echo $path_parts['extension']; |
||
{ | ||
$fileName = basename($path); | ||
|
||
if (strpos($path, '.') === false) { | ||
return $fileName; | ||
} | ||
|
||
$fileExtension = substr($fileName, strrpos($fileName, '.') + 1); | ||
return $fileExtension; | ||
} | ||
|
||
private function throwUnsupportedFileException($path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is no PHP Doc Block There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it required to have DocBlock on private methods that describe its purpose in the name? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's required for all methods and properties, no matter whether it private or public one |
||
{ | ||
$fileExtension = $this->extractFileExtension($path); | ||
throw new \Magento\Framework\Exception\LocalizedException( | ||
__('\'%1\' file extension is not supported', $fileExtension) | ||
); | ||
} | ||
|
||
private function mapBackwardCompatibleFileParserOption($options) | ||
{ | ||
if (!is_array($options)) { | ||
$options = ['delimiter' => $options ?? ',']; | ||
} | ||
return $options; | ||
} | ||
|
||
/** | ||
* @return self | ||
*/ | ||
private static function createBackwardCompatibleInstance() | ||
{ | ||
return ObjectManager::getInstance()->create(self::class); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,8 @@ | |
|
||
/** | ||
* CSV import adapter | ||
* | ||
* @deprecated | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reason of depreciation should be provided and @ see tag specifying the new, correct version of current API |
||
*/ | ||
class Csv extends \Magento\ImportExport\Model\Import\AbstractSource | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento, Inc. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
|
||
namespace Magento\ImportExport\Model\Import\Source; | ||
|
||
use Magento\ImportExport\Model\Import\AbstractSource; | ||
|
||
class File extends AbstractSource | ||
{ | ||
private $parser; | ||
|
||
public function __construct(FileParser\ParserInterface $parser) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PHP Doc Block absent |
||
{ | ||
$this->parser = $parser; | ||
parent::__construct($this->parser->getColumnNames()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's not correct to make computation in constructors, Magento encourages developers to use constructors just for assignments |
||
} | ||
|
||
protected function _getNextRow() | ||
{ | ||
return $this->parser->fetchRow(); | ||
} | ||
|
||
public function rewind() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doc Blocks are absent |
||
{ | ||
$this->parser->reset(); | ||
parent::rewind(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento, Inc. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
|
||
namespace Magento\ImportExport\Model\Import\Source; | ||
|
||
|
||
use Magento\Framework\ObjectManagerInterface; | ||
use Magento\ImportExport\Model\Import\AbstractSource; | ||
use Magento\ImportExport\Model\Import\Source\FileParser\ParserFactoryInterface; | ||
use Magento\ImportExport\Model\Import\Source\FileParser\ParserInterface; | ||
|
||
class FileFactory | ||
{ | ||
/** @var ParserFactoryInterface */ | ||
private $parserFactory; | ||
|
||
/** @var ObjectManagerInterface */ | ||
private $objectManager; | ||
|
||
public function __construct(ParserFactoryInterface $parserFactory, ObjectManagerInterface $objectManager) | ||
{ | ||
$this->parserFactory = $parserFactory; | ||
$this->objectManager = $objectManager; | ||
} | ||
|
||
/** | ||
* Creates file source from file path | ||
* | ||
* @param string $path | ||
* @param array $options | ||
* @return AbstractSource | ||
*/ | ||
public function createFromFilePath($path, array $options = []) | ||
{ | ||
return $this->createFromFileParser( | ||
$this->parserFactory->create($path, $options) | ||
); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we have two different methods of creation in the same class? |
||
/** | ||
* Creates file source from file parser | ||
* | ||
* @param ParserInterface $parser | ||
* @return AbstractSource | ||
*/ | ||
public function createFromFileParser(ParserInterface $parser) | ||
{ | ||
return $this->objectManager->create(File::class, ['parser' => $parser]); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento, Inc. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
|
||
namespace Magento\ImportExport\Model\Import\Source\FileParser; | ||
|
||
/** | ||
* File Parser Composite | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be marked as @ api , because used as an Extension Point |
||
* | ||
*/ | ||
class CompositeParserFactory implements ParserFactoryInterface | ||
{ | ||
/** @var ParserFactoryInterface[] */ | ||
private $parserFactories; | ||
|
||
public function __construct(array $parserFactories = []) | ||
{ | ||
$this->parserFactories = []; | ||
|
||
foreach ($parserFactories as $parserFactory) { | ||
$this->addParserFactory($parserFactory); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there should not be logic in the constructors , just assignments There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Variadic types are not supported by DI and I doubt it should be supported. For now, there are two possibilities to make a type check :
// Type check for array to make sure
// that all parser factories implement ParserFactoryInterface
$this->parserFactories = (function(ParserFactoryInterface ...$parserFactories) {
return $parserFactories;
})(...$parserFactories); |
||
} | ||
|
||
public function create($path, array $options = []) | ||
{ | ||
foreach ($this->parserFactories as $parserFactory) { | ||
$parser = $this->createParserIfSupported($parserFactory, $path, $options); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this does not look like a proper Object Oriented way of polymorphic object creation. because we instantiate in advance all the factories and just looping through all of them trying to instantiate needed parser |
||
|
||
if ($parser === false) { | ||
continue; | ||
} | ||
|
||
return $parser; | ||
} | ||
|
||
$this->thereWasNoParserFound($path); | ||
} | ||
|
||
public function addParserFactory(ParserFactoryInterface $parserFactory) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method should not be public, actually I doubt we need it at all , because all it does is just add item to the array |
||
{ | ||
$this->parserFactories[] = $parserFactory; | ||
} | ||
|
||
private function thereWasNoParserFound($path) | ||
{ | ||
throw new UnsupportedPathException($path); | ||
} | ||
|
||
private function createParserIfSupported(ParserFactoryInterface $parserFactory, $path, array $options) | ||
{ | ||
try { | ||
$parser = $parserFactory->create($path, $options); | ||
} catch (UnsupportedPathException $e) { | ||
return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this does not look like a proper way of work with Exceptions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate on this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The root cause of the problem described above
Because we don't use proper Object Oriented design to choose correct factory to be used and instantiate needed parser. We just look over all possible factories and try to instantiate parser with the help of each next factory in the list. So, our Exception is not actually exception (because Exception represents unexpected behavior, but here we have expected false results) that's why we have a juggling of Exception -> FALSE There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a valid case for actual CSV file parser factory or for ZIP file parser factory as if they are used directly by a client, returning false is a terrible thing. Adding if ($status) is worse practice as it increases cyclomatic complexity of client code and hidden bugs for unchecked conditions, exceptions look better in this case in my opinion. Also, a CompositeFactory does throw the same exception if no other parser factories gave a valid parser instance, so unexpected path gets notified up the chain. We cannot use here common for Magento 2 "file extension based" strategy pattern, as file path might be a stream or even a temporary file without the extension, so offloading logic to parser factory looks more clean and extensible. As well factory always throws UnsupportedPathException if the file cannot be read by any of the parsers. If you have a better idea how to solve this issue, I would be happy to change the code to that structure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idea is to have a decision point. The decision point in your case is your composite factory. In this case, Exception would be a real exceptional state, not like it's now when Exception is Expected behavior.
Yes, that's why I am advising you to make your code more Object Oriented friendly to get rid of such code $parser = $this->createParserIfSupported($parserFactory, $path, $options);
if ($parser === false) {
continue;
} Problems you have with Exceptions and False handling is just a side-effect of wrong OO design with choosing right Factory to handle the precise business case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you'd like I can move it to RegExp based detection on a path via factory selection strategy. But I wouldn't change actual CSV and ZIP factories for path validation, as they must throw an exception if the file is not in the expected format. |
||
} | ||
|
||
return $parser; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
/** | ||
* Copyright © Magento, Inc. All rights reserved. | ||
* See COPYING.txt for license details. | ||
*/ | ||
|
||
namespace Magento\ImportExport\Model\Import\Source\FileParser; | ||
|
||
class CorruptedFileException extends \RuntimeException | ||
{ | ||
private $fileName; | ||
|
||
public function __construct($fileName = '', $message = '') | ||
{ | ||
$this->fileName = $fileName; | ||
parent::__construct($message ?: sprintf('File "%s" is corrupted', $fileName)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. something wrong with this code, because it has two default parameters |
||
} | ||
|
||
|
||
public function getFileName() | ||
{ | ||
return $this->fileName; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need public getter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The file name can be of a help for client code that catches this specific exception. I can remove it if filename is redundant error information for parsing files. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't recommend to use Inheritance Based API, so all the methods should be whether public or private