yavor-ivanov/csv-importer

为Laravel提供的CSV导入/导出库

1.1.0 2017-04-26 11:05 UTC

This package is not auto-updated.

Last update: 2024-09-20 22:02:49 UTC


README

一个Laravel 4库,用于将CSV文件导入/导出到数据库表中,使用Eloquent。

注意:此库仍处于早期开发阶段。您可能会发现一些功能不够灵活,文档编写不佳。欢迎您提出问题,但由于我的日程安排,我可能无法及时回应。尽管如此,我还是将其开源了,因为没有一个Laravel CSV导入/导出包能够处理跨CSV引用。

功能

  • 自动导入/导出注册
  • CSV字段预处理
  • CSV字段验证
  • 跨CSV引用(关系)
  • 自动依赖解析
  • 错误回滚

要求

  • PHP >=5.4
  • Laravel框架 >= 4
  • Composer
  • Eloquent支持的关系型数据库管理系统(Laravel ORM)

安装

要安装,请运行:在您的laravel项目根目录下运行php composer.phar require yavor-ivanov/csv-importer

或者,您可以将"yavor-ivanov/csv-importer": "dev-master"手动添加到您的composer.json文件下的require字段。

然后,打开app/config/app.php,并在providers数组中添加以下行

'providers' => array(
    'YavorIvanov\CsvImporter\CsvImporterServiceProvider',
)

默认情况下,此包配置为在以下位置查找CSV文件:

  • app/csv/files
  • 导入器在app/csv/importers/
  • 导出器在app/csv/exporters/
  • 将CSV备份放在app/csv/files/backup/

您需要运行以下命令来创建所有这些文件夹

mkdir -p app/csv/files/backup
mkdir app/csv/importers
mkdir app/csv/exporters

示例

我已经设置了一个示例仓库,其中包含一个具有多个导入器和导出器的Laravel 4应用程序。

配置

导入器包默认配置为在app/csv/files/app/csv/importer(和导出器)文件夹中查找CSV文件和导入器/导出器脚本。如果您想更改此设置,您需要首先通过运行以下命令获取自己的(应用程序)配置副本:

php artisan config:publish yavor-ivanov/csv-importer

您将在app/config/packages/yavor-ivanov/csv-importer/config.php中获得默认配置的副本。对该文件的任何更改都将覆盖默认配置。

这是默认配置文件的外观:

    'import' => [
        'file_match' => '*Importer.php',
        'register_path' => '\\csv\\importers\\',
        'class_match_pattern' => '/^(?!CSV)(.*)Importer$/',
        'default_csv_path' => '/csv/files/',
    ],
    'export' => [
        'file_match' = '*Exporter.php',
        'register_path' => '\\csv\\exporters\\',
        'class_match_pattern' => '/^(?!CSV)(.*)Exporter$/',
        'default_csv_path' => '/csv/files/',
    ],

配置选项如下:

  • file_match - 匹配要考虑为导入器/导出器的文件名的正则表达式。包使用此来自动注册您的导入器/导出器类。
  • register_path - 从包含导入器/导出器的php脚本的位置包括php脚本(相对于app文件夹)
  • class_match_pattern - 包区分导入器/导出器与其他类的模式。用于自动导入器/导出器注册。
  • default_csv_path - 导入器/导出器查找CSV文件默认目录(相对于app文件夹)

命名规范

包为导入器/导出器建立了以下文件和类命名约定:

  • 所有导入器/导出器都必须放在指定的文件夹中(默认为app/csv/importersapp/csv/exporters)。此位置由register_path配置选项控制。
  • 默认情况下,PHP文件本身必须以Importer.php结尾(导出器为Exporter.php)。这也由file_match配置选项控制。
  • 文件中的类必须匹配 class_match_pattern。默认情况下,以 ImporterExporter 结尾的类名就足够有效。

作为这些限制的交换,该包能够自动注册遵循约定的任何导入器/导出器。这允许您

  • 创建导入器/导出器脚本,并立即在导入/导出命令中使用它们
  • 将导入器/导出器作为其他导入器/导出器的依赖项引用,并在运行时自动解决它们。
  • 获取所有已注册导入器/导出器的列表。

用法

命令

该包包含两个命令 csv:importcsv:export

命令格式是:csv:import <导入器名称> [<模式>]

示例用法:php artisan csv:import categoriesphp artisan csv:import expenses validate

