├── .gitignore ├── tests ├── files │ ├── bad_word.csv │ ├── invalid_guitars.csv │ └── guitars.csv ├── Queue │ ├── run-queue-app-with-file-cache-driver.php │ ├── run-queue-app-with-redis-cache-driver.php │ ├── run-queue-app-with-memcached-cache-driver.php │ └── AppSetUp.php ├── FileMutexFunctionalityTest.php ├── RedisMutexFunctionalityTest.php ├── MemcachedMutexFunctionalityTest.php ├── CsvImporters │ ├── CastFilters │ │ └── MyCastFilter.php │ ├── ValidationFilters │ │ └── MyValidationFilter.php │ ├── HeadersFilters │ │ └── MyHeadersFilter.php │ ├── CustomValidationImporter.php │ ├── CsvImporter.php │ └── AsyncCsvImporter.php ├── CsvManipulationsTest.php ├── Jobs │ └── TestImportJob.php ├── StandardFiltersTest.php ├── BaseTestCase.php ├── SettersAndGettersTest.php ├── CustomFiltersTest.php └── MutexFunctionality.php ├── src ├── BaseCastFilter.php ├── Exceptions │ ├── ImportValidationException.php │ └── CsvImporterException.php ├── NameableTrait.php ├── stubs │ ├── cast_filter.stub │ ├── validation_filter.stub │ ├── headers_filter.stub │ └── importer.stub ├── BaseValidationFilter.php ├── ClosureCastFilter.php ├── ClosureHeadersFilter.php ├── ClosureValidationFilter.php ├── BaseHeadersFilter.php ├── Commands │ ├── MakeCsvImporter.php │ ├── MakeCastFilter.php │ ├── MakeHeadersFilter.php │ └── MakeValidationFilter.php ├── CsvImporterServiceProvider.php ├── CsvImporterConfigurationTrait.php ├── config │ └── csv-importer.php └── BaseCsvImporter.php ├── phpunit.xml ├── LICENSE.txt ├── composer.json ├── .travis.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | /tests/files/import/* 4 | /tests/files/cache/* 5 | .idea -------------------------------------------------------------------------------- /tests/files/bad_word.csv: -------------------------------------------------------------------------------- 1 | serial_number,company,title,email,date,date_time 2 | 1,ESP,Ltd MH-1000ET,test@test.test,26-02-2017,2017-02-26 10:00:12 3 | 1,ESP,bad_word,test@test.test,7-02-26,2017-02-26 10:00t3 -------------------------------------------------------------------------------- /tests/Queue/run-queue-app-with-file-cache-driver.php: -------------------------------------------------------------------------------- 1 | setCacheDriver('file')->setUp(); 6 | 7 | \Illuminate\Support\Facades\Artisan::call('queue:work'); -------------------------------------------------------------------------------- /tests/Queue/run-queue-app-with-redis-cache-driver.php: -------------------------------------------------------------------------------- 1 | setCacheDriver('redis')->setUp(); 6 | 7 | \Illuminate\Support\Facades\Artisan::call('queue:work'); -------------------------------------------------------------------------------- /src/BaseCastFilter.php: -------------------------------------------------------------------------------- 1 | setCacheDriver('memcached')->setUp(); 6 | 7 | \Illuminate\Support\Facades\Artisan::call('queue:work'); -------------------------------------------------------------------------------- /src/Exceptions/ImportValidationException.php: -------------------------------------------------------------------------------- 1 | name && is_string($this->name)) ? $this->name : (new \ReflectionClass($this))->getShortName(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/stubs/cast_filter.stub: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Exceptions/CsvImporterException.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 22 | } 23 | 24 | /** 25 | * @param $value 26 | * @return mixed 27 | */ 28 | public function filter($value) 29 | { 30 | return $this->closure->__invoke($value); 31 | } 32 | } -------------------------------------------------------------------------------- /src/ClosureHeadersFilter.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 22 | } 23 | 24 | /** 25 | * @param array $csvHeaders 26 | * @return bool 27 | */ 28 | public function filter(array $csvHeaders) 29 | { 30 | return $this->closure->__invoke($csvHeaders); 31 | } 32 | } -------------------------------------------------------------------------------- /src/stubs/headers_filter.stub: -------------------------------------------------------------------------------- 1 | closure = $closure; 27 | } 28 | 29 | /** 30 | * @param array $value 31 | * @return bool 32 | */ 33 | public function filter($value) 34 | { 35 | return $this->closure->__invoke($value); 36 | } 37 | } -------------------------------------------------------------------------------- /tests/Queue/AppSetUp.php: -------------------------------------------------------------------------------- 1 | cacheDriver = $driver; 33 | 34 | return $this; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/BaseHeadersFilter.php: -------------------------------------------------------------------------------- 1 | filter($csvHeaders)) { 27 | return (object)['error' => false]; 28 | } 29 | 30 | return (object)['error' => true, 'message' => $this->errorMessage]; 31 | } 32 | } -------------------------------------------------------------------------------- /tests/files/guitars.csv: -------------------------------------------------------------------------------- 1 | serial_number,company,title,email,date,date_time 2 | 1,ESP,Ltd MH-1000ET,test@test.test,26-02-2017,2017-02-26 10:00:12 3 | 1,ESP,Ltd MH-1000ET,test@test.test,7-02-26,2017-02-26 10:00t3 4 | 2,Ibanez,TAM100 Tosin Abasi Signature,test@test.test,2017-02-26,2017-02-27 10:00 5 | 3,Music Man,John Petrucci signature,test@test.test,2017-02-26,2017-02-27 10:00 6 | 4,Mayones,Regius MM QM,test@test.test,2017-02-26,2017-02-27 10:00 7 | 5,Strandberg,Boden 6 Custom,test@test.test,2017-02-26,2017-02-27 10:00 8 | 6,Blackmachine,B6,test@test.test,2017-02-26,2017-02-27 10:00 9 | not_numeric,Jackson,Misha Mansoor Juggernaut HT6,test@test.test,2017-02-26,2017-02-27 10:00 10 | 8,Skervesen,Raptor 7,invalid_email,2017-02-26,2017-02-27 10:00 11 | 9,Daemoness,Cimmerian 7,test@test.test,2017-02-26,2017-02-27 10:00 12 | 10,Mayones,Duvell 7 Elite,test@test.test,2017-02-26,2017-02-27 10:00 13 | 11,Mayones,,test@test.test,2017-02-26,2017-02-27 10:00 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 RGSoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgilyov/laravel-csv-importer", 3 | "description": "Easy and reliable way to import, parse, validate and transform your csv files with laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["csv", "import", "laravel", "parse", "validate", "transform"], 7 | "authors": [ 8 | { 9 | "name": "Roman Gilyov", 10 | "email": "rgilyov@gmail.com" 11 | } 12 | ], 13 | "require-dev": { 14 | "orchestra/testbench": "^3.0", 15 | "phpunit/phpunit": "^5.7", 16 | "mockery/mockery": "^0.9.4", 17 | "predis/predis": "1.0.*" 18 | }, 19 | "require": { 20 | "php": ">=5.6.4", 21 | "arvenil/ninja-mutex": "0.6.0", 22 | "league/csv": "8.0", 23 | "laravel/framework": "^5.1 || ^6.0", 24 | "nesbot/carbon": "^1.20 || ^2.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "RGilyov\\CsvImporter\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "RGilyov\\CsvImporter\\Test\\": "tests/" 34 | } 35 | }, 36 | "minimum-stability": "dev" 37 | } 38 | -------------------------------------------------------------------------------- /src/Commands/MakeCsvImporter.php: -------------------------------------------------------------------------------- 1 | publishes([ 21 | __DIR__.'/config/csv-importer.php' => config_path('csv-importer.php') 22 | ], 'config'); 23 | } 24 | 25 | /** 26 | * Register any application services. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->mergeConfigFrom(__DIR__ . '/config/csv-importer.php', 'csv-importer'); 33 | 34 | if (method_exists($this, 'commands')) { 35 | $this->commands([ 36 | MakeCsvImporter::class, 37 | MakeHeadersFilter::class, 38 | MakeValidationFilter::class, 39 | MakeCastFilter::class 40 | ]); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | include: 5 | - php: 5.6 6 | env: TESTBENCH_VERSION=3.1.* PHPUNIT_VERSION=5.7.* LARAVEL_VERSION=5.1 7 | - php: 7.1 8 | env: TESTBENCH_VERSION=3.4.* PHPUNIT_VERSION=6.0.* LARAVEL_VERSION=5.4 9 | - php: 7.2 10 | env: TESTBENCH_VERSION=3.6.* PHPUNIT_VERSION=7.0.* LARAVEL_VERSION=5.6 11 | 12 | services: 13 | - memcached 14 | - redis-server 15 | 16 | before_script: 17 | - if [[ $TRAVIS_PHP_VERSION != "hhvm" ]]; then echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi; 18 | - if [[ $TRAVIS_PHP_VERSION != "hhvm" ]]; then echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi; 19 | - composer self-update 20 | - composer require orchestra/testbench:${TESTBENCH_VERSION} --no-update 21 | - composer require phpunit/phpunit:${PHPUNIT_VERSION} --no-update 22 | - composer require laravel/framework:${LARAVEL_VERSION} --no-update 23 | - composer install --prefer-source --no-interaction --dev 24 | - php tests/Queue/run-queue-app-with-file-cache-driver.php & 25 | - php tests/Queue/run-queue-app-with-redis-cache-driver.php & 26 | - php tests/Queue/run-queue-app-with-memcached-cache-driver.php & 27 | 28 | script: 29 | - vendor/bin/phpunit -------------------------------------------------------------------------------- /tests/CsvManipulationsTest.php: -------------------------------------------------------------------------------- 1 | importer = (new CsvImporter())->setCsvFile(__DIR__.'/files/guitars.csv'); 20 | } 21 | 22 | /** @test */ 23 | public function it_can_count_the_csv() 24 | { 25 | $quantity = $this->importer->countCsv(); 26 | 27 | $this->assertEquals(12, $quantity); 28 | } 29 | 30 | /** @test */ 31 | public function it_can_extract_distinct_values_from_the_given_csv() 32 | { 33 | $distinct = $this->importer->distinct('title'); 34 | 35 | $this->assertEquals('TAM100 Tosin Abasi Signature', $distinct[1]); 36 | $this->assertEquals(10, count($distinct)); 37 | } 38 | 39 | /** @test */ 40 | public function it_can_iterate_the_given_csv() 41 | { 42 | $companies = []; 43 | 44 | $this->importer->each(function ($item) use (&$companies) { 45 | $companies[] = $item['company']; 46 | }); 47 | 48 | $this->assertEquals('ESP', $companies[0]); 49 | $this->assertEquals(12, count($companies)); 50 | } 51 | } -------------------------------------------------------------------------------- /tests/Jobs/TestImportJob.php: -------------------------------------------------------------------------------- 1 | cacheDriver = $driver; 28 | } 29 | 30 | /** 31 | * Execute the job. 32 | * 33 | * @return void 34 | */ 35 | public function handle() 36 | { 37 | if (config('cache.default') != $this->cacheDriver) { 38 | dispatch(new TestImportJob($this->cacheDriver)); 39 | return; 40 | } 41 | 42 | try { 43 | Cache::forever( 44 | 'csv_importer_response', 45 | (new AsyncCsvImporter())->setCsvFile(__DIR__.'/../files/guitars.csv')->setAsyncMode(true)->run() 46 | ); 47 | } catch (\Exception $e) { 48 | Cache::forever('csv_importer_response', $e->getMessage()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/stubs/importer.stub: -------------------------------------------------------------------------------- 1 | [ 18 | 'name' => ['required', 'validation' => ['required'], 'cast' => 'string'], 19 | 'email' => ['validation' => ['email'], 'cast' => 'string'] 20 | ], 21 | 'csv_files' => [ 22 | 'valid_entities' => 'import/valid_entities.csv', 23 | 'invalid_entities' => 'import/invalid_entities.csv', 24 | ] 25 | ]; 26 | } 27 | 28 | /** 29 | * Will be executed for a csv line if it passed validation 30 | * 31 | * @param $item 32 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 33 | * @return void 34 | */ 35 | public function handle($item) 36 | { 37 | $this->insertTo('valid_entities', $item); 38 | } 39 | 40 | /** 41 | * Will be executed if a csv line did not pass validation 42 | * 43 | * @param $item 44 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 45 | * @return void 46 | */ 47 | public function invalid($item) 48 | { 49 | $this->insertTo('invalid_entities', $item); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CsvImporterConfigurationTrait.php: -------------------------------------------------------------------------------- 1 | getConfigHelper(); 23 | } 24 | 25 | return $configHelper->get($key); 26 | } 27 | 28 | /** 29 | * Inject given config file into an instance of Laravel's config 30 | * 31 | * @throws \Exception when the configuration file is not found 32 | * @return \Illuminate\Config\Repository configuration repository 33 | */ 34 | protected function getConfigHelper() 35 | { 36 | $configFile = $this->getConfigFile(); 37 | 38 | if (!file_exists($configFile)) { 39 | throw new \Exception('Config file not found.'); 40 | } 41 | 42 | return new Repository(['csv-importer' => require $configFile]); 43 | } 44 | 45 | /** 46 | * Get the config path and file name 47 | * 48 | * @return string config file path 49 | */ 50 | protected function getConfigFile() 51 | { 52 | return __DIR__ . '/config/csv-importer.php'; 53 | } 54 | } -------------------------------------------------------------------------------- /tests/CsvImporters/CustomValidationImporter.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'company' => ['validation' => ['string']], 19 | 'serial_number' => ['required', 'validation' => ['numeric'], 'cast' => 'string'], 20 | 'title' => ['validation' => ['required', 'bad_word_validation'], 'cast' => 'string'] 21 | ], 22 | 'csv_files' => [ 23 | 'valid_entities' => '/valid_entities.csv', 24 | 'invalid_entities' => '/invalid_entities.csv', 25 | ] 26 | ]; 27 | } 28 | 29 | /** 30 | * Will be executed for a csv line if it passes validation 31 | * 32 | * @param $item 33 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 34 | * @return void 35 | */ 36 | public function handle($item) 37 | { 38 | $this->insertTo('valid_entities', $item); 39 | } 40 | 41 | /** 42 | * Will be executed if a csv line did not pass validation 43 | * 44 | * @param $item 45 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 46 | * @return void 47 | */ 48 | public function invalid($item) 49 | { 50 | $this->insertTo('invalid_entities', $item); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/CsvImporters/CsvImporter.php: -------------------------------------------------------------------------------- 1 | configDate = ($configDate) ? 'Y-m-d' : null; 16 | } 17 | 18 | /** 19 | * Specify mappings and rules for our csv, we also may create csv files when we can write csv entities 20 | * 21 | * @return array 22 | */ 23 | public function csvConfigurations() 24 | { 25 | return [ 26 | 'mappings' => [ 27 | 'serial_number' => ['required', 'validation' => ['numeric'], 'cast' => 'string'], 28 | 'title' => ['validation' => ['required'], 'cast' => ['string', 'lowercase']], 29 | 'company' => ['validation' => ['string'], 'cast' => 'super_caster'], 30 | 'some_field_1' => ['cast' => 'string'], 31 | 'some_field_2' => [], 32 | 'email' => ['validation' => 'email'], 33 | 'date' => ['cast' => 'date'], 34 | 'date_time' => ['cast' => 'date_time'] 35 | ], 36 | 'csv_files' => [ 37 | 'valid_entities' => '/valid_entities.csv', 38 | 'invalid_entities' => '/invalid_entities.csv', 39 | ], 40 | 'config' => [ 41 | 'csv_date_format' => $this->configDate 42 | ] 43 | ]; 44 | } 45 | 46 | /** 47 | * Will be executed for a csv line if it passes validation 48 | * 49 | * @param $item 50 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 51 | * @return void 52 | */ 53 | public function handle($item) 54 | { 55 | $this->insertTo('valid_entities', $item); 56 | } 57 | 58 | /** 59 | * Will be executed if a csv line did not pass validation 60 | * 61 | * @param $item 62 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 63 | * @return void 64 | */ 65 | public function invalid($item) 66 | { 67 | $this->insertTo('invalid_entities', $item); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/StandardFiltersTest.php: -------------------------------------------------------------------------------- 1 | expectException(CsvImporterException::class); 15 | 16 | (new CsvImporter())->setCsvFile(__DIR__.'/files/invalid_guitars.csv')->run(); 17 | } 18 | 19 | /** @test */ 20 | public function validation_filters() 21 | { 22 | $invalidEntities = $this->getResultCsv($this->importCsv()['files']['invalid_entities']); 23 | 24 | $this->assertEquals('not_numeric', $invalidEntities[1][0]); 25 | $this->assertEquals('invalid_email', $invalidEntities[2][3]); 26 | $this->assertEquals('', $invalidEntities[3][2]); 27 | } 28 | 29 | /** @test */ 30 | public function cast_date_filters() 31 | { 32 | /* 33 | * only date cast functionality will be tested coz usual php cast system works pretty straight forward 34 | */ 35 | 36 | $entities = $this->getResultCsv($this->importCsv()['files']['valid_entities']); 37 | 38 | $this->assertEquals('2017-02-26', $entities[1][4]); 39 | $this->assertEquals('2017-02-26 10:00:12', $entities[1][5]); 40 | $this->assertEquals('2007-02-26', $entities[2][4]); 41 | $this->assertEquals('0001-01-01 00:00:00', $entities[2][5]); 42 | 43 | $importer = (new CsvImporter(true))->setCsvFile(__DIR__.'/files/guitars.csv'); 44 | 45 | $importer->run(); 46 | 47 | $entities = $this->getResultCsv($importer->finish()['files']['valid_entities']); 48 | 49 | $this->assertEquals('2017-02-26', $entities[1][4]); 50 | $this->assertEquals('2017-02-26 10:00:12', $entities[1][5]); 51 | $this->assertEquals('2007-02-26', $entities[2][4]); 52 | $this->assertEquals('0001-01-01 00:00:00', $entities[2][5]); 53 | 54 | $importer = (new CsvImporter())->setCsvFile(__DIR__.'/files/guitars.csv'); 55 | 56 | $importer->setCsvDateFormat('Y-m-d')->run(); 57 | 58 | $entities = $this->getResultCsv($importer->finish()['files']['valid_entities']); 59 | 60 | $this->assertEquals('0001-01-01', $entities[1][4]); 61 | $this->assertEquals('0001-01-01 00:00:00', $entities[1][5]); 62 | $this->assertEquals('0007-02-26', $entities[2][4]); 63 | $this->assertEquals('0001-01-01 00:00:00', $entities[2][5]); 64 | } 65 | } -------------------------------------------------------------------------------- /tests/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | set('cache.default', $this->cacheDriver); 38 | $app['config']->set('queue.default', 'redis'); 39 | $app['config']->set('cache.stores.file', [ 40 | 'driver' => 'file', 41 | 'path' => static::$cachePath, 42 | ]); 43 | $app['config']->set('filesystems.default', 'local'); 44 | $app['config']->set('filesystems.disks.local', [ 45 | 'driver' => 'local', 46 | 'root' => static::$filesPath, 47 | ]); 48 | } 49 | 50 | public function tearDown() 51 | { 52 | File::deleteDirectory(static::$cachePath, true); 53 | File::deleteDirectory(static::$filesPath, true); 54 | 55 | parent::tearDown(); 56 | } 57 | 58 | ////////////////////////////////////////////////////////// 59 | 60 | /** 61 | * @param $path 62 | * @return array 63 | */ 64 | protected function getResultCsv($path) 65 | { 66 | $res = fopen($path, 'r'); 67 | 68 | $csvEntities = []; 69 | while ($entity = fgetcsv($res, 1000)) { 70 | $csvEntities[] = $entity; 71 | } 72 | 73 | return $csvEntities; 74 | } 75 | 76 | /** 77 | * @param null $path 78 | * @return array 79 | */ 80 | protected function importCsv($path = null) 81 | { 82 | $importer = (new CsvImporter())->setCsvFile(($path) ? $path : __DIR__.'/files/guitars.csv'); 83 | 84 | $importer->run(); 85 | 86 | return $importer->finish(); 87 | } 88 | } -------------------------------------------------------------------------------- /src/config/csv-importer.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 28 | 29 | 'mutex_lock_time' => 300, 30 | 31 | 'memory_limit' => 128, 32 | 33 | /* 34 | * An import class's short name (without namespace) by default 35 | */ 36 | 'mutex_lock_key' => null, 37 | 38 | /* 39 | * Encoding of given csv file 40 | */ 41 | 'input_encoding' => 'UTF-8', 42 | 43 | /* 44 | * Encoding of processed csv values 45 | */ 46 | 'output_encoding' => 'UTF-8', 47 | 48 | /* 49 | * Specify which date format the given csv file has 50 | * to use `date` ('Y-m-d') and `datetime` ('Y-m-d H:i:s') casters, 51 | * if the parameter will be set to `null` `date` caster will replace 52 | * `/` and `\` and `|` and `.` and `,` on `-` and will assume that 53 | * the given csv file has `Y-m-d` or `d-m-Y` date format 54 | */ 55 | 'csv_date_format' => null, 56 | 57 | 'delimiter' => ',', 58 | 59 | 'enclosure' => '"', 60 | 61 | /* 62 | * Warning: The library depends on PHP SplFileObject class. 63 | * Since this class exhibits a reported bug (https://bugs.php.net/bug.php?id=55413), 64 | * Data using the escape character are correctly 65 | * escaped but the escape character is not removed from the CSV content. 66 | */ 67 | 'escape' => '\\', 68 | 69 | 'newline' => "\n", 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Progress bar messages 74 | |-------------------------------------------------------------------------- 75 | */ 76 | 77 | 'does_not_running' => 'Import process does not run', 78 | 'initialization' => 'Initialization', 79 | 'progress' => 'Import process is running', 80 | 'final_stage' => 'Final stage', 81 | 'finished' => 'Almost done, please click to the `finish` button to proceed', 82 | 'final' => 'The import process successfully finished!' 83 | 84 | ]; 85 | -------------------------------------------------------------------------------- /tests/SettersAndGettersTest.php: -------------------------------------------------------------------------------- 1 | setCsvDateFormat('y-m-d') 15 | ->setDelimiter('d') 16 | ->setEnclosure('e') 17 | ->setEscape("x") 18 | ->setCsvFile(__DIR__.'/files/guitars.csv') 19 | ->setInputEncoding('RANCH DUBOIS-8') 20 | ->setOutputEncoding('Bird up-23') 21 | ->setNewline('newline'); 22 | 23 | $this->assertEquals('y-m-d', $importer->csvDateFormat); 24 | $this->assertEquals('y-m-d', $importer->getCsvDateFormat()); 25 | $this->assertEquals('y-m-d', $importer->yo('CsvDateFormat')); 26 | $this->assertEquals('d', $importer->delimiter); 27 | $this->assertEquals('d', $importer->getDelimiter()); 28 | $this->assertEquals('d', $importer->commentCaVa('Delimiter')); 29 | $this->assertEquals('e', $importer->enclosure); 30 | $this->assertEquals('e', $importer->getEnclosure()); 31 | $this->assertEquals('e', $importer->caVa('Enclosure')); 32 | $this->assertEquals('x', $importer->escape); 33 | $this->assertEquals('x', $importer->getEscape()); 34 | $this->assertEquals('x', $importer->quelleEstVotreBurritoAmigoQuestionMark('escape')); 35 | $this->assertEquals(__DIR__.'/files/guitars.csv', $importer->csvFile); 36 | $this->assertEquals(__DIR__.'/files/guitars.csv', $importer->getCsvFile()); 37 | $this->assertEquals(__DIR__.'/files/guitars.csv', $importer->supAsapUltraDigitalCoruscant('csvFile')); 38 | $this->assertEquals('RANCH DUBOIS-8', $importer->inputEncoding); 39 | $this->assertEquals('RANCH DUBOIS-8', $importer->getInputEncoding()); 40 | $this->assertEquals('RANCH DUBOIS-8', $importer->dghstr('inputEncoding')); 41 | $this->assertEquals('Bird up-23', $importer->outputEncoding); 42 | $this->assertEquals('Bird up-23', $importer->getOutputEncoding()); 43 | $this->assertEquals('Bird up-23', $importer->jeMappelleRoman('outputEncoding')); 44 | $this->assertEquals('newline', $importer->newline); 45 | $this->assertEquals('newline', $importer->getNewline()); 46 | $this->assertEquals('newline', $importer->GimmeFuelGimmeFireGimmeThatWhichIDesireExlamationMark('newline')); 47 | } 48 | 49 | /** @test */ 50 | public function it_can_transform_data_according_to_mappings() 51 | { 52 | $item = [ 53 | 'title' => 'title', 54 | 'some_field_1' => 'some_field_1', 55 | 'weird_one' => 'weird' 56 | ]; 57 | 58 | $extracted = (new CsvImporter())->extractDefinedFields($item); 59 | 60 | $this->assertEquals('title', $extracted['title']); 61 | $this->assertEquals('some_field_1', $extracted['some_field_1']); 62 | $this->assertFalse(isset($extracted['weird_one'])); 63 | 64 | //////////////////////////////////////////////////////////////// 65 | 66 | $this->assertNull((new CsvImporter())->toCsvHeaders($item)); 67 | 68 | $attachedToHeaders = (new CsvImporter())->setCsvFile(__DIR__.'/files/guitars.csv')->toCsvHeaders($item); 69 | 70 | $this->assertEquals('title', $attachedToHeaders['title']); 71 | $this->assertEquals(null, $attachedToHeaders['serial_number']); 72 | $this->assertEquals(null, $attachedToHeaders['company']); 73 | $this->assertFalse(isset($attachedToHeaders['some_field_1'])); 74 | $this->assertFalse(isset($attachedToHeaders['weird_one'])); 75 | } 76 | } -------------------------------------------------------------------------------- /tests/CsvImporters/AsyncCsvImporter.php: -------------------------------------------------------------------------------- 1 | [ 56 | 'serial_number' => ['required', 'validation' => ['numeric'], 'cast' => 'string'], 57 | 'title' => ['validation' => ['required'], 'cast' => ['string', 'lowercase']], 58 | 'company' => ['validation' => ['string'], 'cast' => 'super_caster'] 59 | ], 60 | 'csv_files' => [ 61 | 'valid_entities' => '/valid_entities.csv', 62 | 'invalid_entities' => '/invalid_entities.csv', 63 | ] 64 | ]; 65 | } 66 | 67 | /** 68 | * @param $mode 69 | * @return $this 70 | */ 71 | public function setAsyncMode($mode) 72 | { 73 | $this->asyncMode = $mode; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Will be executed for a csv line if it passes validation 80 | * 81 | * @param $item 82 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 83 | * @return void 84 | */ 85 | protected function handle($item) 86 | { 87 | if ($this->asyncMode) { 88 | sleep(1); 89 | } 90 | 91 | $this->insertTo('valid_entities', $item); 92 | } 93 | 94 | /** 95 | * Will be executed if a csv line did not pass validation 96 | * 97 | * @param $item 98 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 99 | * @return void 100 | */ 101 | protected function invalid($item) 102 | { 103 | $this->insertTo('invalid_entities', $item); 104 | } 105 | 106 | /** 107 | * @return void 108 | */ 109 | protected function before() 110 | { 111 | if ($this->asyncMode) { 112 | Cache::forever(AsyncCsvImporter::$cacheStartedKey, true); 113 | 114 | sleep(5); 115 | 116 | Cache::forever(AsyncCsvImporter::$cacheInitFinishedKey, true); 117 | } 118 | } 119 | 120 | /** 121 | * @return void 122 | */ 123 | protected function after() 124 | { 125 | if ($this->asyncMode) { 126 | Cache::forever(AsyncCsvImporter::$cacheFinalStageStartedKey, true); 127 | 128 | $this->setFinalDetails('Buzz me Mulatto'); 129 | 130 | sleep(5); 131 | 132 | Cache::forever(AsyncCsvImporter::$cacheCustomProgressBarKey, true); 133 | 134 | $this->initProgressBar('Custom progress bar', 5); 135 | 136 | for ($i = 0; $i < 5; $i++) { 137 | sleep(1); 138 | $this->incrementProgress(); 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * @return void 145 | */ 146 | protected function onCancel() 147 | { 148 | if ($this->asyncMode) { 149 | Cache::forever(AsyncCsvImporter::$cacheOnCancelKey, 'Hey there!'); 150 | } 151 | } 152 | 153 | /** 154 | * @return string 155 | */ 156 | public function progressBarDetails() 157 | { 158 | return 'Sup Mello?'; 159 | } 160 | 161 | ////////////////////////////////////////////////////////////////////////////////////////////// 162 | 163 | /** 164 | * @return void 165 | */ 166 | public static function flushAsyncInfo() 167 | { 168 | Cache::forget(AsyncCsvImporter::$cacheInfoKey); 169 | Cache::forget(AsyncCsvImporter::$cacheStartedKey); 170 | Cache::forget(AsyncCsvImporter::$cacheInitFinishedKey); 171 | Cache::forget(AsyncCsvImporter::$cacheFinalStageStartedKey); 172 | Cache::forget(AsyncCsvImporter::$cacheOnCancelKey); 173 | Cache::forget(AsyncCsvImporter::$cacheCustomProgressBarKey); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/CustomFiltersTest.php: -------------------------------------------------------------------------------- 1 | setCsvFile(__DIR__.'/files/guitars.csv')->run(); 40 | 41 | $this->expectException(CsvImporterException::class); 42 | $this->expectExceptionMessage( 43 | '{"quantity":1,"Headers error:":["The csv must contain either `name` field either `first_name` and `last_name` fields"]}' 44 | ); 45 | 46 | (new CsvImporter())->setCsvFile(__DIR__.'/files/guitars.csv')->run(); 47 | } 48 | 49 | /** @test */ 50 | public function it_can_add_required_headers_filters_from_closure() 51 | { 52 | CsvImporter::addHeadersFilter(function ($item) { 53 | if (isset($item['name']) || isset($item['some_another'])) { 54 | return true; 55 | } 56 | 57 | return false; 58 | }); 59 | 60 | $this->expectException(CsvImporterException::class); 61 | $this->expectExceptionMessage('{"quantity":1,"Headers error:":["Headers error occurred"]}'); 62 | 63 | (new CsvImporter())->setCsvFile(__DIR__.'/files/guitars.csv')->run(); 64 | } 65 | 66 | /** @test */ 67 | public function getters_and_setters_for_required_filters() 68 | { 69 | CsvImporter::addHeadersFilters(function ($item) {}, new MyHeadersFilter()); 70 | 71 | $this->assertTrue(CsvImporter::headersFilterExists('filter')); 72 | $this->assertTrue(CsvImporter::headersFilterExists('MyHeadersFilter')); 73 | $this->assertTrue(CsvImporter::getHeadersFilter('filter') instanceof ClosureHeadersFilter); 74 | $this->assertTrue(CsvImporter::getHeadersFilter('MyHeadersFilter') instanceof MyHeadersFilter); 75 | 76 | $filters = CsvImporter::getHeadersFilters(); 77 | 78 | $this->assertTrue($filters['filter'] instanceof ClosureHeadersFilter); 79 | $this->assertTrue($filters['MyHeadersFilter'] instanceof MyHeadersFilter); 80 | 81 | CsvImporter::flushHeadersFilters(); 82 | 83 | $this->assertFalse(CsvImporter::headersFilterExists('filter')); 84 | $this->assertFalse(CsvImporter::headersFilterExists('MyHeadersFilter')); 85 | 86 | CsvImporter::addHeadersFilters($filters); 87 | 88 | $this->assertTrue(CsvImporter::headersFilterExists('filter')); 89 | $this->assertTrue(CsvImporter::headersFilterExists('MyHeadersFilter')); 90 | $this->assertTrue(CsvImporter::getHeadersFilter('filter') instanceof ClosureHeadersFilter); 91 | $this->assertTrue(CsvImporter::getHeadersFilter('MyHeadersFilter') instanceof MyHeadersFilter); 92 | } 93 | 94 | /** @test */ 95 | public function it_can_add_validation_filters() 96 | { 97 | CustomValidationImporter::addValidationFilter(new MyValidationFilter()); 98 | 99 | $importer = (new CustomValidationImporter())->setCsvFile(__DIR__.'/files/bad_word.csv'); 100 | 101 | $importer->run(); 102 | 103 | $entities = $this->getResultCsv($importer->finish()['files']['invalid_entities']); 104 | 105 | $this->assertEquals('bad_word', $entities[1][2]); 106 | } 107 | 108 | /** @test */ 109 | public function it_can_add_validation_filters_from_closure() 110 | { 111 | CustomValidationImporter::addValidationFilter(function ($item) { 112 | if (strpos($item['title'], 'bad_word') !== false) { 113 | return false; 114 | } 115 | 116 | return true; 117 | }, 'bad_word_validation'); 118 | 119 | $importer = (new CustomValidationImporter())->setCsvFile(__DIR__.'/files/bad_word.csv'); 120 | 121 | $importer->run(); 122 | 123 | $entities = $this->getResultCsv($importer->finish()['files']['invalid_entities']); 124 | 125 | $this->assertEquals('bad_word', $entities[1][2]); 126 | } 127 | 128 | /** @test */ 129 | public function if_validation_filter_does_not_exists() 130 | { 131 | $this->expectException(ImportValidationException::class); 132 | $this->expectExceptionMessage("Method [validateBadWordValidation] does not exist."); 133 | 134 | $importer = (new CustomValidationImporter())->setCsvFile(__DIR__.'/files/bad_word.csv'); 135 | 136 | $importer->run(); 137 | } 138 | 139 | /** @test */ 140 | public function getters_and_setters_for_validation_filters() 141 | { 142 | CsvImporter::addValidationFilters(function ($item) {}, new MyValidationFilter()); 143 | 144 | $this->assertTrue(CsvImporter::validationFilterExists('filter')); 145 | $this->assertTrue(CsvImporter::validationFilterExists('bad_word_validation')); 146 | $this->assertTrue(CsvImporter::getValidationFilter('filter') instanceof ClosureValidationFilter); 147 | $this->assertTrue(CsvImporter::getValidationFilter('bad_word_validation') instanceof MyValidationFilter); 148 | 149 | $filters = CsvImporter::getValidationFilters(); 150 | 151 | $this->assertTrue($filters['filter'] instanceof ClosureValidationFilter); 152 | $this->assertTrue($filters['bad_word_validation'] instanceof MyValidationFilter); 153 | 154 | CsvImporter::flushValidationFilters(); 155 | 156 | $this->assertFalse(CsvImporter::validationFilterExists('filter')); 157 | $this->assertFalse(CsvImporter::validationFilterExists('bad_word_validation')); 158 | 159 | CsvImporter::addValidationFilters($filters); 160 | 161 | $this->assertTrue(CsvImporter::validationFilterExists('filter')); 162 | $this->assertTrue(CsvImporter::validationFilterExists('bad_word_validation')); 163 | $this->assertTrue(CsvImporter::getValidationFilter('filter') instanceof ClosureValidationFilter); 164 | $this->assertTrue(CsvImporter::getValidationFilter('bad_word_validation') instanceof MyValidationFilter); 165 | } 166 | 167 | /** @test */ 168 | public function it_can_add_cast_filters() 169 | { 170 | CsvImporter::addCastFilter(new MyCastFilter()); 171 | 172 | $invalidEntities = $this->getResultCsv($this->importCsv()['files']['invalid_entities']); 173 | $validEntities = $this->getResultCsv($this->importCsv()['files']['valid_entities']); 174 | 175 | $this->assertTrue(strcmp('misha mansoor juggernaut ht6', $invalidEntities[1][2]) === 0); 176 | $this->assertTrue(strcmp('tam100 tosin abasi signature', $validEntities[3][2]) === 0); 177 | } 178 | 179 | /** @test */ 180 | public function it_can_add_cast_filters_from_closure() 181 | { 182 | CsvImporter::addCastFilter(function ($value) { 183 | return strtolower($value); 184 | }, 'lowercase'); 185 | 186 | $invalidEntities = $this->getResultCsv($this->importCsv()['files']['invalid_entities']); 187 | $validEntities = $this->getResultCsv($this->importCsv()['files']['valid_entities']); 188 | 189 | $this->assertTrue(strcmp('misha mansoor juggernaut ht6', $invalidEntities[1][2]) === 0); 190 | $this->assertTrue(strcmp('tam100 tosin abasi signature', $validEntities[3][2]) === 0); 191 | } 192 | 193 | /** @test */ 194 | public function getters_and_setters_for_cast_filters() 195 | { 196 | CsvImporter::addCastFilters(function ($item) {}, new MyCastFilter()); 197 | 198 | $this->assertTrue(CsvImporter::castFilterExists('filter')); 199 | $this->assertTrue(CsvImporter::castFilterExists('lowercase')); 200 | $this->assertTrue(CsvImporter::getCastFilter('filter') instanceof ClosureCastFilter); 201 | $this->assertTrue(CsvImporter::getCastFilter('lowercase') instanceof MyCastFilter); 202 | 203 | $filters = CsvImporter::getCastFilters(); 204 | 205 | $this->assertTrue($filters['filter'] instanceof ClosureCastFilter); 206 | $this->assertTrue($filters['lowercase'] instanceof MyCastFilter); 207 | 208 | CsvImporter::flushCastFilters(); 209 | 210 | $this->assertFalse(CsvImporter::castFilterExists('filter')); 211 | $this->assertFalse(CsvImporter::castFilterExists('lowercase')); 212 | 213 | CsvImporter::addCastFilters($filters); 214 | 215 | $this->assertTrue(CsvImporter::castFilterExists('filter')); 216 | $this->assertTrue(CsvImporter::castFilterExists('lowercase')); 217 | $this->assertTrue(CsvImporter::getCastFilter('filter') instanceof ClosureCastFilter); 218 | $this->assertTrue(CsvImporter::getCastFilter('lowercase') instanceof MyCastFilter); 219 | } 220 | } -------------------------------------------------------------------------------- /tests/MutexFunctionality.php: -------------------------------------------------------------------------------- 1 | importer = (new AsyncCsvImporter())->setCsvFile(__DIR__.'/files/guitars.csv'); 22 | 23 | $this->importer->clearSession(); 24 | $this->importer->flushAsyncInfo(); 25 | 26 | if ($this->runTests()) { 27 | dispatch(new TestImportJob($this->cacheDriver)); 28 | 29 | /* 30 | * We need to wait till queue start import in the separated system process 31 | */ 32 | $this->waitUntilStart(); 33 | } 34 | } 35 | 36 | protected function runTests() 37 | { 38 | return function_exists('dispatch'); 39 | } 40 | 41 | public function tearDown() 42 | { 43 | if ($this->runTests()) { 44 | /* 45 | * Make sure the import is finished before next test 46 | */ 47 | $this->checkImportFinalResponse(); 48 | } 49 | 50 | parent::tearDown(); 51 | } 52 | 53 | /** @test */ 54 | public function it_can_import_and_lock_csv() 55 | { 56 | if (!$this->runTests()) { 57 | return; 58 | } 59 | 60 | $initProgress = $this->importer->getProgress(); 61 | 62 | $this->waitUntilEndOfInitialization(); 63 | 64 | $progress = $this->importer->getProgress(); 65 | 66 | /* 67 | * Instead of execution we will get progress information from import which is queued 68 | * and running in the another system process 69 | */ 70 | $preventedRunResponse = $this->importer->run(); 71 | 72 | $this->waitUntilFinalStage(); 73 | 74 | $finalStageProgress = $this->importer->getProgress(); 75 | 76 | $this->waitUntilCustomProgressBar(); 77 | 78 | $customProgress = $this->importer->getProgress(); 79 | 80 | $finishedMessage = $this->checkImportFinalResponse(); 81 | 82 | $finalInformation = $this->importer->finish(); 83 | 84 | $this->assertEquals('Initialization', $initProgress['data']['message']); 85 | $this->assertFalse($initProgress['meta']['finished']); 86 | $this->assertTrue($initProgress['meta']['init']); 87 | $this->assertTrue($initProgress['meta']['running']); 88 | 89 | $this->assertEquals('Import process is running', $progress['data']['message']); 90 | $this->assertEquals('Sup Mello?', $progress['data']['details']); 91 | $this->assertEquals('integer', gettype($progress['meta']['processed'])); 92 | $this->assertEquals('integer', gettype($progress['meta']['remains'])); 93 | $this->assertEquals('double', gettype($progress['meta']['percentage'])); 94 | $this->assertFalse($progress['meta']['finished']); 95 | $this->assertFalse($progress['meta']['init']); 96 | $this->assertTrue($progress['meta']['running']); 97 | 98 | $this->assertEquals('Import process is running', $preventedRunResponse['data']['message']); 99 | $this->assertEquals('integer', gettype($preventedRunResponse['meta']['processed'])); 100 | $this->assertEquals('integer', gettype($preventedRunResponse['meta']['remains'])); 101 | $this->assertEquals('double', gettype($preventedRunResponse['meta']['percentage'])); 102 | $this->assertFalse($preventedRunResponse['meta']['finished']); 103 | $this->assertFalse($preventedRunResponse['meta']['init']); 104 | $this->assertTrue($preventedRunResponse['meta']['running']); 105 | 106 | $this->assertEquals("Final stage", $finalStageProgress['data']['message']); 107 | $this->assertFalse($finalStageProgress['meta']['finished']); 108 | $this->assertFalse($finalStageProgress['meta']['init']); 109 | $this->assertTrue($finalStageProgress['meta']['running']); 110 | 111 | $this->assertEquals('Custom progress bar', $customProgress['data']['message']); 112 | $this->assertEquals('Sup Mello?', $customProgress['data']['details']); 113 | $this->assertEquals('integer', gettype($customProgress['meta']['processed'])); 114 | $this->assertEquals('integer', gettype($customProgress['meta']['remains'])); 115 | $this->assertEquals('double', gettype($customProgress['meta']['percentage'])); 116 | $this->assertFalse($customProgress['meta']['finished']); 117 | $this->assertFalse($customProgress['meta']['init']); 118 | $this->assertTrue($customProgress['meta']['running']); 119 | 120 | $this->assertEquals( 121 | "Almost done, please click to the `finish` button to proceed", 122 | $finishedMessage['data']['message'] 123 | ); 124 | $this->assertTrue($finishedMessage['meta']['finished']); 125 | $this->assertFalse($finishedMessage['meta']['init']); 126 | $this->assertFalse($finishedMessage['meta']['running']); 127 | 128 | $this->assertEquals("The import process successfully finished!", $finalInformation['data']['message']); 129 | $this->assertEquals("Buzz me Mulatto", $finalInformation['data']['details']); 130 | $this->assertTrue($finalInformation['meta']['finished']); 131 | $this->assertFalse($finalInformation['meta']['init']); 132 | $this->assertFalse($finalInformation['meta']['running']); 133 | $this->assertTrue(strpos($finalInformation['files']['valid_entities'], "valid_entities") !== false); 134 | $this->assertTrue(strpos($finalInformation['files']['invalid_entities'], "invalid_entities") !== false); 135 | } 136 | 137 | /** @test */ 138 | public function it_can_cancel_import_process() 139 | { 140 | if (!$this->runTests()) { 141 | return; 142 | } 143 | 144 | $this->importer->cancel(); 145 | 146 | $finishedMessage = $this->checkImportFinalResponse(); 147 | 148 | $this->assertEquals("Importing had canceled", json_decode($finishedMessage, true)['message']); 149 | $this->assertEquals("Hey there!", Cache::get(AsyncCsvImporter::$cacheOnCancelKey)); 150 | } 151 | 152 | /** @test */ 153 | public function it_can_concatenate_import_lock_key() 154 | { 155 | if (!$this->runTests()) { 156 | return; 157 | } 158 | 159 | $finishedMessage = $this->importer->concatMutexKey('unrelated_guitars')->run(); 160 | $finalInformation = $this->importer->finish(); 161 | 162 | $this->assertEquals( 163 | "Almost done, please click to the `finish` button to proceed", 164 | $finishedMessage['data']['message'] 165 | ); 166 | $this->assertTrue($finishedMessage['meta']['finished']); 167 | $this->assertFalse($finishedMessage['meta']['init']); 168 | $this->assertFalse($finishedMessage['meta']['running']); 169 | 170 | $this->assertEquals("The import process successfully finished!", $finalInformation['data']['message']); 171 | $this->assertTrue($finalInformation['meta']['finished']); 172 | $this->assertFalse($finalInformation['meta']['init']); 173 | $this->assertFalse($finalInformation['meta']['running']); 174 | $this->assertTrue(strpos($finalInformation['files']['valid_entities'], "valid_entities") !== false); 175 | $this->assertTrue(strpos($finalInformation['files']['invalid_entities'], "invalid_entities") !== false); 176 | } 177 | 178 | ///////////////////////////////////////////////////////////////////////////////////////////////// 179 | 180 | /** 181 | * @param int $counter 182 | * @return mixed 183 | * @throws \Exception 184 | */ 185 | protected function checkImportFinalResponse($counter = 0) 186 | { 187 | if ($info = Cache::get(AsyncCsvImporter::$cacheInfoKey)) { 188 | usleep(200); // reduce possibility of race condition 189 | return $info; 190 | } 191 | 192 | $this->fuse($counter, AsyncCsvImporter::$cacheInfoKey); 193 | 194 | return $this->checkImportFinalResponse(++$counter); 195 | } 196 | 197 | /** 198 | * @param int $counter 199 | * @return bool|mixed 200 | * @throws \Exception 201 | */ 202 | protected function waitUntilStart($counter = 0) 203 | { 204 | if (Cache::get(AsyncCsvImporter::$cacheStartedKey)) { 205 | usleep(200); // reduce possibility of race condition 206 | return true; 207 | } 208 | 209 | $this->fuse($counter, AsyncCsvImporter::$cacheStartedKey); 210 | 211 | return $this->waitUntilStart(++$counter); 212 | } 213 | 214 | /** 215 | * @param int $counter 216 | * @return bool|mixed 217 | * @throws \Exception 218 | */ 219 | protected function waitUntilCustomProgressBar($counter = 0) 220 | { 221 | if (Cache::get(AsyncCsvImporter::$cacheCustomProgressBarKey)) { 222 | usleep(200); // reduce possibility of race condition 223 | return true; 224 | } 225 | 226 | $this->fuse($counter, AsyncCsvImporter::$cacheCustomProgressBarKey); 227 | 228 | return $this->waitUntilCustomProgressBar(++$counter); 229 | } 230 | 231 | /** 232 | * @param int $counter 233 | * @return bool|mixed 234 | * @throws \Exception 235 | */ 236 | protected function waitUntilEndOfInitialization($counter = 0) 237 | { 238 | if (Cache::get(AsyncCsvImporter::$cacheInitFinishedKey)) { 239 | usleep(200); // reduce possibility of race condition 240 | return true; 241 | } 242 | 243 | $this->fuse($counter, AsyncCsvImporter::$cacheInitFinishedKey); 244 | 245 | return $this->waitUntilEndOfInitialization(++$counter); 246 | } 247 | 248 | /** 249 | * @param int $counter 250 | * @return bool|mixed 251 | * @throws \Exception 252 | */ 253 | protected function waitUntilFinalStage($counter = 0) 254 | { 255 | if (Cache::get(AsyncCsvImporter::$cacheFinalStageStartedKey)) { 256 | usleep(200); // reduce possibility of race condition 257 | return true; 258 | } 259 | 260 | $this->fuse($counter, AsyncCsvImporter::$cacheFinalStageStartedKey); 261 | 262 | return $this->waitUntilFinalStage(++$counter); 263 | } 264 | 265 | /** 266 | * @param $counter 267 | * @param $key 268 | * @throws \Exception 269 | */ 270 | protected function fuse($counter, $key) 271 | { 272 | if ($counter > 25) { 273 | throw new \PHPUnit_Framework_ExpectationFailedException( 274 | "Timeout error. Check your queue. Key: '" . $key . "'. Cache driver: '" . $this->cacheDriver . "'." 275 | ); 276 | } 277 | 278 | sleep(1); 279 | } 280 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-csv-importer 2 | Flexible and reliable way to import, parse, validate and transform your csv files with laravel 3 | 4 | ## Installation ## 5 | 6 | ```php 7 | composer require rgilyov/laravel-csv-importer 8 | ``` 9 | 10 | Register \RGilyov\CsvImporter\CsvImporterServiceProvider inside `config/app.php` 11 | ```php 12 | 'providers' => [ 13 | //... 14 | \RGilyov\CsvImporter\CsvImporterServiceProvider::class, 15 | ]; 16 | ``` 17 | 18 | After installation you may publish default configuration file 19 | ``` 20 | php artisan vendor:publish --tag=config 21 | ``` 22 | 23 | Works with laravel 5 and above, hhvm are supported. 24 | 25 | ## Requirements ## 26 | 27 | Each created importer will have built in `mutex` functionality to make imports secure and avoid possible data 28 | incompatibilities, it's important especially in cases when > 100k lines csv files will be imported, due to that a 29 | laravel application should have `file`, `redis` or `memcached` cache driver set in the `.env` file 30 | 31 | ## Basic usage ## 32 | 33 | To create new csv importer, a class should extends `\RGilyov\CsvImporter\BaseCsvImporter` abstract class 34 | or the `php artisan make:csv-importer MyImporter` console command can be used, after execution new file with name 35 | `MyImporter.php` and basic importer set up will be placed inside `app/CsvImporters/` directory. 36 | 37 | ```php 38 | [ 56 | 'serial_number' => ['required', 'validation' => ['numeric'], 'cast' => 'string'], 57 | 'title' => ['validation' => ['required'], 'cast' => ['string']], 58 | 'company' => ['validation' => ['string']] 59 | ], 60 | 'csv_files' => [ 61 | 'valid_entities' => '/valid_entities.csv', 62 | 'invalid_entities' => '/invalid_entities.csv', 63 | ] 64 | ]; 65 | } 66 | 67 | /** 68 | * Will be executed for a csv line if it passed validation 69 | * 70 | * @param $item 71 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 72 | * @return void 73 | */ 74 | public function handle($item) 75 | { 76 | $this->insertTo('valid_entities', $item); 77 | } 78 | 79 | /** 80 | * Will be executed if a csv line did not pass validation 81 | * 82 | * @param $item 83 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 84 | * @return void 85 | */ 86 | public function invalid($item) 87 | { 88 | $this->insertTo('invalid_entities', $item); 89 | } 90 | } 91 | ``` 92 | 93 | There are 3 main methods: 94 | 95 | - `csvConfigurations` which returns configurations for the given type of csv, configurations has 3 parts: 96 | 97 | * `'mappings'`: you may specify fields(headers) which you expect the given csv has and attach 98 | rules to each field(header), there are 3 types of rules(filters) which you can specify: 99 | 100 | * To make a field(header) mandatory for the import you need to set `required` parameter for the field(header) 101 | `'name' => ['required']`, so if the given csv file won't have the field(header) `name` an error will be 102 | thrown. 103 | 104 | * You may set validation to each field(header), the importer uses laravel validation, so you can use any rules 105 | from there https://laravel.com/docs/5.4/validation#available-validation-rules to check csv values 106 | `'email' => ['required', validation => ['email']]`, so if a value won't be a valid email, the csv line 107 | which contains the email will be put inside `invalid($item)` method otherwise inside `handle($item)`. 108 | 109 | * You can cast csv values in any native php type and format date which csv contains 110 | `'name' => ['required', 'cast' => 'string']` or `'birth_date' => ['cast' => ['string', 'date']]` 111 | cast will work before validation. 112 | 113 | * `'csv_files'`: files specified inside the key will be created, you can write csv lines inside each file, 114 | with `$this->insertTo('csv_file_name', $item);` method, for example you can separate invalid csv lines from valid, 115 | it uses laravel filesystem `\Storage` support class https://laravel.com/docs/5.4/filesystem. 116 | 117 | * `'config'`: you may overwrite global `config/csv-importer.php` configurations hear for the given csv importer. 118 | 119 | - `handle`: will be executed for csv lines which passed validation. 120 | - `invalid`: will be executed for csv lines which didn't pass validation. 121 | 122 | Let's finally import a csv: 123 | 124 | ```php 125 | $importer = (new \App\CsvImporters\MyImporter())->setCsvFile('my_huge_csv_with_1000k_lines.csv'); 126 | $importer->run(); 127 | 128 | // progress information will be here, due to the import process already started above 129 | $result = $importer->run(); 130 | ``` 131 | 132 | After the import had started you won't be able to start another import until the first one finished. 133 | 134 | During the import though you may want to know progress of the running process: 135 | 136 | ```php 137 | $progress = $importer->getProgress(); 138 | 139 | /* 140 | [ 141 | 'data' => ["message" => 'The import process is running'], 142 | 'meta' => [ 143 | 'processed' => 250000, 144 | 'remains' => 750000, 145 | 'percentage' => 25, 146 | 'finished' => false, 147 | 'init' => false, 148 | 'running' => true 149 | ] 150 | ] 151 | */ 152 | ``` 153 | 154 | At the end of the import you will have key `finished => true` inside `meta` data. 155 | So you will need to finish your csv import: 156 | 157 | ```php 158 | $finishDetails = $importer->finish(); 159 | 160 | /* 161 | [ 162 | [ 163 | 'data' => [ 164 | "message" => 'The import process successfully finished.' 165 | ], 166 | 'meta' => ["finished" => true, 'init' => false, 'running' => false], 167 | 'csv_files' => [ 168 | 'valid_entities.csv', 169 | 'invalid_entities.csv' 170 | ] 171 | ] 172 | ] 173 | */ 174 | ``` 175 | 176 | If something went wrong, you can cancel the current import process: 177 | 178 | ```php 179 | $importer->cancel(); 180 | ``` 181 | 182 | ## Importer customization ## 183 | 184 | Besides methods above the importer also has a list of methods which can help you to easily 185 | expand your functionality for some particular cases: 186 | 187 | - `before` - will be executed before start of an import process 188 | - `after` - will be executed after an import process finished 189 | - `onCancel` - will be executed before abortion of an import process 190 | - `initProgressBar` - will initialize new progress bar 191 | - `progressBarDetails` - additional information for progress bar 192 | - `setFinalDetails` - set additional information after `finish()` of an import process 193 | - `setError` - add an error to the list of errors which will be thrown (if exists) 194 | 195 | ```php 196 | checkSomething()) { 215 | $this->setError('Oops', 'something went wrong.'); 216 | }; 217 | } 218 | 219 | /** 220 | * Adjust additional information to progress bar during import process 221 | * 222 | * @return null|string|array 223 | */ 224 | public function progressBarDetails() 225 | { 226 | return "I'm a csv importer and I'm running :)"; 227 | } 228 | 229 | /** 230 | * Will be executed after importing 231 | * 232 | * @return void 233 | */ 234 | protected function after() 235 | { 236 | // do something after the import finished 237 | $entities = \App\CsvEntity::all(); // just a demo, in real life you don't want to do it ;) 238 | $this->initProgressBar('Something running.', $entities->count()); 239 | 240 | $entities->each(function ($entity) { 241 | // do something 242 | $this->incrementProgress(); 243 | }); 244 | 245 | $this->setFinalDetails('Final details.'); 246 | } 247 | 248 | /** 249 | * Will be executed during the import process canceling 250 | */ 251 | protected function onCancel() 252 | { 253 | \DB::rollBack(); 254 | } 255 | } 256 | ``` 257 | 258 | ## Basic csv aggregations ## 259 | 260 | If a csv file is set to an import class you can `count` it, get `distinct` values from it or loop through the csv: 261 | 262 | ```php 263 | $importer = (new \App\CsvImporters\MyImporter())->setCsvFile('my_huge_csv_with_1000k_lines.csv'); 264 | 265 | $quantity = $importer->countCsv(); // returns amount of csv lines without headers 266 | $distinctNames = $importer->distinct('name'); // returns array with distinct names 267 | 268 | $importer->each(function ($item) { // encoded and casted csv line 269 | // do something 270 | }); 271 | ``` 272 | 273 | All methods above returns `false` if a csv file wasn't set. 274 | 275 | ## Configurations ## 276 | 277 | There are 3 layers of configurations: 278 | 279 | - 1) global `config/csv-importer.php` configuration file, which has default parameters applied for all csv importers 280 | - 2) local configurations, which `csvConfigurations()` method returns, overwrites global configurations 281 | - 3) manual configuration customization with setters, overwrites `global` and `local` configurations 282 | 283 | 1) Global configurations: 284 | 285 | ```php 286 | /* 287 | |-------------------------------------------------------------------------- 288 | | Main csv import configurations 289 | |-------------------------------------------------------------------------- 290 | | 291 | | `cache_driver` - keeps all progress and final information, it also allows 292 | | the mutex functionality to work, there are only 3 cache drivers supported: 293 | | redis, file and memcached 294 | | 295 | | `mutex_lock_time` - how long script will be executed and how long 296 | | the import process will be locked, another words if we will import 297 | | list of electric guitars we won't be able to run another import of electric 298 | | guitars at the same time, to avoid duplicates and different sorts of 299 | | incompatibilities. The value set in minutes. 300 | | 301 | | `memory_limit` - if you want store all csv values in memory or something like that, 302 | | you may increase amount of memory for the script 303 | | 304 | | `encoding` - which encoding we have, UTF-8 by default 305 | | 306 | */ 307 | 'cache_driver' => env('CACHE_DRIVER', 'file'), 308 | 309 | 'mutex_lock_time' => 300, 310 | 311 | 'memory_limit' => 128, 312 | 313 | /* 314 | * An import class's short name (without namespace) by default 315 | */ 316 | 'mutex_lock_key' => null, 317 | 318 | /* 319 | * Encoding of given csv file 320 | */ 321 | 'input_encoding' => 'UTF-8', 322 | 323 | /* 324 | * Encoding of processed csv values 325 | */ 326 | 'output_encoding' => 'UTF-8', 327 | 328 | /* 329 | * Specify which date format the given csv file has 330 | * to use `date` ('Y-m-d') and `datetime` ('Y-m-d H:i:s') casters, 331 | * if the parameter will be set to `null` `date` caster will replace 332 | * `/` and `\` and `|` and `.` and `,` on `-` and will assume that 333 | * the given csv file has `Y-m-d` or `d-m-Y` date format 334 | */ 335 | 'csv_date_format' => null, 336 | 337 | 'delimiter' => ',', 338 | 339 | 'enclosure' => '"', 340 | 341 | /* 342 | * Warning: The library depends on PHP SplFileObject class. 343 | * Since this class exhibits a reported bug (https://bugs.php.net/bug.php?id=55413), 344 | * Data using the escape character are correctly 345 | * escaped but the escape character is not removed from the CSV content. 346 | */ 347 | 'escape' => '\\', 348 | 349 | 'newline' => "\n", 350 | 351 | /* 352 | |-------------------------------------------------------------------------- 353 | | Progress bar messages 354 | |-------------------------------------------------------------------------- 355 | */ 356 | 357 | 'does_not_running' => 'Import process does not run', 358 | 'initialization' => 'Initialization', 359 | 'progress' => 'Import process is running', 360 | 'final_stage' => 'Final stage', 361 | 'finished' => 'Almost done, please click to the `finish` button to proceed', 362 | 'final' => 'The import process successfully finished!' 363 | ``` 364 | 365 | 2) Local configurations: 366 | 367 | ```php 368 | [//...], 386 | 'csv_files' => [//...], 387 | 'config' => [ 388 | 'mutex_lock_time' => 500, 389 | 'memory_limit' => 256, 390 | 'mutex_lock_key' => 'my-key', 391 | 'input_encoding' => 'cp1252', 392 | 'output_encoding' => 'UTF-8', 393 | 'csv_date_format' => 'm/d/Y', 394 | 'delimiter' => ';', 395 | 'enclosure' => '\'', 396 | 'escape' => '\\', 397 | 'newline' => "\n", 398 | 399 | /* 400 | |-------------------------------------------------------------------------- 401 | | Progress bar messages 402 | |-------------------------------------------------------------------------- 403 | */ 404 | 405 | 'does_not_running' => 'Something does not run', 406 | 'initialization' => 'Init', 407 | 'progress' => 'Something running', 408 | 'final_stage' => 'After the import had finished', 409 | 'finished' => 'Please click to the `finish` button to proceed', 410 | 'final' => 'Something successfully finished!' 411 | ] 412 | ]; 413 | } 414 | } 415 | ``` 416 | 417 | 3) Configurations with setters: 418 | 419 | ```php 420 | (new \App\CsvImporters\MyImporter()) 421 | ->setCsvFile('my_huge_csv_with_1000k_lines.csv') 422 | ->setCsvDateFormat('y-m-d') 423 | ->setDelimiter('d') 424 | ->setEnclosure('e') 425 | ->setEscape("x") 426 | ->setInputEncoding('cp1252') 427 | ->setOutputEncoding('UTF-8') 428 | ->setNewline('newline') 429 | ->run(); 430 | ``` 431 | 432 | ## From csv headers to defined mappings and reverse array transformation ## 433 | 434 | Sounds awful, better just show how it works: 435 | 436 | Suppose we have a csv file with this structure: 437 | 438 | ``` 439 | name,some_weird_header 440 | John,some_weird_data 441 | ``` 442 | 443 | And we make an import class and define `mappings`, in this case we interested only in `name` field(header): 444 | 445 | ```php 446 | class GuitarsCsvImporter extends BaseCsvImporter 447 | { 448 | /** 449 | * Specify mappings and rules for the csv importer, you also may create csv files to write csv entities 450 | * and overwrite global configurations 451 | * 452 | * @return array 453 | */ 454 | public function csvConfigurations() 455 | { 456 | return [ 457 | 'mappings' => [ 458 | 'name' => [] // <- defined mappings, we only need data from this column 459 | ] 460 | ]; 461 | } 462 | 463 | /** 464 | * Will be executed for a csv line if it passed validation 465 | * 466 | * @param $item 467 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 468 | * @return void 469 | */ 470 | public function handle($item) 471 | { 472 | /* 473 | $item contains ['name' => 'John', 'some_weird_header' => 'some_weird_data'] 474 | so the $item will have all columns inside, so we need extract only columns we need, which was defined 475 | inside csv configurations mappings array 476 | */ 477 | 478 | $dataOnlyFromDefinedFields = $this->extractDefinedFields($item); // will return ['name' => 'John'] 479 | 480 | /* 481 | Assume we need to do some manipulations with the $item 482 | array and then write it to the `valid_entities.csv` 483 | we need to make sure that data inside formatted array are 484 | match headers inside the csv, we can do it with this `toCsvHeaders($item)` method: 485 | */ 486 | 487 | // will return ['name' => 'John', 'some_weird_header' => null] 488 | $csvHeadersData = $this->toCsvHeaders($dataOnlyFromDefinedFields); 489 | 490 | $this->insertTo('valid_entities', $csvHeadersData); 491 | } 492 | } 493 | ``` 494 | 495 | ## Mutex key concatenation ## 496 | 497 | There are cases when you need to be able to run several similar import processes at the same time, for example you have 498 | `guitars` and `guitar_companies` tables in your db and two csv files `ltd_guitars.csv` and `black_machine_guitars.csv` 499 | and you use the same import class for both csvs, but since import process is locked you not able to import both at the 500 | same time, in this case use mutex key concatenation, to have different mutex key for each `guitar company`: 501 | 502 | ```php 503 | class GuitarsCsvImporter extends BaseCsvImporter 504 | { 505 | //.. 506 | 507 | protected $guitarCompany; 508 | 509 | public function setCompany(\App\GuitarCompany $guitarCompany) 510 | { 511 | $this->guitarCompany = $guitarCompany; 512 | $this->concatMutexKey($guitarCompany->id); 513 | 514 | return $this; 515 | } 516 | 517 | /** 518 | * Will be executed for a csv line if it passed validation 519 | * 520 | * @param $item 521 | * @throws \RGilyov\CsvImporter\Exceptions\CsvImporterException 522 | * @return void 523 | */ 524 | public function handle($item) 525 | { 526 | \App\Guitars::create( 527 | array_merge(['guitar_company_id' => $this->guitarCompany->id], $this->extractDefinedFields($item)) 528 | ); 529 | } 530 | } 531 | ``` 532 | 533 | Now you can run the importer for each company in the same time. But not for the same company. 534 | 535 | ## Custom filters ## 536 | 537 | As mentioned above in the `Basic usage` chapter, the csv importer has 3 types of filters, which you can specify for each 538 | csv field(header), but some times you need to do something more sophisticated, for example: 539 | 540 | - if a given csv has field(header) `A` `OR` field(header) `B` and if both are missing throw headers validation error, 541 | in this case parameter `required` will be insufficient for the task due to the csv import will check each field(header) 542 | which has the parameter which is `AND` logic, in this case you need to create `headers filter`. 543 | 544 | - Or for example you need to make white list check for all values in a particular field(header), which is new 545 | `validation rule(filter)`. 546 | 547 | - Or you may want to perform some advanced transformation for csv values, in this situation you will need to create 548 | `cast filter`. 549 | 550 | ## Custom headers filters ## 551 | 552 | To make custom headers filter you need to make a class which will extends `\RGilyov\CsvImporter\BaseHeadersFilter` 553 | or just run `php artisan make:csv-importer-headers-filter MyHeadersFilter` which will make `MyHeadersFilter.php` 554 | file with basic set up inside `app/CsvImporters/HeadersFilters/` folder: 555 | 556 | ```php 557 | get('without_filters')) { 630 | \App\CsvImporters\MyImporter::flushHeadersFilters(); 631 | } 632 | ``` 633 | 634 | ## Custom validation filters ## 635 | 636 | To make custom validation filter you need to make a class which will extends `\RGilyov\CsvImporter\BaseValidationFilter` 637 | or just run `php artisan make:csv-importer-validation-filter MyValidationFilter` which will make 638 | `MyValidationFilter.php` file with basic set up inside `app/CsvImporters/ValidationFilters/` folder: 639 | 640 | ```php 641 | ['validation' => 'bad_word_validation']` 672 | 673 | But there are cases when you'd like to make a global sort of speak validation filter, to be able validate whole csv 674 | entity array, for example a csv line is valid only if it has not empty user name or not empty user first name and last 675 | name, to get array with all csv columns instead of just a value you need to set `public $global = true`, 676 | of course no need to specify such validation filter inside an import class csv configurations 677 | 678 | ```php 679 | global set to `true` 702 | { 703 | if (empty($value['name']) || (empty($value['first_name']) && empty($value['last_name']))) { 704 | return false; 705 | } 706 | 707 | return true; 708 | } 709 | } 710 | ``` 711 | 712 | All filters manipulation is similar to what was described in `Custom headers filters` chapter: 713 | 714 | ```php 715 | \App\CsvImporters\MyImporter::addValidationFilter(new \App\CsvImporters\ValidationFilters\MyValidationFilter()); 716 | 717 | // closure validation filters are global 718 | \App\CsvImporters\MyImporter::addValidationFilter(function ($item) { 719 | if (!empty($csvHeaders['A']) || !empty($csvHeaders['B'])) { 720 | return true; 721 | } 722 | 723 | return false; 724 | }, 'not-empty'); 725 | 726 | // you may add multiple filters with one method 727 | $notEmpty = function ($item) { 728 | if (!empty($item['A']) || !empty($item['B'])) { 729 | return true; 730 | } 731 | 732 | return false; 733 | } 734 | 735 | $myValidationFilter = new \App\CsvImporters\ValidationFilters\MyValidationFilter(); 736 | $myGlobalValidationFilter = new \App\CsvImporters\ValidationFilters\MyGlobalValidationFilter(); 737 | 738 | \App\CsvImporters\MyImporter::addValidationFilters($notEmpty, $myValidationFilter, $myGlobalValidationFilter); 739 | 740 | ///////////////////////////////////////////////////////////////////////////// 741 | 742 | \App\CsvImporters\MyImporter::validationFilterExists('bad_word_validation'); // will return `true` 743 | \App\CsvImporters\MyImporter::getValidationFilter('global_validation'); // will return instance of MyGlobalValidationFilter class 744 | \App\CsvImporters\MyImporter::getValidationFilters(); // will return array with all filter objects 745 | \App\CsvImporters\MyImporter::unsetValidationFilter('bad_word_validation'); // will return `true` 746 | \App\CsvImporters\MyImporter::flushValidationFilters(); // will return empty array 747 | 748 | // example case 749 | if ($request->get('without_filters')) { 750 | \App\CsvImporters\MyImporter::flushValidationFilters(); 751 | } 752 | ``` 753 | 754 | ``` 755 | WARNING!!! 756 | All closure validation filters are global. 757 | ``` 758 | 759 | ``` 760 | WARNING!!! 761 | If you will set not related validation rule(filter) for a field(header) and not specify and register 762 | a custom validation filter for that the RGilyov\CsvImporter\Exceptions\ImportValidationException will be thrown 763 | ``` 764 | 765 | ## Custom cast filters ## 766 | 767 | To make custom cast filter you need to make a class which will extends `\RGilyov\CsvImporter\BaseCastFilter` 768 | or just run `php artisan make:csv-importer-cast-filter MyCastFilter` which will make `MyCastFilter.php` 769 | file with basic set up inside `app/CsvImporters/CastFilters/` folder: 770 | 771 | ```php 772 | ['cast' => 'lowercase']` 795 | 796 | All filters manipulation is similar to what was described in `Custom headers filters` and `Custom validation filters` 797 | chapters: 798 | 799 | ```php 800 | \App\CsvImporters\MyImporter::addCastFilter(new \App\CsvImporters\CastFilters\MyCastFilter()); 801 | 802 | \App\CsvImporters\MyImporter::addCastFilter(function ($value) { 803 | return htmlspecialchars($value); 804 | }); 805 | 806 | $htmlentities = function ($value) { 807 | return htmlentities($value); 808 | } 809 | 810 | $myCastFilter = new \App\CsvImporters\CastFilters\MyCastFilter(); 811 | 812 | \App\CsvImporters\MyImporter::addCastFilters($htmlentities, $myCastFilter); 813 | 814 | ///////////////////////////////////////////////////////////////////////////// 815 | 816 | \App\CsvImporters\MyImporter::castFilterExists('lowercase'); // will return `true` 817 | \App\CsvImporters\MyImporter::getCastFilter('lowercase'); // will return instance of MyCastFilter class 818 | \App\CsvImporters\MyImporter::getCastFilters(); // will return array with all filter objects 819 | \App\CsvImporters\MyImporter::unsetCastFilter('lowercase'); // will return `true` 820 | \App\CsvImporters\MyImporter::flushCastFilters(); // will return empty array 821 | 822 | // example case 823 | if ($request->get('without_filters')) { 824 | \App\CsvImporters\MyImporter::flushCastFilters(); 825 | } 826 | ``` 827 | 828 | ## The best way to register your custom filters ## 829 | 830 | I think the best way to register custom filters is to use a service provider: https://laravel.com/docs/5.4/providers 831 | -------------------------------------------------------------------------------- /src/BaseCsvImporter.php: -------------------------------------------------------------------------------- 1 | 0 149 | ]; 150 | 151 | /** 152 | * @var string 153 | */ 154 | protected $progressCacheKey; 155 | 156 | /** 157 | * @var string 158 | */ 159 | protected $inputEncoding; 160 | 161 | /** 162 | * @var string 163 | */ 164 | protected $outputEncoding; 165 | 166 | /** 167 | * @var string 168 | */ 169 | protected $progressMessageKey; 170 | 171 | /** 172 | * @var string 173 | */ 174 | protected $progressCancelKey; 175 | 176 | /** 177 | * @var string 178 | */ 179 | protected $progressDetailsKey; 180 | 181 | /** 182 | * @var string 183 | */ 184 | protected $progressFinalDetailsKey; 185 | 186 | /** 187 | * @var string 188 | */ 189 | protected $csvCountCacheKey; 190 | 191 | /** 192 | * @var string 193 | */ 194 | protected $mutexLockKey; 195 | 196 | /** 197 | * @var string 198 | */ 199 | protected $importPathsKey; 200 | 201 | /** 202 | * @var string 203 | */ 204 | protected $progressFinishedKey; 205 | 206 | /** 207 | * @var bool 208 | */ 209 | protected $csvDateFormat; 210 | 211 | /** 212 | * BaseCsvImporter constructor. 213 | * @throws CsvImporterException 214 | */ 215 | public function __construct() 216 | { 217 | $this->baseConfig = $this->getBaseConfig(); 218 | $this->mutexLockKey = $this->filterMutexLockKey(); 219 | 220 | $this->mutexLockTime = $this->getConfigProperty('mutex_lock_time', 300, 'integer'); 221 | $this->inputEncoding = $this->getConfigProperty('input_encoding', 'UTF-8', 'string'); 222 | $this->outputEncoding = $this->getConfigProperty('output_encoding', 'UTF-8', 'string'); 223 | 224 | $this->delimiter = $this->getConfigProperty('delimiter', ','); 225 | $this->enclosure = $this->getConfigProperty('enclosure', '"'); 226 | $this->escape = $this->getConfigProperty('escape', '\\'); 227 | $this->newline = $this->getConfigProperty('newline', ''); 228 | $this->csvDateFormat = $this->getConfigProperty('csv_date_format', null, 'string'); 229 | 230 | $this->config = $this->csvConfigurations(); 231 | $this->csvWriters = collect([]); 232 | $this->cache = $this->getCacheDriver(); 233 | 234 | $this->setKeys(); 235 | } 236 | 237 | /* 238 | |-------------------------------------------------------------------------- 239 | | Configuration methods 240 | |-------------------------------------------------------------------------- 241 | */ 242 | 243 | /** 244 | * @param $property 245 | * @param $default 246 | * @param bool $cast 247 | * @return mixed 248 | */ 249 | protected function getConfigProperty($property, $default = null, $cast = null) 250 | { 251 | if (isset($this->config['config'][$property]) && ($value = $this->config['config'][$property])) { 252 | return $this->castField($value, $cast); 253 | } 254 | 255 | if (isset($this->baseConfig[$property]) && ($value = $this->baseConfig[$property])) { 256 | return $this->castField($value, $cast); 257 | } 258 | 259 | return $default; 260 | } 261 | 262 | /** 263 | * @return void 264 | */ 265 | protected function systemSettings() 266 | { 267 | /* 268 | * Make sure that import will run even if a user will close the import page 269 | */ 270 | ignore_user_abort(true); 271 | 272 | /* 273 | * Make sure the application have enough memory for the import 274 | */ 275 | ini_set('memory_limit', $this->getConfigProperty('memory_limit', 128, 'integer') . 'M'); 276 | 277 | /* 278 | * Make sure the script will run as long as mutex locked 279 | */ 280 | ini_set('max_execution_time', $this->mutexLockTime * 60); 281 | } 282 | 283 | /** 284 | * Always reset keys after mutes key concatenation or key changes 285 | * 286 | * @throws CsvImporterException 287 | */ 288 | protected function setKeys() 289 | { 290 | $this->importPathsKey = $this->mutexLockKey . '_paths'; 291 | $this->csvCountCacheKey = $this->mutexLockKey . '_quantity'; 292 | $this->progressCacheKey = $this->mutexLockKey . '_processed'; 293 | $this->progressMessageKey = $this->mutexLockKey . '_message'; 294 | $this->progressCancelKey = $this->mutexLockKey . '_cancel'; 295 | $this->progressDetailsKey = $this->mutexLockKey . '_details'; 296 | $this->progressFinalDetailsKey = $this->mutexLockKey . '_final_details'; 297 | $this->progressFinishedKey = $this->mutexLockKey . '_finished'; 298 | $this->setMutex(); 299 | } 300 | 301 | /** 302 | * @return mixed 303 | */ 304 | protected function filterMutexLockKey() 305 | { 306 | return Str::slug(($key = $this->getConfigProperty('mutex_lock_key')) ? $key : (string)($this), '_'); 307 | } 308 | 309 | /** 310 | * @return $this 311 | * @throws CsvImporterException 312 | */ 313 | public function resetMutexLockKey() 314 | { 315 | $this->mutexLockKey = $this->filterMutexLockKey(); 316 | $this->setKeys(); 317 | 318 | return $this; 319 | } 320 | 321 | /** 322 | * You may change mutex key with concatenation, 323 | * useful when you have multiple imports for one import class at the same time 324 | * 325 | * @param $concat 326 | * @return $this 327 | * @throws CsvImporterException 328 | */ 329 | public function concatMutexKey($concat) 330 | { 331 | $this->mutexLockKey = $this->mutexLockKey . '_' . $concat; 332 | 333 | /* 334 | * Important to reset all keys after concatenation due to all keys depends on `mutexLockKey` 335 | */ 336 | $this->setKeys(); 337 | 338 | return $this; 339 | } 340 | 341 | /** 342 | * @return Repository 343 | */ 344 | protected function getCacheDriver() 345 | { 346 | $cacheDriver = $this->getConfigProperty('mutex_cache_driver', 'file', 'string'); 347 | 348 | return (new CacheManager(app()))->driver($cacheDriver); 349 | } 350 | 351 | /* 352 | |-------------------------------------------------------------------------- 353 | | Methods for import customization 354 | |-------------------------------------------------------------------------- 355 | */ 356 | 357 | /** 358 | * Specify csv mappings and rules here 359 | * 360 | * @return array 361 | */ 362 | public function csvConfigurations() 363 | { 364 | return []; 365 | } 366 | 367 | /** 368 | * If a csv line is valid, the method will be executed on it 369 | * 370 | * @param $item 371 | * @return array 372 | */ 373 | protected function handle($item) 374 | { 375 | 376 | } 377 | 378 | /** 379 | * If a csv line will not pass `validation` filters, the method will be executed on the line 380 | * 381 | * @param $item 382 | * @return array 383 | */ 384 | protected function invalid($item) 385 | { 386 | 387 | } 388 | 389 | /** 390 | * Will be executed before importing 391 | * 392 | * @return void 393 | */ 394 | protected function before() 395 | { 396 | 397 | } 398 | 399 | /** 400 | * Will be executed after importing 401 | * 402 | * @return void 403 | */ 404 | protected function after() 405 | { 406 | 407 | } 408 | 409 | /** 410 | * Initialize new progress bar 411 | * 412 | * @param $message 413 | * @param $quantity 414 | */ 415 | protected function initProgressBar($message, $quantity) 416 | { 417 | $this->dropProgress(); 418 | $this->setProgressMessage($message); 419 | $this->setProgressQuantity($quantity); 420 | } 421 | 422 | /** 423 | * Adjust additional information to progress bar during import process 424 | * 425 | * @return null|string|array 426 | */ 427 | public function progressBarDetails() 428 | { 429 | 430 | } 431 | 432 | /** 433 | * Set final details to your importer, which a user will see at the end of the import process 434 | * 435 | * @param $details 436 | */ 437 | public function setFinalDetails($details) 438 | { 439 | $this->cache->forever($this->progressFinalDetailsKey, $details); 440 | } 441 | 442 | /** 443 | * @return mixed 444 | */ 445 | public function getFinalDetails() 446 | { 447 | return $this->cache->get($this->progressFinalDetailsKey); 448 | } 449 | 450 | /** 451 | * Will be executed during the import process canceling 452 | */ 453 | protected function onCancel() 454 | { 455 | 456 | } 457 | 458 | /* 459 | |-------------------------------------------------------------------------- 460 | | Setters 461 | |-------------------------------------------------------------------------- 462 | */ 463 | 464 | /** 465 | * @param string|\SplFileInfo $file 466 | * @return static 467 | */ 468 | public function setCsvFile($file) 469 | { 470 | $this->csvFile = $file; 471 | $this->setReader(); 472 | 473 | return $this; 474 | } 475 | 476 | /** 477 | * @param string $delimiter 478 | * @return $this 479 | */ 480 | public function setDelimiter($delimiter) 481 | { 482 | $this->delimiter = $delimiter; 483 | $this->resetReader(); 484 | 485 | return $this; 486 | } 487 | 488 | /** 489 | * @param string $enclosure 490 | * @return $this 491 | */ 492 | public function setEnclosure($enclosure) 493 | { 494 | $this->enclosure = $enclosure; 495 | $this->resetReader(); 496 | 497 | return $this; 498 | } 499 | 500 | /** 501 | * @param string $escape 502 | * @return $this 503 | */ 504 | public function setEscape($escape) 505 | { 506 | $this->escape = $escape; 507 | $this->resetReader(); 508 | 509 | return $this; 510 | } 511 | 512 | /** 513 | * @param string $newline 514 | * @return $this 515 | */ 516 | public function setNewline($newline) 517 | { 518 | $this->newline = $newline; 519 | $this->resetReader(); 520 | 521 | return $this; 522 | } 523 | 524 | /** 525 | * Specify encoding of your file, UTF-8 by default 526 | * 527 | * @param string $encoding 528 | * @return $this 529 | */ 530 | public function setInputEncoding($encoding) 531 | { 532 | $this->inputEncoding = $encoding; 533 | 534 | return $this; 535 | } 536 | 537 | /** 538 | * Specify encoding of your file, UTF-8 by default 539 | * 540 | * @param string $encoding 541 | * @return $this 542 | */ 543 | public function setOutputEncoding($encoding) 544 | { 545 | $this->outputEncoding = $encoding; 546 | 547 | return $this; 548 | } 549 | 550 | /** 551 | * Specify date format that contains your csv file, `Y-m-d` by default 552 | * 553 | * @param $format 554 | * @return $this 555 | */ 556 | public function setCsvDateFormat($format) 557 | { 558 | $this->csvDateFormat = $format; 559 | 560 | return $this; 561 | } 562 | 563 | /* 564 | |-------------------------------------------------------------------------- 565 | | Getter 566 | |-------------------------------------------------------------------------- 567 | */ 568 | 569 | /** 570 | * @param $name 571 | * @return mixed 572 | */ 573 | public function __get($name) 574 | { 575 | $name = Str::camel(str_replace('get', '', $name)); 576 | 577 | return (property_exists($this, $name)) ? $this->{$name} : null; 578 | } 579 | 580 | /** 581 | * @param $name 582 | * @param $get 583 | * @return mixed 584 | */ 585 | public function __call($name, $get = null) 586 | { 587 | return (isset($get[0]) && is_string($get[0])) ? $this->__get($get[0]) : $this->__get($name); 588 | } 589 | 590 | /* 591 | |-------------------------------------------------------------------------- 592 | | Basic csv manipulation methods 593 | |-------------------------------------------------------------------------- 594 | */ 595 | 596 | /** 597 | * @param \Closure $callable 598 | * @return bool 599 | */ 600 | public function each(\Closure $callable) 601 | { 602 | if (!$this->exists()) { 603 | return false; 604 | } 605 | 606 | foreach ($this->csvReader->setOffset(1)->fetchAssoc($this->headers) as $item) { 607 | $callable($this->castFields($this->checkEncoding($item))); 608 | } 609 | 610 | return true; 611 | } 612 | 613 | /** 614 | * @param string $property 615 | * @return Collection|bool 616 | */ 617 | public function distinct($property) 618 | { 619 | if (!$this->exists()) { 620 | return false; 621 | } 622 | 623 | $distinct = collect([]); 624 | 625 | $this->each(function ($item) use ($distinct, $property) { 626 | $value = (isset($item[$property]) && $item[$property]) ? $item[$property] : false; 627 | if (false !== $value && !$distinct->offsetExists($value)) { 628 | $distinct->put($value, true); 629 | } 630 | }); 631 | 632 | return $distinct->keys(); 633 | } 634 | 635 | /** 636 | * @return int 637 | */ 638 | public function countCsv() 639 | { 640 | if (!$this->exists()) { 641 | return false; 642 | } 643 | 644 | $quantity = $this->csvReader->each(function () { 645 | return true; 646 | }); 647 | 648 | /* 649 | * -- to exclude headers line 650 | */ 651 | return --$quantity; 652 | } 653 | 654 | /* 655 | |-------------------------------------------------------------------------- 656 | | Api methods 657 | |-------------------------------------------------------------------------- 658 | */ 659 | 660 | /** 661 | * Run import 662 | * 663 | * @return array|bool 664 | * @throws CsvImporterException 665 | */ 666 | public function run() 667 | { 668 | return $this->tryStart(); 669 | } 670 | 671 | /** 672 | * Check if import process is finished 673 | * 674 | * @return bool 675 | */ 676 | public function isFinished() 677 | { 678 | return ( bool )$this->cache->get($this->progressFinishedKey); 679 | } 680 | 681 | /** 682 | * Finish import, unlock mutex and get final information 683 | * 684 | * @return array 685 | */ 686 | public function finish() 687 | { 688 | if ($this->isLocked() && !$this->isFinished()) { 689 | return $this->progressBar(); 690 | } 691 | 692 | /* 693 | * Get data before mutex unlocked and session cleared 694 | */ 695 | $data = $this->finalProgressDetails(); 696 | 697 | $this->unlock(); 698 | 699 | return $data; 700 | } 701 | 702 | /** 703 | * Cancel your import process in any time 704 | * 705 | * @return bool 706 | */ 707 | public function cancel() 708 | { 709 | return $this->cache->put($this->progressCancelKey, true, $this->mutexLockTime); 710 | } 711 | 712 | /** 713 | * Get progress bar 714 | * 715 | * @return array 716 | */ 717 | public function getProgress() 718 | { 719 | return $this->progressBar(); 720 | } 721 | 722 | /** 723 | * @return bool 724 | */ 725 | public function exists() 726 | { 727 | return ( bool )$this->csvFile; 728 | } 729 | 730 | /** 731 | * Insert an item (csv line) to a file from `csv_files` configuration array 732 | * 733 | * @param $fileName 734 | * @param $item 735 | * @return mixed 736 | * @throws CsvImporterException 737 | */ 738 | public function insertTo($fileName, $item) 739 | { 740 | try { 741 | return $this->csvWriters[$fileName]->insertOne($item); 742 | } catch (\Exception $e) { 743 | throw new CsvImporterException( 744 | ['message' => $fileName . ' file was not found, please check `csv_files` paths inside your configurations'], 745 | 400 746 | ); 747 | } 748 | } 749 | 750 | /** 751 | * Extract any array values to given csv headers 752 | * 753 | * @param array $data 754 | * @return array 755 | */ 756 | public function toCsvHeaders(array $data) 757 | { 758 | if (!$this->exists()) { 759 | return null; 760 | } 761 | 762 | $csvData = []; 763 | foreach ($this->headers as $value) { 764 | $csvData[$value] = (isset($data[$value])) ? $data[$value] : ''; 765 | } 766 | 767 | return $csvData; 768 | } 769 | /** 770 | * Extract fields which was specified inside `mappings` array in the configurations, from the given csv line 771 | * 772 | * @param array $item 773 | * @return array 774 | */ 775 | public function extractDefinedFields(array $item) 776 | { 777 | return ($this->configMappingsExists()) ? array_intersect_key($item, $this->config['mappings']) : []; 778 | } 779 | 780 | /* 781 | |-------------------------------------------------------------------------- 782 | | Main functionality 783 | |-------------------------------------------------------------------------- 784 | */ 785 | 786 | /** 787 | * @return array|bool 788 | * @throws CsvImporterException 789 | */ 790 | private function tryStart() 791 | { 792 | if (!$this->isLocked() && !$this->isFinished()) { 793 | if (!$this->exists()) { 794 | return false; 795 | } 796 | 797 | $this->lock(); 798 | 799 | $this->initialize(); 800 | $this->process(); 801 | $this->finalStage(); 802 | 803 | $this->setAsFinished(); 804 | $this->mutex->releaseLock(); 805 | } 806 | 807 | return $this->progressBar(); 808 | } 809 | 810 | /** 811 | * @throws CsvImporterException 812 | */ 813 | private function initialize() 814 | { 815 | $this->isCanceled(); 816 | 817 | $this->systemSettings(); 818 | $this->setWriters(); 819 | $this->validateHeaders(); 820 | $this->checkHeadersDuplicates(); 821 | 822 | $this->hasErrors(); 823 | $this->isCanceled(); 824 | 825 | $this->before(); 826 | $this->initProgressBar( 827 | $this->getConfigProperty('progress', '', 'string'), 828 | $this->countCsv() 829 | ); 830 | 831 | $this->hasErrors(); 832 | $this->isCanceled(); 833 | } 834 | 835 | /** 836 | * @throws CsvImporterException 837 | */ 838 | private function finalStage() 839 | { 840 | $this->isCanceled(); 841 | $this->after(); 842 | $this->hasErrors(); 843 | } 844 | 845 | /** 846 | * @return void 847 | */ 848 | public function clearSession() 849 | { 850 | $this->cache->forget($this->progressCacheKey); 851 | $this->cache->forget($this->csvCountCacheKey); 852 | $this->cache->forget($this->progressMessageKey); 853 | $this->cache->forget($this->progressDetailsKey); 854 | $this->cache->forget($this->progressFinalDetailsKey); 855 | $this->cache->forget($this->progressCancelKey); 856 | $this->cache->forget($this->progressFinishedKey); 857 | $this->cache->forget($this->importPathsKey); 858 | } 859 | 860 | /** 861 | * @return void 862 | */ 863 | protected function process() 864 | { 865 | $this->each(function ($item) { 866 | $this->isCanceled(); 867 | ($this->validateItem($item)) ? $this->handle($item) : $this->invalid($item); 868 | $this->incrementProgress(); 869 | }); 870 | } 871 | 872 | /** 873 | * @param array $item 874 | * @return array 875 | */ 876 | protected function checkEncoding(array $item) 877 | { 878 | if (strcasecmp($this->inputEncoding, $this->outputEncoding) !== 0) { 879 | foreach ($item as $key => $value) { 880 | if (is_string($value) && !is_numeric($value)) { 881 | $item[$key] = iconv($this->inputEncoding, $this->outputEncoding, $value); 882 | } 883 | } 884 | } 885 | 886 | return $item; 887 | } 888 | 889 | /** 890 | * @return void 891 | */ 892 | protected function setReader() 893 | { 894 | $this->csvReader = Reader::createFromPath($this->csvFile) 895 | ->setDelimiter($this->delimiter) 896 | ->setEnclosure($this->enclosure) 897 | ->setEscape($this->escape) 898 | ->setNewline($this->newline); 899 | 900 | $this->headers = array_map(function ($value) 901 | { 902 | return strtolower(preg_replace('/[[:^print:]]/', '', $value)); 903 | }, $this->csvReader->fetchOne()); 904 | } 905 | 906 | /** 907 | * @return void 908 | */ 909 | protected function resetReader() 910 | { 911 | if ($this->exists()) { 912 | $this->setReader(); 913 | } 914 | } 915 | 916 | /** 917 | * @throws CsvImporterException 918 | */ 919 | private function setWriters() 920 | { 921 | if (isset($this->config['csv_files']) && is_array($this->config['csv_files'])) { 922 | $paths = []; 923 | foreach ($this->config['csv_files'] as $csvFileKeyName => $path) { 924 | $fullPath = $this->createFile($this->unifyPathIfExists($path)); 925 | 926 | $this->csvWriters->put($csvFileKeyName, $this->makeWriter($fullPath)); 927 | 928 | $paths[$csvFileKeyName] = $fullPath; 929 | } 930 | 931 | $this->cache->forever($this->importPathsKey, $paths); 932 | } 933 | } 934 | 935 | /** 936 | * @param $path 937 | * @return string 938 | * @throws CsvImporterException 939 | */ 940 | protected function createFile($path) 941 | { 942 | if (!Storage::put($path, '')) { 943 | throw new CsvImporterException( 944 | ['message' => 'Not able to create csv file. Path: ' . $this->fullPath($path)], 945 | 400 946 | ); 947 | } 948 | 949 | return $this->fullPath($path); 950 | } 951 | 952 | /** 953 | * @param $path 954 | * @return string 955 | */ 956 | protected function fullPath($path) 957 | { 958 | return $this->concatenatePath(Storage::disk()->getDriver()->getAdapter()->getPathPrefix(), $path); 959 | } 960 | 961 | /** 962 | * @param $path 963 | * @return mixed|string 964 | */ 965 | protected function unifyPathIfExists($path) 966 | { 967 | if (Storage::exists($path)) { 968 | $time = "_" . Carbon::now()->format('Y_M_D_\a\t_H_i') . "_"; 969 | return (substr_count($path, '.') === 1) ? str_replace(".", $time.'.', $path) : ($path . $time); 970 | } 971 | 972 | return $path; 973 | } 974 | 975 | /** 976 | * @param $path 977 | * @return Writer 978 | */ 979 | private function makeWriter($path) 980 | { 981 | return Writer::createFromPath($path) 982 | ->setDelimiter($this->delimiter) 983 | ->setEnclosure($this->enclosure) 984 | ->setEscape($this->escape) 985 | ->setNewline($this->newline) 986 | ->insertOne(implode($this->delimiter, $this->headers)); 987 | } 988 | 989 | /** 990 | * @param $firstPart 991 | * @param $lastPart 992 | * @return string 993 | */ 994 | protected function concatenatePath($firstPart, $lastPart) 995 | { 996 | return rtrim($firstPart, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($lastPart, DIRECTORY_SEPARATOR); 997 | } 998 | 999 | /** 1000 | * @throws CsvImporterException 1001 | */ 1002 | protected function isCanceled() 1003 | { 1004 | if ($this->cache->get($this->progressCancelKey)) { 1005 | $this->onCancel(); 1006 | $this->unlock(); 1007 | throw new CsvImporterException(['message' => 'Importing had canceled'], 200); 1008 | } 1009 | } 1010 | 1011 | /** 1012 | * @param array $item 1013 | * @return array 1014 | */ 1015 | public function castFields(array $item) 1016 | { 1017 | if ($this->configMappingsExists()) { 1018 | foreach ($this->config['mappings'] as $field => $rules) { 1019 | if (isset($rules[self::CAST]) && isset($item[$field])) { 1020 | 1021 | $castFilter = $rules[self::CAST]; 1022 | 1023 | if (is_array($castFilter)) { 1024 | foreach ($castFilter as $cast) { 1025 | $item[$field] = $this->performCastOnValue($item[$field], $cast); 1026 | } 1027 | } elseif (is_string($castFilter)) { 1028 | $item[$field] = $this->performCastOnValue($item[$field], $castFilter); 1029 | } 1030 | } 1031 | } 1032 | } 1033 | 1034 | return $item; 1035 | } 1036 | 1037 | /** 1038 | * @param $value 1039 | * @param $cast 1040 | * @return mixed 1041 | */ 1042 | public function performCastOnValue($value, $cast) 1043 | { 1044 | if ($filter = static::getCastFilter($cast)) { 1045 | return $filter->filter($value); 1046 | } 1047 | 1048 | return $this->castField($value, $cast); 1049 | } 1050 | 1051 | /** 1052 | * Cast a field to a native PHP type. 1053 | * 1054 | * @param string $type 1055 | * @param mixed $value 1056 | * @return mixed 1057 | */ 1058 | protected function castField($value, $type) 1059 | { 1060 | if (is_null($value) || !is_string($type)) { 1061 | return $value; 1062 | } 1063 | 1064 | switch ($type) { 1065 | case 'int': 1066 | case 'integer': 1067 | return (int) $value; 1068 | case 'real': 1069 | case 'float': 1070 | case 'double': 1071 | return (float) $value; 1072 | case 'string': 1073 | return (string) $value; 1074 | case 'bool': 1075 | case 'boolean': 1076 | return (bool) $value; 1077 | case 'date': 1078 | return $this->toDate($value); 1079 | case 'datetime': 1080 | case 'date_time': 1081 | return $this->toDateTime($value); 1082 | case 'array': 1083 | return (array)$value; 1084 | default: 1085 | return $value; 1086 | } 1087 | } 1088 | 1089 | /** 1090 | * @param $date 1091 | * @return string 1092 | */ 1093 | public function toDateTime($date) 1094 | { 1095 | return $this->formatDate($date)->toDateTimeString(); 1096 | } 1097 | 1098 | /** 1099 | * @param $date 1100 | * @return string 1101 | */ 1102 | public function toDate($date) 1103 | { 1104 | return $this->formatDate($date)->toDateString(); 1105 | } 1106 | 1107 | /** 1108 | * @param $date 1109 | * @return Carbon 1110 | */ 1111 | public function formatDate($date) 1112 | { 1113 | return ($this->csvDateFormat) ? $this->withDateFormat($date) : $this->withoutDateFormat($date); 1114 | } 1115 | 1116 | /** 1117 | * @param $date 1118 | * @return Carbon 1119 | */ 1120 | protected function withDateFormat($date) 1121 | { 1122 | try { 1123 | return Carbon::createFromFormat($this->csvDateFormat, $date); 1124 | } catch (\Exception $e) { 1125 | return $this->dummyCarbonDate(); 1126 | } 1127 | } 1128 | 1129 | /** 1130 | * @param $date 1131 | * @return Carbon 1132 | */ 1133 | protected function withoutDateFormat($date) 1134 | { 1135 | try { 1136 | return Carbon::parse(trim(preg_replace('/(\/|\\\|\||\.|\,)/', '-', $date))); 1137 | } catch (\Exception $e) { 1138 | return $this->dummyCarbonDate(); 1139 | } 1140 | } 1141 | 1142 | /** 1143 | * @return Carbon 1144 | */ 1145 | protected function dummyCarbonDate() 1146 | { 1147 | return Carbon::createFromFormat('Y-m-d H:i:s', '0001-01-01 00:00:00'); 1148 | } 1149 | 1150 | /** 1151 | * @return array 1152 | */ 1153 | public function getErrors() 1154 | { 1155 | return $this->errors; 1156 | } 1157 | 1158 | /** 1159 | * @param array $item 1160 | * @return bool 1161 | * @throws ImportValidationException 1162 | */ 1163 | public function validateItem(array $item) 1164 | { 1165 | if (!$this->executeGlobalValidationFilters($item)) { 1166 | return false; 1167 | } 1168 | 1169 | if ($this->configMappingsExists()) { 1170 | $validationRules = []; 1171 | $customValidationRules = []; 1172 | 1173 | foreach ($this->config['mappings'] as $field => $rules) { 1174 | if (isset($rules[self::VALIDATION]) && isset($item[$field])) { 1175 | $rules = $this->separateValidationFilters($rules[self::VALIDATION]); 1176 | $validationRules[$field] = $rules['standard']; 1177 | 1178 | if (!empty($rules['custom'])) { 1179 | $customValidationRules[] = ['filters' => $rules['custom'], 'value' => $item[$field]]; 1180 | } 1181 | } 1182 | } 1183 | 1184 | if (!empty($customValidationRules) && !$this->performCustomValidation($customValidationRules)) { 1185 | return false; 1186 | } 1187 | 1188 | if (!empty($validationRules) && !$this->passes($item, $validationRules)) { 1189 | return false; 1190 | } 1191 | } 1192 | 1193 | return true; 1194 | } 1195 | 1196 | /** 1197 | * @param array $customValidationRules 1198 | * @return bool 1199 | */ 1200 | protected function performCustomValidation(array $customValidationRules) 1201 | { 1202 | foreach ($customValidationRules as $couple) { 1203 | foreach ($couple['filters'] as $filterName) { 1204 | $filter = static::getFilter(self::VALIDATION, $filterName); 1205 | if ($filter instanceof BaseValidationFilter && !$filter->global) { 1206 | if (!$filter->filter($couple['value'])) { 1207 | return false; 1208 | } 1209 | } 1210 | } 1211 | } 1212 | 1213 | return true; 1214 | } 1215 | 1216 | /** 1217 | * @param $filters 1218 | * @return array 1219 | */ 1220 | public function separateValidationFilters($filters) 1221 | { 1222 | $filters = (is_string($filters)) ? explode('|', $filters) : (array)$filters; 1223 | $customValidationFilters = []; 1224 | 1225 | foreach ($filters as $key => $filter) { 1226 | if (static::validationFilterExists($filter)) { 1227 | $customValidationFilters[] = $filter; 1228 | unset($filters[$key]); 1229 | } 1230 | } 1231 | 1232 | return ['standard' => $filters, 'custom' => $customValidationFilters]; 1233 | } 1234 | 1235 | /** 1236 | * @param array $item 1237 | * @param array $validationRules 1238 | * @return mixed 1239 | * @throws ImportValidationException 1240 | */ 1241 | protected function passes(array $item, array $validationRules) 1242 | { 1243 | try { 1244 | return Validator::make($item, $validationRules)->passes(); 1245 | } catch (\BadMethodCallException $e) { 1246 | throw new ImportValidationException($e->getMessage(), 400); 1247 | } 1248 | } 1249 | 1250 | /** 1251 | * @param array $item 1252 | * @return bool 1253 | */ 1254 | public function executeGlobalValidationFilters(array $item) 1255 | { 1256 | foreach (static::getValidationFilters() as $filter) { 1257 | if ($filter instanceof BaseValidationFilter && $filter->global) { 1258 | if (!$filter->filter($item)) { 1259 | return false; 1260 | } 1261 | } 1262 | } 1263 | 1264 | return true; 1265 | } 1266 | 1267 | /** 1268 | * @return void 1269 | */ 1270 | protected function validateHeaders() 1271 | { 1272 | foreach ($this->getRequiredHeaders() as $field) { 1273 | if (array_search($field, $this->headers) === false) { 1274 | $this->setError('Required headers not found:', 'The "' . $field . '" header is required'); 1275 | } 1276 | } 1277 | 1278 | $this->executeHeadersFilters(); 1279 | } 1280 | 1281 | /** 1282 | * @return void 1283 | */ 1284 | protected function executeHeadersFilters() 1285 | { 1286 | foreach (static::getHeadersFilters() as $filter) { 1287 | if ($filter instanceof BaseHeadersFilter) { 1288 | $result = $filter->executeFilter($this->headers); 1289 | if ($result->error) { 1290 | $this->setError('Headers error:', $result->message); 1291 | } 1292 | } 1293 | } 1294 | } 1295 | 1296 | /** 1297 | * @parameters BaseHeaderFilter 1298 | * @return array 1299 | */ 1300 | public static function addHeadersFilters() 1301 | { 1302 | return static::addFilters(self::HEADERS, func_get_args()); 1303 | } 1304 | 1305 | /** 1306 | * @parameters BaseHeaderFilter 1307 | * @return array 1308 | */ 1309 | public static function addValidationFilters() 1310 | { 1311 | return static::addFilters(self::VALIDATION, func_get_args()); 1312 | } 1313 | 1314 | /** 1315 | * @parameters BaseHeaderFilter 1316 | * @return array 1317 | */ 1318 | public static function addCastFilters() 1319 | { 1320 | return static::addFilters(self::CAST, func_get_args()); 1321 | } 1322 | 1323 | /** 1324 | * @param $filter 1325 | * @param null $name 1326 | * @return bool|ClosureCastFilter|ClosureValidationFilter|ClosureHeadersFilter 1327 | */ 1328 | public static function addHeadersFilter($filter, $name = null) 1329 | { 1330 | return static::addFilter(self::HEADERS, $filter, $name); 1331 | } 1332 | 1333 | /** 1334 | * @param $filter 1335 | * @param null $name 1336 | * @return bool|ClosureCastFilter|ClosureValidationFilter|ClosureHeadersFilter 1337 | */ 1338 | public static function addValidationFilter($filter, $name = null) 1339 | { 1340 | return static::addFilter(self::VALIDATION, $filter, $name); 1341 | } 1342 | 1343 | /** 1344 | * @param $filter 1345 | * @param null $name 1346 | * @return bool|ClosureCastFilter|ClosureValidationFilter|ClosureHeadersFilter 1347 | */ 1348 | public static function addCastFilter($filter, $name = null) 1349 | { 1350 | return static::addFilter(self::CAST, $filter, $name); 1351 | } 1352 | 1353 | /** 1354 | * @return array 1355 | */ 1356 | public static function getHeadersFilters() 1357 | { 1358 | return static::getFilters(self::HEADERS); 1359 | } 1360 | 1361 | /** 1362 | * @return array 1363 | */ 1364 | public static function getValidationFilters() 1365 | { 1366 | return static::getFilters(self::VALIDATION); 1367 | } 1368 | 1369 | /** 1370 | * @return array 1371 | */ 1372 | public static function getCastFilters() 1373 | { 1374 | return static::getFilters(self::CAST); 1375 | } 1376 | 1377 | /** 1378 | * @param $type 1379 | * @return mixed 1380 | */ 1381 | public static function getFilters($type) 1382 | { 1383 | return Arr::get(static::${$type . 'Filters'}, static::class, []); 1384 | } 1385 | 1386 | /** 1387 | * @param $name 1388 | * @return mixed 1389 | */ 1390 | public static function getHeadersFilter($name) 1391 | { 1392 | return static::getFilter(self::HEADERS, $name); 1393 | } 1394 | 1395 | /** 1396 | * @param $name 1397 | * @return mixed 1398 | */ 1399 | public static function getValidationFilter($name) 1400 | { 1401 | return static::getFilter(self::VALIDATION, $name); 1402 | } 1403 | 1404 | /** 1405 | * @param $name 1406 | * @return mixed 1407 | */ 1408 | public static function getCastFilter($name) 1409 | { 1410 | return static::getFilter(self::CAST, $name); 1411 | } 1412 | 1413 | /** 1414 | * @param $type 1415 | * @param $name 1416 | * @return null 1417 | */ 1418 | public static function getFilter($type, $name) 1419 | { 1420 | return (static::filterExists($type, $name)) ? static::${$type . 'Filters'}[static::class][$name] : null; 1421 | } 1422 | 1423 | /** 1424 | * @param $name 1425 | * @return mixed 1426 | */ 1427 | public static function unsetHeadersFilter($name) 1428 | { 1429 | return static::getFilter(self::HEADERS, $name); 1430 | } 1431 | 1432 | /** 1433 | * @param $name 1434 | * @return mixed 1435 | */ 1436 | public static function unsetValidationFilter($name) 1437 | { 1438 | return static::getFilter(self::VALIDATION, $name); 1439 | } 1440 | 1441 | /** 1442 | * @param $name 1443 | * @return mixed 1444 | */ 1445 | public static function unsetCastFilter($name) 1446 | { 1447 | return static::getFilter(self::CAST, $name); 1448 | } 1449 | 1450 | /** 1451 | * @param $type 1452 | * @param $name 1453 | * @return null 1454 | */ 1455 | public static function unsetFilter($type, $name) 1456 | { 1457 | if (static::filterExists($type, $name)) { 1458 | Arr::set(static::${$type . 'Filters'}[static::class], $name, null); 1459 | return true; 1460 | } 1461 | 1462 | return false; 1463 | } 1464 | 1465 | /** 1466 | * @return array 1467 | */ 1468 | public static function flushHeadersFilters() 1469 | { 1470 | return static::flushFilters(self::HEADERS); 1471 | } 1472 | 1473 | /** 1474 | * @return array 1475 | */ 1476 | public static function flushValidationFilters() 1477 | { 1478 | return static::flushFilters(self::VALIDATION); 1479 | } 1480 | 1481 | /** 1482 | * @return array 1483 | */ 1484 | public static function flushCastFilters() 1485 | { 1486 | return static::flushFilters(self::CAST); 1487 | } 1488 | 1489 | /** 1490 | * @param $type 1491 | * @return array 1492 | */ 1493 | public static function flushFilters($type) 1494 | { 1495 | static::${$type . 'Filters'}[static::class] = []; 1496 | 1497 | return static::${$type . 'Filters'}[static::class]; 1498 | } 1499 | 1500 | /** 1501 | * @param $name 1502 | * @return bool 1503 | */ 1504 | public static function headersFilterExists($name) 1505 | { 1506 | return static::filterExists(self::HEADERS, $name); 1507 | } 1508 | 1509 | /** 1510 | * @param $name 1511 | * @return bool 1512 | */ 1513 | public static function validationFilterExists($name) 1514 | { 1515 | return static::filterExists(self::VALIDATION, $name); 1516 | } 1517 | 1518 | /** 1519 | * @param $name 1520 | * @return bool 1521 | */ 1522 | public static function castFilterExists($name) 1523 | { 1524 | return static::filterExists(self::CAST, $name); 1525 | } 1526 | 1527 | /** 1528 | * @param $type 1529 | * @param $name 1530 | * @return bool 1531 | */ 1532 | public static function filterExists($type, $name) 1533 | { 1534 | return isset(static::${$type . 'Filters'}[static::class][$name]); 1535 | } 1536 | 1537 | /** 1538 | * @param $type 1539 | * @param array $filters 1540 | * @return mixed 1541 | */ 1542 | public static function addFilters($type, array $filters) 1543 | { 1544 | foreach ($filters as $filter) { 1545 | (is_array($filter)) ? static::addFilters($type, $filter) : static::addFilter($type, $filter); 1546 | } 1547 | 1548 | return static::${$type . 'Filters'}[static::class]; 1549 | } 1550 | 1551 | /** 1552 | * @param $type 1553 | * @param $filter 1554 | * @param null $name 1555 | * @return bool|ClosureCastFilter|ClosureValidationFilter|ClosureHeadersFilter 1556 | */ 1557 | public static function addFilter($type, $filter, $name = null) 1558 | { 1559 | if ($resolved = static::resolveFilter($type, $filter)) { 1560 | return static::${$type . 'Filters'}[static::class][static::filterName($type, $resolved, $name)] = $resolved; 1561 | } 1562 | 1563 | return false; 1564 | } 1565 | 1566 | /** 1567 | * @param $type 1568 | * @param $filter 1569 | * @return bool|ClosureCastFilter|ClosureHeadersFilter|ClosureValidationFilter 1570 | */ 1571 | public static function resolveFilter($type, $filter) 1572 | { 1573 | switch ($type) { 1574 | case self::HEADERS: 1575 | return (static::isClosure($filter)) ? new ClosureHeadersFilter($filter) : static::checkHeadersFilter($filter); 1576 | case self::VALIDATION: 1577 | return (static::isClosure($filter)) ? new ClosureValidationFilter($filter) : static::checkValidationFilter($filter); 1578 | case self::CAST: 1579 | return (static::isClosure($filter)) ? new ClosureCastFilter($filter) : static::checkCastFilter($filter); 1580 | } 1581 | 1582 | return false; 1583 | } 1584 | 1585 | /** 1586 | * @param $type 1587 | * @param $filter 1588 | * @param $name 1589 | * @return string 1590 | */ 1591 | protected static function filterName($type, $filter, $name) 1592 | { 1593 | return static::checkFilterName($type, (is_string($name)) ? $name : (string)$filter); 1594 | } 1595 | 1596 | /** 1597 | * @param string $type 1598 | * @param null $name 1599 | * @param int $counter 1600 | * @return string 1601 | */ 1602 | protected static function checkFilterName($type, $name, $counter = 1) 1603 | { 1604 | $filterName = $name . (($counter > 1) ? '_' . $counter : ''); 1605 | 1606 | if (call_user_func([static::class, $type . 'FilterExists'], $filterName)) { 1607 | return static::checkFilterName($type, $name, ++$counter); 1608 | } 1609 | 1610 | return $filterName; 1611 | } 1612 | 1613 | /** 1614 | * @param $filter 1615 | * @return bool|BaseHeadersFilter 1616 | */ 1617 | public static function checkHeadersFilter($filter) 1618 | { 1619 | return ($filter instanceof BaseHeadersFilter) ? $filter : false; 1620 | } 1621 | 1622 | /** 1623 | * @param $filter 1624 | * @return bool|BaseValidationFilter 1625 | */ 1626 | public static function checkValidationFilter($filter) 1627 | { 1628 | return ($filter instanceof BaseValidationFilter) ? $filter : false; 1629 | } 1630 | 1631 | /** 1632 | * @param $filter 1633 | * @return bool|BaseCastFilter 1634 | */ 1635 | public static function checkCastFilter($filter) 1636 | { 1637 | return ($filter instanceof BaseCastFilter) ? $filter : false; 1638 | } 1639 | 1640 | /** 1641 | * @param $filter 1642 | * @return bool 1643 | */ 1644 | protected static function isClosure($filter) 1645 | { 1646 | return ($filter instanceof \Closure); 1647 | } 1648 | 1649 | /** 1650 | * @return void 1651 | */ 1652 | protected function checkHeadersDuplicates() 1653 | { 1654 | $duplicates = array_diff_assoc($this->headers, array_unique($this->headers)); 1655 | 1656 | if (!empty($duplicates)) { 1657 | foreach ($duplicates as $value) { 1658 | $this->setError('Duplicated values:', 'Csv headers has duplicated fields "' . $value . '"'); 1659 | } 1660 | } 1661 | } 1662 | 1663 | /** 1664 | * @param string $error 1665 | * @param string $message 1666 | */ 1667 | protected function setError($error, $message) 1668 | { 1669 | $this->errors[$error][] = $message; 1670 | $this->errors['quantity'] += 1; 1671 | } 1672 | 1673 | /** 1674 | * @throws CsvImporterException 1675 | */ 1676 | protected function hasErrors() 1677 | { 1678 | if (0 !== $this->errors['quantity']) { 1679 | $this->unlock(); 1680 | throw new CsvImporterException($this->getErrors()); 1681 | } 1682 | } 1683 | 1684 | /** 1685 | * @return array 1686 | */ 1687 | protected function getRequiredHeaders() 1688 | { 1689 | $fieldsWithRules = []; 1690 | 1691 | if ($this->configMappingsExists()) { 1692 | foreach ($this->config['mappings'] as $field => $rules) { 1693 | if (array_search('required', $rules) !== false) { 1694 | $fieldsWithRules[] = $field; 1695 | } 1696 | } 1697 | } 1698 | 1699 | return $fieldsWithRules; 1700 | } 1701 | 1702 | /** 1703 | * @return bool 1704 | */ 1705 | protected function configMappingsExists() 1706 | { 1707 | return (isset($this->config['mappings']) && is_array($this->config['mappings'])); 1708 | } 1709 | 1710 | /* 1711 | |-------------------------------------------------------------------------- 1712 | | Mutex functionality 1713 | |-------------------------------------------------------------------------- 1714 | */ 1715 | 1716 | /** 1717 | * @return Mutex 1718 | * @throws CsvImporterException 1719 | */ 1720 | protected function setMutex() 1721 | { 1722 | $cacheStore = $this->cache->getStore(); 1723 | 1724 | if ($cacheStore instanceof RedisStore) { 1725 | return $this->initMutex( 1726 | new PredisRedisLock( 1727 | (($client = $cacheStore->connection()) instanceof PredisClient) 1728 | ? $client 1729 | : $client->client(null) 1730 | ) 1731 | ); 1732 | } 1733 | 1734 | if ($cacheStore instanceof MemcachedStore) { 1735 | return $this->initMutex(new MemcachedLock($cacheStore->getMemcached())); 1736 | } 1737 | 1738 | if ($cacheStore instanceof FileStore) { 1739 | return $this->initMutex(new FlockLock($cacheStore->getDirectory())); 1740 | } 1741 | 1742 | throw new CsvImporterException( 1743 | ['message' => 'Csv importer supports only: file, memcached and redis cache drivers'], 1744 | 400 1745 | ); 1746 | } 1747 | 1748 | /** 1749 | * @param $driver 1750 | * @return Mutex 1751 | */ 1752 | protected function initMutex($driver) 1753 | { 1754 | return $this->mutex = new Mutex($this->mutexLockKey, $driver); 1755 | } 1756 | 1757 | /** 1758 | * @return bool 1759 | */ 1760 | public function lock() 1761 | { 1762 | return $this->mutex->acquireLock($this->mutexLockTime); 1763 | } 1764 | 1765 | /** 1766 | * @return bool 1767 | */ 1768 | public function unlock() 1769 | { 1770 | $this->clearSession(); 1771 | return $this->mutex->releaseLock(); 1772 | } 1773 | 1774 | /** 1775 | * @return bool 1776 | */ 1777 | public function isLocked() 1778 | { 1779 | return $this->mutex->isLocked(); 1780 | } 1781 | 1782 | /* 1783 | |-------------------------------------------------------------------------- 1784 | | Progress bar functionality 1785 | |-------------------------------------------------------------------------- 1786 | */ 1787 | 1788 | /** 1789 | * @param $quantity 1790 | */ 1791 | protected function setProgressQuantity($quantity) 1792 | { 1793 | $this->cache->put($this->csvCountCacheKey, $quantity, $this->mutexLockTime); 1794 | } 1795 | 1796 | /** 1797 | * @return void 1798 | */ 1799 | protected function incrementProgress() 1800 | { 1801 | $this->cache->increment($this->progressCacheKey); 1802 | } 1803 | 1804 | /** 1805 | * return void 1806 | */ 1807 | protected function setAsFinished() 1808 | { 1809 | $this->cache->forever($this->progressFinishedKey, true); 1810 | } 1811 | 1812 | /** 1813 | * Drop progress quantity 1814 | * 1815 | * @return void 1816 | */ 1817 | protected function dropProgress() 1818 | { 1819 | $this->cache->put($this->progressCacheKey, 0, $this->mutexLockTime); 1820 | } 1821 | 1822 | /** 1823 | * Specify custom progress message during import process 1824 | * 1825 | * @param string $message 1826 | */ 1827 | protected function setProgressMessage($message) 1828 | { 1829 | $this->cache->put($this->progressMessageKey, $message, $this->mutexLockTime); 1830 | } 1831 | 1832 | /** 1833 | * @return array 1834 | */ 1835 | private function progressBar() 1836 | { 1837 | $progress = $this->getProgressDetails(); 1838 | 1839 | if ($progress->finished) { 1840 | return [ 1841 | 'data' => ["message" => $this->getConfigProperty('finished', "", 'string')], 1842 | 'meta' => ["finished" => true, 'init' => false, 'running' => false] 1843 | ]; 1844 | } elseif (!$progress->quantity && !$this->isLocked()) { 1845 | return [ 1846 | 'data' => [ 1847 | "message" => $this->getConfigProperty('does_not_running', "", 'string') 1848 | ], 1849 | 'meta' => ["finished" => false, 'init' => false, 'running' => false] 1850 | ]; 1851 | } elseif (!$progress->quantity && $this->isLocked()) { 1852 | return [ 1853 | 'data' => ["message" => $this->getConfigProperty('initialization', "", 'string')], 1854 | 'meta' => ["finished" => false, 'init' => true, 'running' => true] 1855 | ]; 1856 | } elseif (($progress->quantity == $progress->processed) && $this->isLocked()) { 1857 | return [ 1858 | 'data' => ["message" => $this->getConfigProperty('final_stage', "", 'string')], 1859 | 'meta' => ["finished" => false, 'init' => false, 'running' => true] 1860 | ]; 1861 | } else { 1862 | $data = [ 1863 | 'data' => ["message" => $progress->message], 1864 | 'meta' => [ 1865 | 'processed' => ( int )$progress->processed, 1866 | 'remains' => ( int )$progress->quantity - $progress->processed, 1867 | 'percentage' => floor(($progress->processed / ($progress->quantity / 100))), 1868 | 'finished' => false, 1869 | 'init' => false, 1870 | 'running' => true 1871 | ] 1872 | ]; 1873 | 1874 | if ($details = $this->progressBarDetails()) { 1875 | $data['data']['details'] = $details; 1876 | } 1877 | 1878 | return $data; 1879 | } 1880 | } 1881 | 1882 | /** 1883 | * @return array 1884 | */ 1885 | private function finalProgressDetails() 1886 | { 1887 | $progress = $this->getProgressDetails(); 1888 | 1889 | $data = [ 1890 | 'data' => [ 1891 | "message" => $this->getConfigProperty('final', '', 'string') 1892 | ], 1893 | 'meta' => ["finished" => true, 'init' => false, 'running' => false] 1894 | ]; 1895 | 1896 | if ($progress->final_details) { 1897 | $data['data']['details'] = $progress->final_details; 1898 | } 1899 | 1900 | if ($progress->paths) { 1901 | $data['files'] = $progress->paths; 1902 | } 1903 | 1904 | return $data; 1905 | } 1906 | 1907 | /** 1908 | * @return object 1909 | */ 1910 | private function getProgressDetails() 1911 | { 1912 | return ( object ) [ 1913 | 'processed' => $this->cache->get($this->progressCacheKey), 1914 | 'quantity' => $this->cache->get($this->csvCountCacheKey), 1915 | 'message' => $this->cache->get($this->progressMessageKey), 1916 | 'finished' => $this->cache->get($this->progressFinishedKey), 1917 | 'details' => $this->cache->get($this->progressDetailsKey), 1918 | 'final_details' => $this->cache->get($this->progressFinalDetailsKey), 1919 | 'paths' => $this->cache->get($this->importPathsKey), 1920 | ]; 1921 | } 1922 | } 1923 | --------------------------------------------------------------------------------