diff --git a/Modules/CsvImport/Config/.gitkeep b/Modules/CsvImport/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Modules/CsvImport/Config/config.php b/Modules/CsvImport/Config/config.php new file mode 100644 index 0000000..1c64c21 --- /dev/null +++ b/Modules/CsvImport/Config/config.php @@ -0,0 +1,5 @@ + 'CsvImport' +]; diff --git a/Modules/CsvImport/CsvConverter.php b/Modules/CsvImport/CsvConverter.php new file mode 100644 index 0000000..3357269 --- /dev/null +++ b/Modules/CsvImport/CsvConverter.php @@ -0,0 +1,66 @@ +csvNavigator = $csvNavigator; + } + + /** + * @param Collection $collection + * + * @return \SplFileObject + */ + public function convertCollection(Collection $collection): \SplFileObject + { + $result = (new TmpFileService())->createFile()->openFile('w'); + foreach ($collection as $row) { + $result->fputcsv($this->toArray($row)); + } + + return $result; + } + + /** + * @param CsvConverterInterface $data + * + * @return array + */ + public function toArray(CsvConverterInterface $data): array + { + $result = []; + if ($data instanceof CsvHasHeaderInterface) { + $result[] = $data->getHeaderMapping(); + } + + foreach (ColumnPosition::$letterByPosition as $position => $letter) { + $objectParser = new ObjectParser($letter, $data); + if (!$objectParser->hasColumn()) { + $result[$position] = ''; + continue; + } + + $result[$position] = $this->csvNavigator->accept($objectParser->getValue()); + } + + return $result; + } +} diff --git a/Modules/CsvImport/CsvNavigator.php b/Modules/CsvImport/CsvNavigator.php new file mode 100644 index 0000000..ff3369c --- /dev/null +++ b/Modules/CsvImport/CsvNavigator.php @@ -0,0 +1,73 @@ +visitor = $visitor; + $this->handlerRegistry = $handlerRegistry; + } + + /** + * @param $data + * @param array|null $type + * + * @return mixed + */ + public function accept($data, array $type = null) + { + if (null === $type) { + $typeName = \gettype($data); + $type = ['name' => $typeName, 'params' => []]; + } + + switch ($type['name']) { + case 'NULL': + return $this->visitor->visitNull(); + + case 'string': + return $this->visitor->visitString($data); + + case 'int': + case 'integer': + return $this->visitor->visitInteger($data); + + case 'bool': + case 'boolean': + return $this->visitor->visitBoolean($data); + + case 'double': + case 'float': + return $this->visitor->visitDouble($data); + + case 'array': + case 'resource': + case 'object': + default: + if ($type['name'] == 'object') { + $type['name'] = get_class($data); + } + + if (null !== $handler = $this->handlerRegistry->getHandler($type['name'])) { + return \call_user_func($handler, $data); + } + + throw new \RuntimeException(sprintf('Type %s is not implement for convert', $type['name'])); + } + } +} \ No newline at end of file diff --git a/Modules/CsvImport/CsvVisitor.php b/Modules/CsvImport/CsvVisitor.php new file mode 100644 index 0000000..29641ca --- /dev/null +++ b/Modules/CsvImport/CsvVisitor.php @@ -0,0 +1,32 @@ +__dtConstruct(); + $this->properties = new Collection(); + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @param int $id + * + * @return $this; + */ + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + * + * @return $this; + */ + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getCode(): string + { + return $this->code; + } + + /** + * @param string $code + * + * @return $this; + */ + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + /** + * @return array + */ + public function getImages(): array + { + return $this->images; + } + + /** + * @param array $images + * + * @return $this; + */ + public function setImages(array $images): self + { + $this->images = $images; + + return $this; + } + + /** + * @return Collection + */ + public function getProperties(): Collection + { + return $this->properties; + } + + /** + * @param Collection $properties + * + * @return $this; + */ + public function setProperties(Collection $properties): self + { + $this->properties = $properties; + + return $this; + } + + /** + * @return array + */ + public function getColumnsMapping(): array + { + return [ + 'B' => 'name', + 'D' => 'code', + 'E' => 'dateCreated', + ]; + } +} diff --git a/Modules/CsvImport/Entities/Property.php b/Modules/CsvImport/Entities/Property.php new file mode 100644 index 0000000..e7d544e --- /dev/null +++ b/Modules/CsvImport/Entities/Property.php @@ -0,0 +1,77 @@ +id; + } + + /** + * @param int $id + * + * @return $this; + */ + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + * + * @return $this; + */ + public function setTitle(string $title): self + { + $this->title = $title; + + return $this; + } + + /** + * @return Product + */ + public function getProduct(): Product + { + return $this->product; + } + + /** + * @param Product $product + * + * @return $this; + */ + public function setProduct(Product $product): self + { + $this->product = $product; + + return $this; + } +} diff --git a/Modules/CsvImport/Handler/ColumnPosition.php b/Modules/CsvImport/Handler/ColumnPosition.php new file mode 100644 index 0000000..8d21d78 --- /dev/null +++ b/Modules/CsvImport/Handler/ColumnPosition.php @@ -0,0 +1,21 @@ + 'A', + 1 => 'B', + 2 => 'C', + 3 => 'D', + 4 => 'E', + 5 => 'F', + 6 => 'G', + ]; + + public static function getPositionByLetter(string $letter): int + { + return array_search($letter, static::$letterByPosition); + } +} \ No newline at end of file diff --git a/Modules/CsvImport/Handler/DateTimeHandler.php b/Modules/CsvImport/Handler/DateTimeHandler.php new file mode 100644 index 0000000..5519fdf --- /dev/null +++ b/Modules/CsvImport/Handler/DateTimeHandler.php @@ -0,0 +1,36 @@ +defaultFormat = $defaultFormat; + } + + /** + * @inheritdoc + */ + public static function getSubscribingMethods(): array + { + return [ + [ + 'type' => 'DateTime', + 'method' => 'serializeDateTime', + ], + ]; + } + + public function serializeDateTime(\DateTime $date): string + { + return $date->format($this->defaultFormat); + } +} diff --git a/Modules/CsvImport/Handler/HandleRegistry.php b/Modules/CsvImport/Handler/HandleRegistry.php new file mode 100644 index 0000000..29a4097 --- /dev/null +++ b/Modules/CsvImport/Handler/HandleRegistry.php @@ -0,0 +1,65 @@ +handlers = $handlers; + } + + /** + * @param SubscribingHandlerInterface $handler + */ + public function registerSubscribingHandler(SubscribingHandlerInterface $handler): void + { + foreach ($handler->getSubscribingMethods() as $methodData) { + if (!isset($methodData['type'])) { + throw new \RuntimeException( + sprintf( + 'For each subscribing method a "type" attribute must be given, but only got "%s" for %s.', + $methodData['type'], + \get_class($handler) + ) + ); + } + + + $this->registerHandler($methodData['type'], [$handler, $methodData['method']]); + } + } + + /** + * @param string $typeName + * @param callable $handler + */ + public function registerHandler(string $typeName, callable $handler) + { + $this->handlers[$typeName] = $handler; + } + + /** + * @param $typeName + * + * @return SubscribingHandlerInterface|null + */ + public function getHandler($typeName) + { + if (!isset($this->handlers[$typeName])) { + return null; + } + + return $this->handlers[$typeName]; + } +} diff --git a/Modules/CsvImport/Handler/HandlerRegistryInterface.php b/Modules/CsvImport/Handler/HandlerRegistryInterface.php new file mode 100644 index 0000000..1550245 --- /dev/null +++ b/Modules/CsvImport/Handler/HandlerRegistryInterface.php @@ -0,0 +1,30 @@ + 'DateTime', + * 'method' => 'serializeDateTime', + * ), + * ) + * + * The direction and method keys can be omitted. + * + * @return array + */ + public static function getSubscribingMethods(): array; +} diff --git a/Modules/CsvImport/Helpers/DateTimeTrait.php b/Modules/CsvImport/Helpers/DateTimeTrait.php new file mode 100644 index 0000000..b0240b1 --- /dev/null +++ b/Modules/CsvImport/Helpers/DateTimeTrait.php @@ -0,0 +1,62 @@ +dateCreated = new \DateTime(); + $this->dateUpdated = new \DateTime(); + } + + /** + * @return \DateTime + */ + public function getDateCreated(): \DateTime + { + return $this->dateCreated; + } + + /** + * @param \DateTime $dateCreated + * + * @return $this; + */ + public function setDateCreated(\DateTime $dateCreated): self + { + $this->dateCreated = $dateCreated; + + return $this; + } + + /** + * @return \DateTime + */ + public function getDateUpdated(): \DateTime + { + return $this->dateUpdated; + } + + /** + * @param \DateTime $dateUpdated + * + * @return $this; + */ + public function setDateUpdated(\DateTime $dateUpdated): self + { + $this->dateUpdated = $dateUpdated; + + return $this; + } +} diff --git a/Modules/CsvImport/Helpers/RandomGeneratedValue.php b/Modules/CsvImport/Helpers/RandomGeneratedValue.php new file mode 100644 index 0000000..3fee4d8 --- /dev/null +++ b/Modules/CsvImport/Helpers/RandomGeneratedValue.php @@ -0,0 +1,22 @@ +file)) { + $filePath = tempnam($this->tmpDir, ''); + } else { + $filePath = $this->file->getRealPath(); + } + + $fp = fopen($filePath, 'w'); + fwrite($fp, $content); + fclose($fp); + + $this->file = new File($filePath); + + return $this->file; + } + + public function setContent($content) + { + if (empty($this->file)) { + throw new \Exception('File is empty'); + } + + $fp = fopen($this->file->getRealPath(), 'w'); + fwrite($fp, $content); + fclose($fp); + } + + /** + * @return File + */ + public function getFile() + { + return $this->file; + } + + /** + * @void + */ + public function deleteFile() + { + @unlink($this->file->getRealPath()); + $this->file = NULL; + } + + /** + * Use this function carefully !!! + */ + public function clearAllFiles() + { + foreach (scandir($this->tmpDir) as $row) { + if (is_dir($this->tmpDir . DIRECTORY_SEPARATOR . $row)) { + continue; + } + + @unlink($this->tmpDir . DIRECTORY_SEPARATOR . $row); + } + } +} \ No newline at end of file diff --git a/Modules/CsvImport/Parser/ObjectParser.php b/Modules/CsvImport/Parser/ObjectParser.php new file mode 100644 index 0000000..6843206 --- /dev/null +++ b/Modules/CsvImport/Parser/ObjectParser.php @@ -0,0 +1,48 @@ +letter = $letter; + $this->object = $object; + } + + + public function hasColumn(): bool + { + return isset($this->object->getColumnsMapping()[$this->letter]); + } + + public function getValue() + { + $methodName = $this->getGetterName(); + if (!method_exists($this->object, $methodName)) { + throw new \RuntimeException( + sprintf('Method %s is not implement in class %s', $methodName, get_class($this->object)) + ); + } + + return $this->object->{$methodName}(); + } + + private function getGetterName(): string + { + return 'get' . ucfirst($this->object->getColumnsMapping()[$this->letter]); + } +} diff --git a/Modules/CsvImport/Providers/.gitkeep b/Modules/CsvImport/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Modules/CsvImport/Providers/CsvImportServiceProvider.php b/Modules/CsvImport/Providers/CsvImportServiceProvider.php new file mode 100644 index 0000000..9f2fda6 --- /dev/null +++ b/Modules/CsvImport/Providers/CsvImportServiceProvider.php @@ -0,0 +1,59 @@ +registerConfig(); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + // + } + + /** + * Register config. + * + * @return void + */ + protected function registerConfig() + { + $this->publishes([ + __DIR__.'/../Config/config.php' => config_path('csvimport.php'), + ], 'config'); + $this->mergeConfigFrom( + __DIR__.'/../Config/config.php', 'csvimport' + ); + } + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return []; + } +} diff --git a/Modules/CsvImport/Tests/.gitkeep b/Modules/CsvImport/Tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Modules/CsvImport/Tests/Features/CsvConverterTest.php b/Modules/CsvImport/Tests/Features/CsvConverterTest.php new file mode 100644 index 0000000..201401e --- /dev/null +++ b/Modules/CsvImport/Tests/Features/CsvConverterTest.php @@ -0,0 +1,41 @@ +registerSubscribingHandler(new DateTimeHandler()); + $navigator = new CsvNavigator(new CsvVisitor(), $handleRegistry); + $this->csvConverter = new CsvConverter($navigator); + } + + public function test_toCsv_withProductFixutre_shouldReturnArray() + { + $product = ProductFixture::create(); + $result = $this->csvConverter->toArray($product); + $this->assertNotEmpty($result); + } + + public function test_convertCollection_withProductCollection_shouldReturnFile() + { + $product = ProductFixture::createCollection(); + $result = $this->csvConverter->convertCollection($product); + $this->assertInstanceOf(\SplFileObject::class, $result); + } +} + diff --git a/Modules/CsvImport/Tests/Fixtures/CollectionFixture.php b/Modules/CsvImport/Tests/Fixtures/CollectionFixture.php new file mode 100644 index 0000000..d959777 --- /dev/null +++ b/Modules/CsvImport/Tests/Fixtures/CollectionFixture.php @@ -0,0 +1,38 @@ +push(static::create()); + } + + return $result; + } + + abstract public static function create(); +} diff --git a/Modules/CsvImport/Tests/Fixtures/ProductFixture.php b/Modules/CsvImport/Tests/Fixtures/ProductFixture.php new file mode 100644 index 0000000..aba1f39 --- /dev/null +++ b/Modules/CsvImport/Tests/Fixtures/ProductFixture.php @@ -0,0 +1,29 @@ +setId(RandomGeneratedValue::getUniqueInteger()) + ->setName(RandomGeneratedValue::getUniqueString()) + ->setCode(RandomGeneratedValue::getUniqueString()) + ->setImages( + [ + RandomGeneratedValue::getUniqueString(), + RandomGeneratedValue::getUniqueString(), + ] + ); + $result->setProperties(PropertyFixture::createCollectionByProduct($result)); + + return $result; + } +} diff --git a/Modules/CsvImport/Tests/Fixtures/PropertyFixture.php b/Modules/CsvImport/Tests/Fixtures/PropertyFixture.php new file mode 100644 index 0000000..3cc124f --- /dev/null +++ b/Modules/CsvImport/Tests/Fixtures/PropertyFixture.php @@ -0,0 +1,36 @@ +setId(RandomGeneratedValue::getUniqueInteger()) + ->setTitle(RandomGeneratedValue::getUniqueString()); + } + + /** + * @param Product $product + * + * @return Collection + */ + public static function createCollectionByProduct(Product $product): Collection + { + /** @var Collection|Property[] $result */ + $result = self::createCollection(); + foreach ($result as $row) { + $row->setProduct($product); + } + + return $result; + } +} diff --git a/Modules/CsvImport/composer.json b/Modules/CsvImport/composer.json new file mode 100644 index 0000000..7807472 --- /dev/null +++ b/Modules/CsvImport/composer.json @@ -0,0 +1,25 @@ +{ + "name": "nwidart/csvimport", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [ + "Modules\\CsvImport\\Providers\\CsvImportServiceProvider" + ], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\CsvImport\\": "" + } + } +} diff --git a/Modules/CsvImport/module.json b/Modules/CsvImport/module.json new file mode 100644 index 0000000..657f6df --- /dev/null +++ b/Modules/CsvImport/module.json @@ -0,0 +1,16 @@ +{ + "name": "CsvImport", + "alias": "csvimport", + "description": "", + "keywords": [], + "active": 1, + "order": 0, + "providers": [ + "Modules\\CsvImport\\Providers\\CsvImportServiceProvider" + ], + "aliases": {}, + "files": [ + "start.php" + ], + "requires": [] +} diff --git a/Modules/CsvImport/package.json b/Modules/CsvImport/package.json new file mode 100644 index 0000000..7c1522a --- /dev/null +++ b/Modules/CsvImport/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "scripts": { + "dev": "npm run development", + "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch-poll": "npm run watch -- --watch-poll", + "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", + "prod": "npm run production", + "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" + }, + "devDependencies": { + "cross-env": "^5.1.4", + "laravel-mix": "^2.1", + "laravel-mix-merge-manifest": "^0.1.1" + } +} \ No newline at end of file diff --git a/Modules/CsvImport/start.php b/Modules/CsvImport/start.php new file mode 100644 index 0000000..f4165e8 --- /dev/null +++ b/Modules/CsvImport/start.php @@ -0,0 +1 @@ + 'Modules', + + /* + |-------------------------------------------------------------------------- + | Module Stubs + |-------------------------------------------------------------------------- + | + | Default module stubs. + | + */ + + 'stubs' => [ + 'enabled' => false, + 'path' => base_path() . '/vendor/nwidart/laravel-modules/src/Commands/stubs', + 'files' => [ + 'start' => 'start.php', + 'routes' => 'Http/routes.php', + 'views/index' => 'Resources/views/index.blade.php', + 'views/master' => 'Resources/views/layouts/master.blade.php', + 'scaffold/config' => 'Config/config.php', + 'composer' => 'composer.json', + 'assets/js/app' => 'Resources/assets/js/app.js', + 'assets/sass/app' => 'Resources/assets/sass/app.scss', + 'webpack' => 'webpack.mix.js', + 'package' => 'package.json', + ], + 'replacements' => [ + 'start' => ['LOWER_NAME', 'ROUTES_LOCATION'], + 'routes' => ['LOWER_NAME', 'STUDLY_NAME', 'MODULE_NAMESPACE'], + 'webpack' => ['LOWER_NAME'], + 'json' => ['LOWER_NAME', 'STUDLY_NAME', 'MODULE_NAMESPACE'], + 'views/index' => ['LOWER_NAME'], + 'views/master' => ['LOWER_NAME', 'STUDLY_NAME'], + 'scaffold/config' => ['STUDLY_NAME'], + 'composer' => [ + 'LOWER_NAME', + 'STUDLY_NAME', + 'VENDOR', + 'AUTHOR_NAME', + 'AUTHOR_EMAIL', + 'MODULE_NAMESPACE', + ], + ], + 'gitkeep' => true, + ], + 'paths' => [ + /* + |-------------------------------------------------------------------------- + | Modules path + |-------------------------------------------------------------------------- + | + | This path used for save the generated module. This path also will be added + | automatically to list of scanned folders. + | + */ + + 'modules' => base_path('Modules'), + /* + |-------------------------------------------------------------------------- + | Modules assets path + |-------------------------------------------------------------------------- + | + | Here you may update the modules assets path. + | + */ + + 'assets' => public_path('modules'), + /* + |-------------------------------------------------------------------------- + | The migrations path + |-------------------------------------------------------------------------- + | + | Where you run 'module:publish-migration' command, where do you publish the + | the migration files? + | + */ + + 'migration' => base_path('database/migrations'), + /* + |-------------------------------------------------------------------------- + | Generator path + |-------------------------------------------------------------------------- + | Customise the paths where the folders will be generated. + | Set the generate key to false to not generate that folder + */ + 'generator' => [ + 'config' => ['path' => 'Config', 'generate' => true], + 'command' => ['path' => 'Console', 'generate' => true], + 'migration' => ['path' => 'Database/Migrations', 'generate' => true], + 'seeder' => ['path' => 'Database/Seeders', 'generate' => true], + 'factory' => ['path' => 'Database/factories', 'generate' => true], + 'model' => ['path' => 'Entities', 'generate' => true], + 'controller' => ['path' => 'Http/Controllers', 'generate' => true], + 'filter' => ['path' => 'Http/Middleware', 'generate' => true], + 'request' => ['path' => 'Http/Requests', 'generate' => true], + 'provider' => ['path' => 'Providers', 'generate' => true], + 'assets' => ['path' => 'Resources/assets', 'generate' => true], + 'lang' => ['path' => 'Resources/lang', 'generate' => true], + 'views' => ['path' => 'Resources/views', 'generate' => true], + 'test' => ['path' => 'Tests', 'generate' => true], + 'repository' => ['path' => 'Repositories', 'generate' => false], + 'event' => ['path' => 'Events', 'generate' => false], + 'listener' => ['path' => 'Listeners', 'generate' => false], + 'policies' => ['path' => 'Policies', 'generate' => false], + 'rules' => ['path' => 'Rules', 'generate' => false], + 'jobs' => ['path' => 'Jobs', 'generate' => false], + 'emails' => ['path' => 'Emails', 'generate' => false], + 'notifications' => ['path' => 'Notifications', 'generate' => false], + 'resource' => ['path' => 'Transformers', 'generate' => false], + ], + ], + /* + |-------------------------------------------------------------------------- + | Scan Path + |-------------------------------------------------------------------------- + | + | Here you define which folder will be scanned. By default will scan vendor + | directory. This is useful if you host the package in packagist website. + | + */ + + 'scan' => [ + 'enabled' => false, + 'paths' => [ + base_path('vendor/*/*'), + ], + ], + /* + |-------------------------------------------------------------------------- + | Composer File Template + |-------------------------------------------------------------------------- + | + | Here is the config for composer.json file, generated by this package + | + */ + + 'composer' => [ + 'vendor' => 'nwidart', + 'author' => [ + 'name' => 'Nicolas Widart', + 'email' => 'n.widart@gmail.com', + ], + ], + /* + |-------------------------------------------------------------------------- + | Caching + |-------------------------------------------------------------------------- + | + | Here is the config for setting up caching feature. + | + */ + 'cache' => [ + 'enabled' => false, + 'key' => 'laravel-modules', + 'lifetime' => 60, + ], + /* + |-------------------------------------------------------------------------- + | Choose what laravel-modules will register as custom namespaces. + | Setting one to false will require you to register that part + | in your own Service Provider class. + |-------------------------------------------------------------------------- + */ + 'register' => [ + 'translations' => true, + /** + * load files on boot or register method + * + * Note: boot not compatible with asgardcms + * + * @example boot|register + */ + 'files' => 'register', + ], +];