导入器支持以下模式

  • append - 仅向表中添加 数据。不删除CSV中已删除的记录,也不更新已更改的记录。
  • overwrite - 删除表中的所有内容,并导入CSV。注意:覆盖模式不会传播到依赖项,因为这可能会导致整个数据库的级联删除。到目前为止,尚无法覆盖此行为。
  • update - 与 append 类似,但更新表中的记录。
  • validate - 检查CSV文件中的错误。不写入数据库。

mode 参数是可选的。如果不提供,导入器始终在 append 模式下运行。

注意:到目前为止,该包不支持在从CSV文件中删除记录时从数据库中删除记录,因为这可能会导致错误。将在以后的版本中添加一个 prune 模式,该模式专门执行此操作。目前,删除数据库记录的唯一方法是使用 overwrite 选项。

有关命令格式的更多信息,请运行 php artisan help csv:import

CSV格式

CSV格式的唯一要求是

  1. CSV必须包含标题行
  2. 标题中的所有列都必须命名
  3. 必须有至少一个唯一列
  4. 包含空格的单元格必须在CSV输出中引用

有效CSV的示例

id,role_name
1,"super admin"
2,admin
3,moderator
4,user

数据库表格式

为了使用缓存、更新模式和跨CSV引用,数据库表必须包含一个 csv_id 列。这用于查找对应CSV行的数据库记录。

模型

该包使用Laravel的Eloquent ORM来读取和写入数据库。这意味着不需要进行数据库访问配置。这也允许您在导入和导出时使用Eloquent功能(如观察者、验证器、自定义属性等)。

为了确保在导入器外部创建新记录时 csv_id 列自动增加,您必须在模型中使用 CSVReferenceTrait,如下所示

class UserRole extends Eloquent
{
    use YavorIvanov\CsvImporter\CSVReferenceTrait;

    protected $fillable = ['role_name'];
    protected $table = 'user_roles';

    // ...
}

CSVReferenceTrait 注册一个 save 钩子,为没有 csv_id 的模型设置正确的 csv_id

注意:可以 将导入到不在 $fillable 数组中列出的属性中,因为导入器在导入时关闭了Eloquent字段保护。(不用担心,完成后会重新保护它们。)

导入器

最小配置导入器示例

以下是需要创建导入器类所需的最小配置,该类创建一个 UserRole 模型的集合,并将其导入数据库,并支持 update 模式

<?php
use YavorIvanov\CsvImporter\CSVImporter;
class UserRolesImporter extends CSVImporter
{
    // Defualt name for the CSV to import.
    public $file = 'user_roles.csv';

    // Eloquent model name to create/update (case sensitive)
    protected $model = 'UserRole';

    // Maps an id field in the csv to a database id field. Format ['csv_column_name' => 'db_column_name']
    protected $primary_key = ['id' => 'csv_id'];

    // Maps an id field in the csv to a database id field. Format ['csv_column_name' => 'db_column_name']
    protected $cache_key = ['id' => 'csv_id'];

    // Maps csv columns to database columns. The importer uses these to automatically
    // import/update The format here is:
    //     csv_column_name => database_column_name
    // or
    //     csv_column_name
    // if both column names happen to be the same. For more information on the mapping format,
    // skip to the column mappings section of the documentation.
    protected $column_mapping = [
        ['csv_id' => 'id'],
        'role_name',
    ];
}

或者,您可以省略$column_mapping数组,自己定义导入和更新函数(您甚至可以将它们混合在一起)

<?php
use YavorIvanov\CsvImporter\CSVImporter;
class UserRolesImporter extends CSVImporter
{
    // Defualt name for the CSV to import.
    public $file = 'user_roles.csv';

    // Eloquent model name to create/update (case sensitive)
    protected $model = 'UserRole';

    // Maps an id field in the csv to a database id field. Format ['csv_column_name' => 'db_column_name']
    protected $primary_key = ['id' => 'csv_id'];

    // Maps an id field in the csv to a database id field. Format ['csv_column_name' => 'db_column_name']
    protected $cache_key = ['id' => 'csv_id'];

    protected function update($row, $o)
    {
        $o->csv_id = $row['id'];
        $o->role_name = $row['role_name'];
        $o->save();
    }

    protected function import_row($row)
    {
        return UserRole::create([
            'role_name' => $row['role_name'],
            'csv_id' => $row['id'],
        ]);
    }
}

每个导入器名称必须以Importer结尾(由class_match_pattern属性控制)并扩展CSVImporter基类。

