ttree/contentrepositoryimporter

帮助导入 Neos 内容存储数据的辅助包

4.1.3 2022-05-04 14:56 UTC

README

本包包含通用的工具,用于帮助导入 Neos 内容存储中的数据。

包含哪些内容?

  • 命令控制器(CLI),用于启动导入预设
  • 基于简单的约定
  • DataProvider:用于从外部源准备和清理数据
  • 导入器:从 DataProvider 获取数据并将所有内容推送到 CR
  • DataType:用于清理值并在 DataProvider 之间共享代码的简单对象
  • 将导入分割成多个子命令,以避免高内存使用
  • 没有太大的魔法,您总是可以通过覆盖默认配置和方法来控制

基本 DataProvider

每个数据提供者都必须扩展 DataProvider 抽象类或实现 DataProviderInterface 接口。检查抽象数据提供者的源代码,那里有一些有用的东西可以探索。

当您从外部源处理数据时,更新 count 属性很重要。在处理过程中,您可以决定跳过一些数据(无效数据、缺失值等),因此我们不能使用 SQL count 功能。

尽量在数据提供者中做大部分的数据清理,以便数据到达导入器时已经准备好插入。基本上,提供者构建的数组应该包含与您的节点类型属性名匹配的属性名。如果您需要传输不会匹配节点属性的值,请用 '_' 前缀。

有一些魔法值,这些值必须位于数组的顶层

  • __identifier(可选)此 UUID 将用于导入的节点,您应该使用 AbstractImporter::applyProperties 来启用此功能,默认使用
  • __externalIdentifier(必需)数据的对外标识符,这一点非常重要。该包跟踪导入的数据
  • __label(必需)此记录的标签,由导入器主要用于记录(此值不导入,但有助于跟踪过程)。如果您运行两次相同的导入,导入的节点将被更新而不是创建。

提示:如果您的节点属性不在数组的顶层,您可以覆盖 AbstractImporter::getPropertiesFromDataProviderPayload 方法

提供者的输出

您的提供者应该输出类似以下内容

	[
		'__label' => 'The external content lable, for internal use'
		'__externalIdentifier' => 'The external external identifier, for internal use'
		'title' => 'My title'
		'year' => 1999
		'text' => '...'
	]

提示:如果您的提供者不返回数组,您必须注册一个 TypeConverter 来将其转换为数组。属性映射器由导入器自动使用。

内容维度支持

如果您的数据提供者遵循此约定,导入器可以自动创建您的节点变体

	[
		'__label' => 'The external content lable, for internal use'
		'__externalIdentifier' => 'The external external identifier, for internal use'
		'title' => 'My title'
		'year' => 1999
		'text' => '...',
                       
	    '@dimensions' => [
		   '@en' => [
			   '@strategy' => 'merge',
			   'title' => '...',
		   ],
		   '@fr' => [
			   '@strategy' => 'merge',
			   'title' => '...',
		   ],
	    ]
	]

@en 是预设名称,您必须在 Settings.yaml 中配置预设

Ttree:
  ContentRepositoryImporter:
    dimensionsImporter:
      presets:
        fr:
	  language: ['fr', 'en', 'de']
        en:
	  language: ['en', 'de']
        de:
	  language: ['de']

在预设部分之间共享数据

您可以将导入操作分成多个部分。每个部分会在单独的请求中执行。有时,在不同部分之间共享数据是有用的(例如,在第一部分中导入分类,在第二部分中映射文档与分类)。这些解决方案可以解决此用例,我们集成了一个名为保险库的功能。保险库简单来说是一个缓存,可以在导入器和数据提供者中通过调用$this->vault->set($key, $name)$this->vault->get($key)来访问。当前的预设是命名空间,因此您可以使用简单的键,如名称、ID等。

如果您调用flow import:init --preset your-preset,缓存将被刷新。

基本提供者

class BasicDataProvider extends DataProvider {

