├── .phpcs.xml
├── LICENSE.md
├── composer.json
├── phpstan.neon
├── readme.md
└── src
├── CsvSeeder.php
└── CsvSeederServiceProvider.php
/.phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | A custom set of code standard rules
4 |
5 |
6 |
7 |
8 |
12 | */tests/*\.php
13 |
14 |
15 |
16 |
19 | */tests/migrations/*\.php
20 |
21 |
22 | src
23 |
24 | */vendor/*
25 | */coverage/*
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (C) 2013 Mior Muhammad Zaki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flynsarmy/csv-seeder",
3 | "description": "Allows seeding of the database with CSV files",
4 | "keywords": ["laravel", "csv", "seed", "seeds", "seeding"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Flyn San",
9 | "email": "flynsarmy@gmail.com"
10 | }
11 | ],
12 | "require": {
13 | "php": ">=7.4",
14 | "illuminate/support": ">=4.1.0"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "Flynsarmy\\CsvSeeder\\": "src/"
19 | }
20 | },
21 | "autoload-dev": {
22 | "psr-4": {
23 | "Flynsarmy\\CsvSeeder\\Tests\\": "tests"
24 | }
25 | },
26 | "extra": {
27 | "laravel": {
28 | "providers": [
29 | "Flynsarmy\\CsvSeeder\\CsvSeederServiceProvider"
30 | ]
31 | }
32 | },
33 | "minimum-stability": "dev",
34 | "require-dev": {
35 | "orchestra/testbench": "6.*",
36 | "squizlabs/php_codesniffer": "3.*",
37 | "nunomaduro/larastan": "^0.6.0@dev"
38 | },
39 | "scripts": {
40 | "phpstan": "php -d memory_limit=-1 ./vendor/bin/phpstan analyse",
41 | "phpcbf": "vendor/bin/phpcbf --standard=./.phpcs.xml ./",
42 | "phpcs": "vendor/bin/phpcs -s --standard=./.phpcs.xml ./",
43 | "phpunit": "vendor/bin/phpunit ./tests",
44 | "coverage": "vendor/bin/phpunit tests --coverage-html coverage --whitelist src/",
45 | "lint": "vendor/bin/parallel-lint --exclude vendor .",
46 | "test": [
47 | "composer validate --strict",
48 | "@phpcs",
49 | "@phpstan",
50 | "@phpunit"
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/nunomaduro/larastan/extension.neon
3 | parameters:
4 | level: 5
5 | inferPrivatePropertyTypeFromConstructor: true
6 | paths:
7 | - src
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## CSV Seeder
2 |
3 | [](https://packagist.org/packages/flynsarmy/csv-seeder)
4 | [](LICENSE.md)
5 | 
6 | [](https://scrutinizer-ci.com/g/flynsarmy/laravel-csv-seeder)
7 | [](https://packagist.org/packages/flynsarmy/csv-seeder)
8 |
9 |
10 | ### Seed your database with CSV files
11 |
12 | This package allows CSV based seeds.
13 |
14 |
15 | ### Installation
16 |
17 | Require this package in your composer.json and run composer update (or run `composer require flynsarmy/csv-seeder:2.*` directly):
18 |
19 | **For PHP 7.4+**
20 |
21 | ```json
22 | "flynsarmy/csv-seeder": "2.0.*"
23 | ```
24 |
25 | **For older PHP versions**
26 |
27 | ```json
28 | "flynsarmy/csv-seeder": "1.*"
29 | ```
30 |
31 | ### Usage
32 |
33 | Your CSV's header row should match the DB columns you wish to import. IE to import *id* and *name* columns, your CSV should look like:
34 |
35 | ```csv
36 | id,name
37 | 1,Foo
38 | 2,Bar
39 | ```
40 |
41 | Seed classes must extend `Flynsarmy\CsvSeeder\CsvSeeder`, they must define the destination database table and CSV file path, and finally they must call `parent::run()` like so:
42 |
43 | ```php
44 | use Flynsarmy\CsvSeeder\CsvSeeder;
45 |
46 | class StopsTableSeeder extends CsvSeeder {
47 |
48 | public function __construct()
49 | {
50 | $this->table = 'your_table';
51 | $this->filename = base_path().'/database/seeds/csvs/your_csv.csv';
52 | }
53 |
54 | public function run()
55 | {
56 | // Recommended when importing larger CSVs
57 | DB::disableQueryLog();
58 |
59 | // Uncomment the below to wipe the table clean before populating
60 | DB::table($this->table)->truncate();
61 |
62 | parent::run();
63 | }
64 | }
65 | ```
66 |
67 | Drop your CSV into */database/seeds/csvs/your_csv.csv* or whatever path you specify in your constructor above.
68 |
69 | ### Configuration
70 |
71 | In addition to setting the database table and CSV filename, the following configuration options are available. They can be set in your class constructor:
72 |
73 | - `connection` (string '') Connection to use for inserts. Leave empty for default connection.
74 | - `insert_chunk_size` (int 500) An SQL insert statement will trigger every `insert_chunk_size` number of rows while reading the CSV
75 | - `csv_delimiter` (string ,) The CSV field delimiter.
76 | - `hashable` (array [password]) List of fields to be hashed before import, useful if you are importing users and need their passwords hashed. Uses `Hash::make()`. Note: This is EXTREMELY SLOW. If you have a lot of rows in your CSV your import will take quite a long time.
77 | - `offset_rows` (int 0) How many rows at the start of the CSV to ignore. Warning: If used, you probably want to set a mapping as your header row in the CSV will be skipped.
78 | - `mapping` (array []) Associative array of csvCol => dbCol. See examples section for details. If not specified, the first row (after offset) of the CSV will be used as the mapping.
79 | - `should_trim` (bool false) Whether to trim the data in each cell of the CSV during import.
80 | - `timestamps` (bool false) Whether or not to add *created_at* and *updated_at* columns on import.
81 | - `created_at` (string current time in ISO 8601 format) Only used if `timestamps` is `true`
82 | - `updated_at` (string current time in ISO 8601 format) Only used if `timestamps` is `true`
83 |
84 |
85 | ### Examples
86 | CSV with pipe delimited values:
87 |
88 | ```php
89 | public function __construct()
90 | {
91 | $this->table = 'users';
92 | $this->csv_delimiter = '|';
93 | $this->filename = base_path().'/database/seeds/csvs/your_csv.csv';
94 | }
95 | ```
96 |
97 | Specifying which CSV columns to import:
98 |
99 | ```php
100 | public function __construct()
101 | {
102 | $this->table = 'users';
103 | $this->csv_delimiter = '|';
104 | $this->filename = base_path().'/database/seeds/csvs/your_csv.csv';
105 | $this->mapping = [
106 | 0 => 'first_name',
107 | 1 => 'last_name',
108 | 5 => 'age',
109 | ];
110 | }
111 | ```
112 |
113 | Trimming the whitespace from the imported data:
114 |
115 | ```php
116 | public function __construct()
117 | {
118 | $this->table = 'users';
119 | $this->csv_delimiter = '|';
120 | $this->filename = base_path().'/database/seeds/csvs/your_csv.csv';
121 | $this->mapping = [
122 | 0 => 'first_name',
123 | 1 => 'last_name',
124 | 5 => 'age',
125 | ];
126 | $this->should_trim = true;
127 | }
128 | ```
129 |
130 | Skipping the CSV header row (Note: A mapping is required if this is done):
131 |
132 | ```php
133 | public function __construct()
134 | {
135 | $this->table = 'users';
136 | $this->csv_delimiter = '|';
137 | $this->filename = base_path().'/database/seeds/csvs/your_csv.csv';
138 | $this->offset_rows = 1;
139 | $this->mapping = [
140 | 0 => 'first_name',
141 | 1 => 'last_name',
142 | 2 => 'password',
143 | ];
144 | $this->should_trim = true;
145 | }
146 | ```
147 |
148 | Specifying the DB connection to use:
149 |
150 | ```php
151 | public function __construct()
152 | {
153 | $this->table = 'users';
154 | $this->connection = 'my_connection';
155 | $this->filename = base_path().'/database/seeds/csvs/your_csv.csv';
156 | }
157 | ```
158 |
159 | ### Migration Guide
160 |
161 | #### 2.0
162 |
163 | - `$seeder->hashable` is now an `array` of columns rather than a single column name. Wrap your old string value in `[]`.
164 |
165 | ### License
166 |
167 | CsvSeeder is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)
168 |
--------------------------------------------------------------------------------
/src/CsvSeeder.php:
--------------------------------------------------------------------------------
1 | timestamps is true
73 | */
74 | public string $created_at = '';
75 | public string $updated_at = '';
76 |
77 | /**
78 | * The mapping of CSV to DB column. If not specified manually, the first
79 | * row (after offset_rows) of your CSV will be read as your DB columns.
80 | *
81 | * Mappings take the form of csvColNumber => dbColName.
82 | *
83 | * IE to read the first, third and fourth columns of your CSV only, use:
84 | * array(
85 | * 0 => id,
86 | * 2 => name,
87 | * 3 => description,
88 | * )
89 | */
90 | public array $mapping = [];
91 |
92 |
93 | /**
94 | * Run DB seed
95 | */
96 | public function run()
97 | {
98 | // Cache created_at and updated_at if we need to
99 | if ($this->timestamps) {
100 | if (!$this->created_at) {
101 | $this->created_at = Carbon::now()->toIso8601String();
102 | }
103 | if (!$this->updated_at) {
104 | $this->updated_at = Carbon::now()->toIso8601String();
105 | }
106 | }
107 |
108 | $this->seedFromCSV($this->filename, $this->csv_delimiter);
109 | }
110 |
111 | /**
112 | * Strip UTF-8 BOM characters from the start of a string
113 | *
114 | * @param string $text
115 | * @return string String with BOM stripped
116 | */
117 | public function stripUtf8Bom(string $text): string
118 | {
119 | $bom = pack('H*', 'EFBBBF');
120 | $text = preg_replace("/^$bom/", '', $text);
121 |
122 | return $text;
123 | }
124 |
125 | /**
126 | * Opens a CSV file and returns it as a resource
127 | *
128 | * @param string $filename
129 | * @return FALSE|resource
130 | */
131 | public function openCSV(string $filename)
132 | {
133 | if (!file_exists($filename) || !is_readable($filename)) {
134 | Log::error("CSV insert failed: CSV " . $filename . " does not exist or is not readable.");
135 | return false;
136 | }
137 |
138 | // check if file is gzipped
139 | $finfo = finfo_open(FILEINFO_MIME_TYPE);
140 | $file_mime_type = finfo_file($finfo, $filename);
141 | finfo_close($finfo);
142 | $gzipped = strcmp($file_mime_type, "application/x-gzip") == 0;
143 |
144 | $handle = $gzipped ? gzopen($filename, 'r') : fopen($filename, 'r');
145 |
146 | return $handle;
147 | }
148 |
149 | /**
150 | * Reads all rows of a given CSV and imports the data.
151 | *
152 | * @param string $filename
153 | * @param string $deliminator
154 | * @return bool Whether or not the import completed successfully.
155 | * @throws Exception
156 | */
157 | public function seedFromCSV(string $filename, string $deliminator = ","): bool
158 | {
159 | $handle = $this->openCSV($filename);
160 |
161 | // CSV doesn't exist or couldn't be read from.
162 | if ($handle === false) {
163 | throw new Exception("CSV insert failed: CSV " . $filename . " does not exist or is not readable.");
164 | }
165 |
166 | $success = true;
167 | $row_count = 0;
168 | $data = [];
169 | $mapping = $this->mapping ?: [];
170 | $offset = $this->offset_rows;
171 |
172 | if ($mapping) {
173 | $this->hashable = $this->removeUnusedHashColumns($mapping);
174 | }
175 |
176 | while (($row = fgetcsv($handle, 0, $deliminator)) !== false) {
177 | // Offset the specified number of rows
178 |
179 | while ($offset-- > 0) {
180 | continue 2;
181 | }
182 |
183 | // No mapping specified - the first row will be used as the mapping
184 | // ie it's a CSV title row. This row won't be inserted into the DB.
185 | if (!$mapping) {
186 | $mapping = $this->createMappingFromRow($row);
187 | $this->hashable = $this->removeUnusedHashColumns($mapping);
188 | continue;
189 | }
190 |
191 | $row = $this->readRow($row, $mapping);
192 |
193 | // insert only non-empty rows from the csv file
194 | if (empty($row)) {
195 | continue;
196 | }
197 |
198 | $data[$row_count] = $row;
199 |
200 | // Chunk size reached, insert
201 | if (++$row_count == $this->insert_chunk_size) {
202 | $success = $success && $this->insert($data);
203 | $row_count = 0;
204 | // clear the data array explicitly when it was inserted so
205 | // that nothing is left, otherwise a leftover scenario can
206 | // cause duplicate inserts
207 | $data = [];
208 | }
209 | }
210 |
211 | // Insert any leftover rows
212 | //check if the data array explicitly if there are any values left to be inserted, if insert them
213 | if (count($data)) {
214 | $success = $success && $this->insert($data);
215 | }
216 |
217 | fclose($handle);
218 |
219 | return $success;
220 | }
221 |
222 | /**
223 | * Creates a CSV->DB column mapping from the given CSV row.
224 | *
225 | * @param array $row List of DB columns to insert into
226 | * @return array List of DB fields to insert into
227 | */
228 | public function createMappingFromRow(array $row): array
229 | {
230 | $mapping = $row;
231 | $mapping[0] = $this->stripUtf8Bom($mapping[0]);
232 |
233 | // skip csv columns that don't exist in the database
234 | foreach ($mapping as $index => $fieldname) {
235 | if (!DB::connection($this->connection)->getSchemaBuilder()->hasColumn($this->table, $fieldname)) {
236 | if (isset($mapping[$index])) {
237 | unset($mapping[$index]);
238 | }
239 | }
240 | }
241 |
242 | return $mapping;
243 | }
244 |
245 | /**
246 | * Removes fields from the hashable array that don't exist in our mapping.
247 | *
248 | * This function acts as a performance enhancement - we don't want
249 | * to search for hashable columns on every row imported when we already
250 | * know they don't exist.
251 | *
252 | * @param array $mapping
253 | * @return array
254 | */
255 | public function removeUnusedHashColumns(array $mapping)
256 | {
257 | $hashables = $this->hashable;
258 |
259 | foreach ($hashables as $key => $field) {
260 | if (!in_array($field, $mapping)) {
261 | unset($hashables[$key]);
262 | }
263 | }
264 |
265 | return $hashables;
266 | }
267 |
268 | /**
269 | * Read a CSV row into a DB insertable array
270 | *
271 | * @param array $row A row of data to read
272 | * @param array $mapping Array of csvCol => dbCol
273 | * @return array
274 | */
275 | public function readRow(array $row, array $mapping): array
276 | {
277 | $row_values = [];
278 |
279 | foreach ($mapping as $csvCol => $dbCol) {
280 | if (!isset($row[$csvCol]) || $row[$csvCol] === '') {
281 | $row_values[$dbCol] = null;
282 | } else {
283 | $row_values[$dbCol] = $this->should_trim ? trim($row[$csvCol]) : $row[$csvCol];
284 | }
285 | }
286 |
287 | if (!empty($this->hashable)) {
288 | foreach ($this->hashable as $columnToHash) {
289 | if (isset($row_values[$columnToHash])) {
290 | $row_values[$columnToHash] = Hash::make($row_values[$columnToHash]);
291 | }
292 | }
293 | }
294 |
295 | if ($this->timestamps) {
296 | $row_values['created_at'] = $this->created_at;
297 | $row_values['updated_at'] = $this->updated_at;
298 | }
299 |
300 | return $row_values;
301 | }
302 |
303 | /**
304 | * Seed a given set of data to the DB
305 | *
306 | * @param array $seedData
307 | * @return bool TRUE on success else FALSE
308 | */
309 | public function insert(array $seedData): bool
310 | {
311 | try {
312 | DB::connection($this->connection)->table($this->table)->insert($seedData);
313 | } catch (\Exception $e) {
314 | Log::error("CSV insert failed: " . $e->getMessage() . " - CSV " . $this->filename);
315 | return false;
316 | }
317 |
318 | return true;
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/src/CsvSeederServiceProvider.php:
--------------------------------------------------------------------------------
1 |