字段拆分

  • $file - 要从app/csv/files/(csv文件夹可配置)中加载的文件名
  • $model - 此包使用Eloquent ORM从数据库加载和保存。为了调用选择和保存函数,该包需要知道导入器对应的模型。
  • $cache_key - 为了减少对自引用CSV文件、更新模式导入以及跳过重复导入的数据库查询次数,该包会缓存已存在于数据库中的实体以及正在导入的实体。为了做到这一点,该包需要知道CSV的主唯一列与数据库表之间的映射。
  • $primary_key - 所有导入器共享一个context,您可以从其中检索依赖导入器的实体。这与外键的工作方式完全相同,即:CSV A通过某个唯一列引用CSV B中的一行。$primary_key是(CSV)唯一列与数据库表唯一列之间的映射。默认情况下,CSV id列命名为id,而表列名为csv_id

注意:导入器不能使用表中的(更确切地说是默认的)id列进行比较,因为在向非空表追加时可能会发生id冲突。

  • import_row - 一旦导入器比较数据库表和CSV,它将遍历CSV文件中的行。对于找到的每一行,都会调用import_row函数。基导入器通过$row参数传递当前行,并期望返回Eloquent模型。

注意:目前,导入器不支持行条件导入。import_row函数必须返回模型实例。

  • update - 基导入器为数据库表和CSV中找到的每个记录调用此函数。导入器通过$row参数传递当前CSV行,以及在$o参数中传递数据库中的Eloquent模型。

注意:目前,导入器不会检查模型的实际更改。它将始终调用该函数。由于这个当前的限制,您必须手动在您的模型上调用save()

注意:此函数仅在以update模式运行导入器时调用。

列映射

通常,数据库表列的名称(或表示形式)与您希望导入的CSV文件不同。例如,数据库接受ISO 8601格式的日期(YYY-MM-DD),但您的CSV文件可能包含美式日期格式(MM/DD/YYYY)。$column_mapping属性允许您定义CSV和数据库表之间的任何名称差异,以及在保存之前转换和验证CSV数据。

$column_mapping格式灵活。它允许您定义具有名称差异的列、多个预处理步骤以及验证函数。

protected $column_mapping = [
    'csv_column' => ['name' => 'table_column',
                          'processors' => ['processor_name' => 'parameter'],
                          'validators' => ['validator_name' => 'parameter']
                ],
];

$column_mapping还用于导入器在未定义import_rowupdate函数时。它通过在csv_column上运行验证器/处理器函数来执行导入,并将结果保存到指定的table_columntable_column也可以是模型属性或模型函数。

protected $column_mapping = [
    'csv_column' => 'modelPropertyName',
];

注意:为了将属性分配给模型,导入器使用table_column键作为eval()调用的一部分。调用仅限于当前模型实例,但没有任何检查恶意意图,例如调用delete()或使用id; call_malicious_function(); $variable_name作为键。

当导入器读取CSV文件时,它会查看$column_mapping以确定是否需要转换输入数据(或对其进行验证)。

处理器函数从get_processors函数的结果中读取。

protected function get_processors()
{
    return [
            'integer' => function ($v) { return intval($v); },

            'to_datetime' => function ($v, $fmt='d/m/y H:i')
            {
                $created_at = DateTime::createFromFormat($fmt, $v);
                return $created_at->format('Y-m-d H:i:s');
            },
    ];
}

预处理器的函数名称由返回的数组键确定。

验证器由get_validators函数返回。

为了简洁起见,您可以从列规范中省略未使用的功能。例如,只更改名称的列导入可以简化为以下内容

protected $column_mapping = [
    'csv_column' => 'table_column',
];

此示例的另一个例子是使用预处理程序而不传递参数

protected $column_mapping = [
    'csv_column' => ['name' => 'table_column',
                    'processors' => ['processor1', 'processor2'],
                ],
];

或者如果只有一个处理器,则省略数组

protected $column_mapping = [
    'csv_column' => ['name' => 'table_column',
                    'processors' => 'my_column_processor',
                ],
];

添加依赖项并引用导入器

通常情况下,CSV文件中的数据希望具有关系型。为了正确解决关系,导入器需要按正确的顺序导入文件。例如,如果users.csv引用了来自phone_numbers.csv的电话号码,导入器应确保在导入用户之前导入电话号码,以及获取可能存在于数据库中但不在phone_numbers.csv文件中的任何电话号码。

该包通过读取静态$deps属性中定义的每个导入器的依赖项来评估导入顺序。这个依赖项集合形成一个依赖图,可以通过拓扑排序来产生包必须调用导入器的顺序。

每次您从命令行运行具有依赖项的导入器时,您将看到导入器及其依赖项的进度条。

Importing: Book.
 6/6 [============================] 100%
 7/7 [============================] 100%