	/**
	 * @return array
	 */
	public function fetch() {
		$result = [];
		$query = $this->createQuery()
			->select('*')
			->from('demo_table', 'd')
			->orderBy('d.name');

		$statement = $query->execute();
		while ($demoRecord = $statement->fetch()) {
			$result[] = [
				'__externalIdentifier' => (integer)$demoRecord['id'],
				'name' => String::create($demoRecord['name'])->getValue()
			];
		}

		$this->count = count($result);

		return $result;
	}

}

一个基本的导入器

每个数据导入器都必须扩展抽象类AbstractImporter或实现接口ImporterInterface

processRecord方法中,您将处理每条记录的处理,例如为每个传入的数据记录创建内容库节点。

不要忘记使用registerNodeProcessing注册已处理的节点。该方法将处理诸如日志记录和跟踪导入节点等功能,以决定是否需要创建或更新本地节点。

class ProductImporter extends AbstractImporter
{

    /**
     * @var string
     */
    protected $externalIdentifierDataKey = 'productNumber';

    /**
     * @var string
     */
    protected $labelDataKey = 'properties.name';

    /**
     * @var string
     */
    protected $nodeNamePrefix = 'product-';

    /**
     * @var string
     */
    protected $nodeTypeName = 'Acme.Demo:Product';

    /**
     * Starts batch processing all commands
     *
     * @return void
     * @api
     */
    public function process()
    {
        $this->initializeStorageNode('shop/products', 'products', 'Products', 'products');
        $this->initializeNodeTemplates();

        $nodeTemplate = new NodeTemplate();
        $this->processBatch($nodeTemplate);
    }

}

一个基本的预设

您可以在Settings.yaml中配置导入预设。预设分为多个部分。如果您使用batchSize,当前部分将通过子CLI请求批量执行。这可以解决大型导入的内存或性能问题。

Ttree:
  ContentRepositoryImporter:
    sources:
      default:
        host: localhost
        driver: pdo_mysql
        dbname: database
        user: user
        password: password
      extraSourceDatabase:
        host: localhost
        driver: pdo_mysql
        dbname: database
        user: user
        password: password

    presets:
      'base':
        parts:
          'news':
            label: 'News Import'
            dataProviderClassName: 'Your\Package\Importer\DataProvider\NewsDataProvider'
            importerClassName: 'Your\Package\Importer\Importer\NewsImporter'
          'page':
            label: 'Page Import'
            dataProviderClassName: 'Your\Package\Importer\DataProvider\PageDataProvider'
            dataProviderOptions:
              source: 'extraSourceDatabase'
              someOption: 'Some option that will be available in the options property of the data provider'
            importerClassName: 'Your\Package\Importer\Importer\PageImporter'
            importerOptions:
              siteNodePath: '/sites/my-site'
              someOption: 'Some option that will be available in the options property of the importer'
            batchSize': 120

          'pageContent':
            label: 'Page Content Import'
            dataProviderClassName: 'Your\Package\Importer\DataProvider\PageContentDataProvider'
            importerClassName: 'Your\Package\Importer\Importer\PageContentImporter'
            batchSize: 120

开始您的导入过程

提示:不要忘记从您进行导入的包中引入此包,以确保正确的加载顺序,这样设置就会被正确覆盖。

从CLI

flow import:batch --preset base

您还可以筛选预设步骤

flow import:batch --preset base --parts page,pageContent

为了测试目的或如果您想覆盖预设中定义的值,您还可以指定在隔离的子进程中一次应导入的记录数

flow import:batch --preset base --batch-size 50

传递超过参数给数据提供者

导入过程支持向DataProvider传递未命名的超过参数。如果您想允许只导入一条记录,这可能很有用。

flow import:batch --preset base recordIdentifier:1234

超过参数将通过$this->getExceedingArguments()DataProvider中可用。您需要自行处理这些数据并将其应用到您的抓取逻辑中。

基于命令的导入器

