Skip to content

Commit

Permalink
[Feature] Add SQL Data Source (#379)
Browse files Browse the repository at this point in the history
* Add SQL data source with connections select

* Create sql data loader

* Add sql format to file format selection

* Update docs

* Apply php-cs-fixer changes

* Add doctrine dependencies

* Replace executeQuery with execute to match doctrine dependency in SqlLoader

* Add missing strict_types declaration in php files

---------

Co-authored-by: msoroka <[email protected]>
  • Loading branch information
msoroka and msoroka authored Sep 9, 2024
1 parent deb4fa0 commit 2d6bd8f
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
"prefer-stable": true,
"minimum-stability": "dev",
"require": {
"pimcore/compatibility-bridge-v10": "^1.0",
"php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
"ext-fileinfo": "*",
"ext-json": "*",
"doctrine/dbal": "^2.12 || ^3.5",
"dragonmantank/cron-expression": "^3.1",
"league/flysystem-sftp-v3": "^3.0",
"nesbot/carbon": "^2.27",
"phpoffice/phpspreadsheet": "^1.24 || ^2.2",
"pimcore/compatibility-bridge-v10": "^1.0",
"pimcore/data-hub": "^1.6",
"pimcore/pimcore": "^10.6 || ^11.0",
"symfony/mime": "^5.2 || ^6.2",
Expand Down
39 changes: 39 additions & 0 deletions doc/03_Configuration/01_Data_Sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,42 @@ The URL for the endpoint is: `http(s)://<YOUR_DOMAIN>>/pimcore-datahub-import/<I
(see also [Import Execution Details](../04_Import_Execution_Details.md)). Thus pushing data to the
endpoint when import queue is not empty would result in an error. Activating this flag will ignore
existing items in the queue and always adds items to the queue when data is pushed to the endpoint.


### SQL

<div class="image-as-lightbox"></div>

![Data Source SQL](../img/datasource_sql.png)

Loads data from a defined doctrine connection.

The SQL Data Loader uses [DBAL](https://www.doctrine-project.org/projects/dbal.html) to allow data to be
loaded from a SQL source. Connections to any database supported by DBAL will work provided they are
configured correctly inside of `database.yaml`. (Database configuration can be placed in any valid
Symfony config file, provided its in the correct format as can be seen in `database.yaml`).

Example connection:
```yaml
doctrine:
dbal:
connections:
new_connection:
host: db
port: '3306'
user: sample_user
password: sample_password
dbname: sample_dbname
driver: any_supported_by_doctrine
```
For different drivers some additional configuration could be needed.
##### Configuration Options:
- **Connection**: Connection from which data will be loaded
- **SELECT**: Valid SQL `SELECT`
- **FROM**: Valid SQL `FROM`
- **WHERE**: Valid SQL `WHERE`
- **GROUP BY**: Valid SQL `GROUP BY`

Ensure to select **SQL** under File Format!
Binary file added doc/img/datasource_sql.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions src/Controller/ConnectionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\DataImporterBundle\Controller;

use Exception;
use Pimcore\Controller\UserAwareController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

/**
* @Route("/admin/pimcoredataimporter/")
*/
class ConnectionController extends UserAwareController
{
/**
* @Route("connections", name="pimcore_dataimporter_connections", methods={"GET"})
*
* @throws Exception
*/
public function connectionAction(): JsonResponse
{
$connections = $this->getParameter('doctrine.connections');

if (!is_array($connections)) {
throw new Exception('Doctrine connection not returned as array');
}

$mappedConnections = array_map(fn ($key, $value): array => [
'name' => $key,
'value' => $value
], array_keys($connections), $connections);

return $this->json($mappedConnections);
}
}
22 changes: 22 additions & 0 deletions src/DataSource/Interpreter/SqlFileInterpreter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\DataImporterBundle\DataSource\Interpreter;

class SqlFileInterpreter extends JsonFileInterpreter
{
}
137 changes: 137 additions & 0 deletions src/DataSource/Loader/SqlLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\DataImporterBundle\DataSource\Loader;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Pimcore;
use Pimcore\Bundle\DataImporterBundle\Exception\InvalidConfigurationException;
use Symfony\Component;

class SqlLoader implements DataLoaderInterface
{
private string $connection;
private string $select;
private string $from;
private string $where;
private string $groupBy;

private string $importFilePath;
private Connection $databaseConnection;

public function __construct(private Component\Filesystem\Filesystem $filesystem)
{
}

/**
* @throws InvalidConfigurationException
* @throws Exception
* @throws FilesystemException
*/
public function loadData(): string
{
$this->setUpConnection();
$this->setUpImportFilePath();

$queryBuilder = $this->databaseConnection->createQueryBuilder();
$queryBuilder->select($this->select)
->from($this->from);

if (!empty($this->where)) {
$queryBuilder->where($this->where);
}

if (!empty($this->groupBy)) {
$queryBuilder->groupBy($this->groupBy);
}

$result = $queryBuilder->execute()->fetchAllAssociative();

$filesystemLocal = new Filesystem(new LocalFilesystemAdapter('/'));
$stream = fopen('php://temp', 'r+');
$resultAsJson = json_encode($result);

fwrite($stream, $resultAsJson);
rewind($stream);

$filesystemLocal->writeStream($this->importFilePath, $stream);

return $this->importFilePath;
}

public function cleanup(): void
{
$this->databaseConnection->close();

unlink($this->importFilePath);
}

/**
* @throws InvalidConfigurationException
*/
public function setSettings(array $settings): void
{
if (empty($settings['connection'])) {
throw new InvalidConfigurationException('Empty connection.');
}
$this->connection = $settings['connection'];

if (empty($settings['select'])) {
throw new InvalidConfigurationException('Empty select.');
}
$this->select = $settings['select'];

if (empty($settings['from'])) {
throw new InvalidConfigurationException('Empty from.');
}
$this->from = $settings['from'];

$this->where = $settings['where'];
$this->groupBy = $settings['groupBy'];
}

/**
* @throws InvalidConfigurationException
*/
private function setUpConnection(): void
{
$container = Pimcore::getContainer();
$databaseConnection = null;

if ($container instanceof Component\DependencyInjection\ContainerInterface) {
$databaseConnection = $container->get($this->connection);
}

if (!$databaseConnection instanceof Connection) {
throw new InvalidConfigurationException('Connection not found.');
}

$this->databaseConnection = $databaseConnection;
}

private function setUpImportFilePath(): void
{
$folder = PIMCORE_PRIVATE_VAR . '/tmp/datahub/dataimporter/sql-loader/';
$this->filesystem->mkdir($folder, 0775);

$this->importFilePath = $folder . uniqid('sql-import-');
}
}
2 changes: 2 additions & 0 deletions src/PimcoreDataImporterBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ public function getJsPaths(): array
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/loader/asset.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/loader/upload.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/loader/push.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/loader/sql.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/interpreter/csv.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/interpreter/json.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/interpreter/xlsx.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/interpreter/xml.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/interpreter/sql.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/cleanup/unpublish.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/cleanup/delete.js',
'/bundles/pimcoredataimporter/js/pimcore/configuration/components/importSettings.js',
Expand Down
6 changes: 6 additions & 0 deletions src/Resources/config/pimcore/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ _data_hub_data_importer_data_object_admin:
options:
expose: true

_data_hub_data_importer_connections:
resource: "@PimcoreDataImporterBundle/Controller/ConnectionController.php"
type: annotation
options:
expose: true

_data_hub_data_importer_data_object_push:
resource: "@PimcoreDataImporterBundle/Controller/PushImportController.php"
type: annotation
11 changes: 11 additions & 0 deletions src/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ services:
tags:
- { name: "pimcore.datahub.data_importer.loader", type: "push" }

Pimcore\Bundle\DataImporterBundle\DataSource\Loader\SqlLoader:
tags:
- { name: "pimcore.datahub.data_importer.loader", type: "sql" }

Pimcore\Bundle\DataImporterBundle\DataSource\Interpreter\DeltaChecker\DeltaChecker: ~

Pimcore\Bundle\DataImporterBundle\DataSource\Interpreter\InterpreterFactory: ~
Expand Down Expand Up @@ -91,6 +95,13 @@ services:
- { name: monolog.logger, channel: 'DATA-IMPORTER' }
- { name: "pimcore.datahub.data_importer.interpreter", type: "xml" }

Pimcore\Bundle\DataImporterBundle\DataSource\Interpreter\SqlFileInterpreter:
calls:
- [ setLogger, [ '@logger' ] ]
tags:
- { name: monolog.logger, channel: 'DATA-IMPORTER' }
- { name: "pimcore.datahub.data_importer.interpreter", type: "sql" }

Pimcore\Bundle\DataImporterBundle\Cleanup\CleanupStrategyFactory: ~

Pimcore\Bundle\DataImporterBundle\Cleanup\DeleteStrategy:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

pimcore.registerNS("pimcore.plugin.pimcoreDataImporterBundle.configuration.components.interpreter.sql");
pimcore.plugin.pimcoreDataImporterBundle.configuration.components.interpreter.sql = Class.create(pimcore.plugin.pimcoreDataImporterBundle.configuration.components.abstractOptionType, {

type: 'sql',

buildSettingsForm: function() {

if(!this.form) {
this.form = Ext.create('DataHub.DataImporter.StructuredValueForm', {
defaults: {
labelWidth: 200,
width: 600
},
border: false,
items: [
]
});
}

return this.form;
}

});
Loading

0 comments on commit 2d6bd8f

Please sign in to comment.