在这里,Book导入器依赖于Author导入器。到目前为止,进度条尚未标记。

除了声明依赖项之外,导入器还需要访问其依赖项的实体数据。在图书和作者示例中,图书导入器可能希望找到其作者的id并将其用作外键。

get_from_context函数通过依赖项名称和搜索值返回Eloquent模型实例:protected function get_from_context($ctx, $key)

要选择的列由依赖导入器的$primary_key属性确定。如果没有定义$primary_key,则使用$cache_key映射。这允许您通过一个列(最常见的是id)缓存模型,但通过其他导入器的另一个(唯一)列来引用它。一个例子是通过id缓存用户模型,但通过订单的email来引用它。

缓存

在导入开始时,该包选择导入器$model表的所有行,并通过唯一的CSV列在$cache_key映射中进行缓存。

protected $cache_key = ['table_column_name' => 'csv_column_name'];

这允许包避免导入数据库中已存在的CSV行。可以通过比较在$cache_key映射中定义的唯一键的值,来检查每个CSV记录是否包含在数据库中。例如,id <--> csv_id的映射会在缓存中搜索一个具有与当前CSV行csv_id相同的id的Eloquent实体。

除了缓存的性能优势之外,你还可以通过调用'get_from_cache($hash)'在导入器中查询缓存。这在导入自引用CSV时非常有用。一个例子是将以下格式的树结构导入:[id, name, parent_id],其中每个分支在其名称前添加其父级的名称。

添加预处理器函数

您可以通过添加一个返回函数数组的get_processors()函数来为您的导入器定义处理器函数。包将在$column_mapping中列出这些处理器的列上运行这些函数。

您可以直接定义处理器函数

protected function get_processors()
{
    return [
        'null_or_datetime' => function ($v, $fmt='Y-m-d')
        {
            $v = $this->process('string_to_null', $v);
            if ($v == Null)
                return $v;
            return $this->process('to_datetime', [$v, $fmt]);
        },
    ];
}

或者,如果您希望它们在导入器之间共享,可以在共享的文件中定义它们,并引用它们

protected function get_processors()
{
    return [
        'null_or_datetime' => my_datetime_function
    ];
}

基本导入器也定义了自己的处理器,这些处理器可以被所有导入器使用,就像它们被继承一样

注意:目前没有方法可以像基本导入器那样注册全局预处理器。

添加验证函数

验证函数的定义方式与预处理器类似。导入器的get_validators函数返回一个函数数组,这些函数可以在导入器通过$column_mapping属性运行时定义以运行。

以下是一个检查列唯一性的示例验证器

protected function get_validators()
{
    return [
        'unique' => function ($col, $row)
        {
            $val = $row[$col];
            $current_obj = $this->get_from_cache($row);
            $model_col = array_get($this->column_mapping, "$col.name", $col);
            $occurrences = $this->cache->reduce(function($carry, $o) use ($current_obj, $model_col, $val) {
                if ($o != $current_obj && strtolower($o->$model_col) == strtolower($val))
                    return $carry + 1;
                return $carry;
            }, 0);

            if ($occurrences > 0)
            {
                Log::error("A $this->name with $col = $val already exists in $this->file.");
                die;
            }
        },
    ];
}

验证函数接收它们被调用的列名和整个CSV行。这允许您创建如下验证规则:属性X只有在Y为NULL时才有效

列旋转

有时您的旋转CSV表与数据库旋转表非常相似

其他时候,您的多对多旋转CSV可能以奇怪的多个列格式出现

虽然将CSV格式调整为与数据库表布局一致是理想的,但您并不总是有这个便利(例如广泛使用的遗留格式)。

在这种情况下,您可以在导入/更新函数读取之前对单个行进行一些预处理。

在下面的示例中,pivot_row函数将具有[book_id, genre1, genre2, ... genreN]格式的行替换为多个[book_id, genre_id]元组的行

protected function pivot_row($row)
{
    $pivoted_row = [];
    $book_id = $row['book'];

    // Loops over the genre columns only, as there is only one book column.
    foreach (array_filter(array_slice($row, 1)) as $genre_id)
    {
        array_push($pivoted_row, [
            'book_id'  => $book_id,
            'genre_id' => $genre_id,
        ]);
    }
    return $pivoted_row;
}

旋转步骤在列处理器和验证器之前运行。

导出器

最小配置导出器示例

以下是一个使用$column_mapping驱动CSV导出的最小导出器示例

<?php
use YavorIvanov\CsvImporter\CSVExporter;
class UserRolesImporter extends CSVExporter
{
    // Defualt name for the CSV to export.
    public $file = 'user_roles.csv';