某些数据源可能由命令而不是数据记录组成。例如,一个JSON文件可能包含createupdatedelete指令,这可以减少导入器端的猜测工作,例如哪些记录可能是新的,哪些应该更新,以及记录不存在是否意味着应该从内容库中删除相应的节点。

对于这些情况,您可以扩展AbstractCommandBasedImporter。如果您的数据记录包含一个mode字段,导入器将尝试调用同一类中的相应命令方法。

以下是一个数据源文件示例

[
    {
        "mode": "create",
        "mpn": "1081251137",
        "languageIdentifier": "de",
        "properties": {
            "label": "Coffee Machine",
            "price": "220000",
            "externalKey": "1081251137"
        }
    },
    {
        "mode": "delete",
        "mpn": "591500202"
    }
]

一个对应的ProductImporter可能看起来像这样

/**
 * Class ProductImporter
 */
class ProductImporter extends AbstractCommandBasedImporter
{

    /**
     * @var string
     */
    protected $storageNodeNodePath = 'products';

    /**
     * @var string
     */
    protected $storageNodeTitle = 'Products';

    /**
     * @var string
     */
    protected $externalIdentifierDataKey = 'mpn';

    /**
     * @var string
     */
    protected $labelDataKey = 'properties.Label';

    /**
     * @var string
     */
    protected $nodeNamePrefix = 'product-';

    /**
     * @var string
    */
    protected $nodeTypeName = 'Acme.MyShop:Product';

    /**
     * Creates a new product
     *
     * @param string $externalIdentifier
     * @param array $data
     * @return void
     */
    protected function createCommand($externalIdentifier, array $data)
    {
        $this->applyProperties($data['properties'], $this->nodeTemplate);

        $node = $this->storageNode->createNodeFromTemplate($this->nodeTemplate);
        $this->registerNodeProcessing($node, $externalIdentifier);
    }

    /**
     * Updates a product
     *
     * @param string $externalIdentifier
     * @param array $data
     * @return void
     */
    protected function updateCommand($externalIdentifier, array $data)
    {
        $this->applyProperties($data['properties'], $this->nodeTemplate);

        $node = $this->storageNode->createNodeFromTemplate($this->nodeTemplate);
        $this->registerNodeProcessing($node, $externalIdentifier);
    }

    /**
     * Deletes a product
     *
     * @param string $externalIdentifier
     * @param array $data
     */
    protected function deleteCommand($externalIdentifier, array $data)
    {
        // delete the product node
    }
}

CSV数据提供者

此包附带了一个用于CSV文件的基本数据提供者,这对于许多场景来说已经足够了。此数据提供者的类名为Ttree\ContentRepositoryImporter\DataProvider\CsvDataProvider

以下选项可以传递给数据提供者

  • csvFilePath:导入文件的完整路径和文件名
  • csvDelimiter:CSV文件中使用的分隔符(默认:,
  • csvEnclosure:用于封装值的字符(默认:"
  • skipHeader:如果CSV文件的第一行应该被忽略(默认:false)

以下是一个使用CSV数据提供者的预设示例

Ttree:
  ContentRepositoryImporter:
    presets:
      'products':
        parts:
          'products':
            label: 'Product Import'
            batchSize: 100
            dataProviderClassName: 'Ttree\ContentRepositoryImporter\DataProvider\CsvDataProvider'
            dataProviderOptions:
              csvFilePath: '/tmp/Products.csv'
              csvDelimiter: ';'
              csvEnclosure: '"'
              skipHeader: true
            importerClassName: 'Acme\MyProductImporter\Service\Import\ProductImporter'
            importerOptions:
              siteNodePath: '/sites/wwwacmecom'

致谢

本开发由ttree ltd - neos 解决方案提供商赞助。

我们竭尽全力以满腔热情制作这个包,我们欢迎赞助、支持请求等,请联系我们。

许可证

根据GPLv3+许可,请参阅LICENSE