    // Eloquent model to select from (case sensitive)
    protected $model = 'UserRole';

    protected $column_mapping = [
        'csv_id' => 'id',
        'role_name',
    ];
}

注意:每个导出器类的名称都必须以Exporter结尾(由class_match_pattern属性控制),并扩展CSVExporter基类。

字段拆分

  • $file - 要保存的文件名。默认文件路径为app/csv/files/(csv文件夹可配置)
  • $model - 包使用Eloquent ORM从数据库中读取数据。为了选择记录,包需要知道导出器对应的模型。

列映射

导出器的$column_mapping属性与导入器的$column_mapping具有相同的目的。它允许您定义表列(或模型属性/方法)与CSV列之间的关系。导出器使用此映射来生成CSV输出。

与导入器类似,导出器 $column_mapping 允许您使用模型属性,以及将模型函数映射到 CSV 列。以下是一个使用后处理函数将模型属性与 CSV 列进行映射的示例

protected $column_mapping = [
    'model_property' => ['name' => 'csv_column', 'processors' => ['postprocessor_name' => 'parameter']],
];

以下是一个将模型函数映射到 CSV 列的导出器示例

protected $column_mapping = [
    'compute_property()' => ['name' => 'csv_column_name', 'processors' => ['postprocessor_name' => 'parameter']],
];

导出通常比导入更简单,因此无需使用 export_row 等函数 (尽管该选项可用)。导出器可以读取 $column_mapping 并自动输出 CSV 文件。

为了支持某些映射,导出器将评估 $column_mapping 条目的键(如上例中的 model_property)并在导出时将结果用作 csv_column 的值。此类映射的示例包括:计算属性、聚合函数和关系属性。

例如,书籍导出器示例 使用此类映射来获取其 authors 关系的 CSV ID

    protected $column_mapping = [
        ['authors()->first()->csv_id' => 'author'],
        // ...
    ];

注意:导出器使用 $column_mapping 键作为 eval() 调用的一部分。该调用仅限于当前模型实例,但没有任何恶意意图的检查,例如调用 delete() 或将 id; call_malicious_function() 作为键。

与导入器 $column_mapping 属性类似,导出器允许您在不需要使用后处理器或 CSV 和数据库列相同的情况下简化行声明

protected $column_mapping = [
    'name',                                                                                      // Column name in the CSV and database is the same
    ['table_column' => 'csv_column'],                                                            // Table column to CSV column mapping with no postprocessor
    ['table_column' => ['name' => 'csv_column', 'processors' => ['processor_name']]],             // Post-processor without paramers (use defaults).
    ['table_column' => ['name' => 'csv_column', 'processors' => ['processor_name' => 'param']]], // Post-processor with parameters.
    ['table_column' => ['name' => 'csv_column', 'processors' => [
        'processor1' => ['param1', 'param2'],
        'processor2' => 'param',
        'processor3']
    ]]],                                                                                         // Multiple post-processors with a differing number of parameters.
];

后处理

与导入器一样,该软件包从 get_processors 函数的结果中读取后处理函数

protected function get_processors()
{
    return [
            'null_to_zero' => function ($v)
            {
                if ($v == Null)
                    return 0;
                return $v;
            },
    ];
}

后处理函数的名称由 get_processors 返回的数组键确定。

程序生成行

有时,列映射不足以处理您的导出逻辑。在这种情况下,您可以使用 generate_row 函数来程序化生成行。您可能希望这样做的情况包括:导出具有可变列数的 CSV 文件、导出包含多个模型数据的 CSV、导出模型外的数据等。

导出过程开始后,导出器将从 $model 实体中选择所有记录,并对每条记录调用 generate_row。该函数应返回以下格式的数组:['csv_column1' => 'value', 'csv_column2' => 'other_vaule']

以下是一个导出具有可变列数的 CSV 的示例。您可以在示例中看到完整的代码

protected function generate_row($o)
{
    $row = parent::generate_row($o);
    $heading = 'genre';
    $current = 1;
    foreach ($o->genres as $genre)
    {
        $col_name = $heading . $current;
        $current += 1;
        $row[$col_name] = $genre->csv_id;
    }
    return $row;
}

注意:如果您选择重写此函数,则导出器不会处理 $column_mapping 属性,除非您调用 parent::generate_row()。这允许您在需要的情况下混合自定义行生成逻辑与列映射(或完全跳过自动映射)。

MIT 协议许可

项目许可文件可在 此处 找到。