19 Commits
v2.1.0 ... v3.x

Author SHA1 Message Date
Jérémy J
176517f9c6 Updated CHANGELOG 2024-12-17 11:50:52 +01:00
Jérémy J
9507a39720 Added error count columns to job tables 2024-12-17 11:48:27 +01:00
Jérémy J
131045afe8 Fix NotModifiedContentFilter when creating new translation 2024-02-05 15:55:55 +01:00
jeremycr
0a47f788dc Allow Dataflow 4 (#43) 2023-07-27 09:51:45 +02:00
Jérémy
2274bbf5e5 Changelog for 3.2.0 2023-04-11 09:47:36 +02:00
jeremycr
4a0070dc04 Improvements on datetime handling + #38 + #39 (#41) 2023-04-11 09:39:57 +02:00
jeremycr
7ca493e3e8 Allow LocationCreateStruct objects inside the $locations argument of ContentCreateStructure to have more control over the created locations. (#40) 2023-01-13 16:41:37 +01:00
jbcr
473968791b bump PHP version (#37)
* bump PHP version

* update PHPUnit config
2022-01-25 16:58:20 +01:00
jbcr
dd655543ce fail-fast: false 2022-01-25 09:12:06 +01:00
jbcr
d49d23b4fe change composer command 2022-01-25 09:06:00 +01:00
jbcr
7ab5b5cbd1 remove redunded action and comment 2022-01-25 09:04:02 +01:00
jbcr
df1fcd5132 remove PHP 7.1 and PHP 7.2 2022-01-25 09:01:50 +01:00
jbcr
35d3788a59 app PHP Lint 2022-01-25 08:59:15 +01:00
jbcr
c36fef9220 Add test in action 2022-01-25 08:53:40 +01:00
jbcr
b4186ed3ce WIP: Add compatibilty to eZ Platform 3 (#20)
* update require to platform 3

* update admin to admin ui 2.0beta3

* some eZ Platform 3 change (#32)

* bump eZ required version

* enable autowire and autoconfigure on controller

* use contracts dependency for translation interface instead of Component

* change requirements

* add change log

Co-authored-by: Jean-Baptiste Nahan <814683+macintoshplus@users.noreply.github.com>
2021-06-25 14:52:13 +02:00
jbcr
657c269eb3 add log in content writer and notmodifiercontentfilter (#35)
* add log in content writer and notmodifiercontentfilter

* add changelog and readme
2021-01-15 17:14:07 +01:00
jbcr
6f1a719314 Update CHANGELOG.md 2021-01-14 17:07:55 +01:00
jeremycr
52f607616d Added a button to display exceptions / log in a modal (#33) 2021-01-14 17:06:41 +01:00
jeremycr
0c13878f79 Added content fields comparison to ignore updates that would result in no change (#26) 2020-10-23 10:23:05 +02:00
63 changed files with 1137 additions and 277 deletions

55
.github/workflows/php.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: PHP Composer
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
strategy:
fail-fast: false
matrix:
php-version: [7.3, 7.4, 8.0, 8.1]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: xdebug
- name: PHP Version
run: php -v
- name: PHP Lint
run: find . -type f -name '*.php' -exec php -l {} \; | (! grep -v "No syntax errors detected" )
- name: Validate composer.json and composer.lock
run: composer validate --strict
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-php${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php${{ matrix.php-version }}-
- name: Install dependencies
run: composer update --prefer-dist --no-progress --no-scripts
- name: PHPUnit
run: php -d xdebug.mode=coverage vendor/bin/phpunit --log-junit junit.xml
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
with:
files: junit.xml

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ composer.lock
.phpunit.result.cache
.php_cs.cache
.php_cs
.php-cs-fixer.cache

View File

@@ -2,7 +2,7 @@
$finder = PhpCsFixer\Finder::create()->in(__DIR__.'/src');
return PhpCsFixer\Config::create()
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'declare_strict_types' => true,

View File

@@ -1,5 +1,48 @@
# Version 3.4.0
* Added error count columns to job tables
# Version 3.3.2
* Fix NotModifiedContentFilter when creating new translation
# Version 3.3.1
* Allow Dataflow 4
# Version 3.2.0
* Fixed History page pagination is hidden by footer on Ibexa 3.3 #38
* Added filter on history page to filter out jobs with 0 items
* Date and times will now be displayed using the user defined timezone, and stored using php timezone
# Version 3.1.0
* Allow `LocationCreateStruct` objects inside the `$locations` argument of `ContentCreateStructure` to have more control over the created locations.
# Version 3.0.1
* Bump minimum PHP version to PHP 7.3 like code-rhapsodie/dataflow-bundle dependency.
* Allow PHP 8.x.
* Add GitHub Action to run tests.
# Version 3.0.0
* Add compatibility with Ibexa Content 3.3
* Add compatibility with Symfony 5.x
# Version 2.3.0
* Added a button to display exceptions / log in a modal
* Add log in `CodeRhapsodie\EzDataflowBundle\Filter\NotModifiedContentFilter` and `CodeRhapsodie\EzDataflowBundle\Writer\ContentWriter`
# Version 2.2.0
* Added `NotModifiedContentFilter` and a bunch of `FieldComparator` classes
# version 2.1.0
* contentWriter return created content
* ContentWriter return created content
# version 2.0.1

View File

@@ -178,7 +178,7 @@ class MyDataflowType extends AbstractDataflowType
/**
* @var ContentStructureFactory
*/
private contentStructureFactory;
private $contentStructureFactory;
public function __construct(ContentWriter $contentWriter, ContentStructureFactory $contentStructureFactory)
{
@@ -206,6 +206,8 @@ class MyDataflowType extends AbstractDataflowType
ContentStructureFactoryInterface::MODE_INSERT_OR_UPDATE //Optional value. Other choice : ContentStructureFactoryInterface::MODE_INSERT_ONLY or ContentStructureFactoryInterface::MODE_UPDATE_ONLY
);
});
// If you want the writer log
$this->contentWriter->setLogger($this->logger);
$builder->addWriter($this->contentWriter);
}
}
@@ -213,6 +215,85 @@ class MyDataflowType extends AbstractDataflowType
This example uses `ContentStructureFactory` to check if the content exists and returns the adequate `ContentStrucure` to pass to the content writer.
## Use the NotModifiedContentFilter
When updating contents, you might want to ignore contents where the update would not result in any actual changes in fields values. In that case, you can add the `NotModifiedContentFilter` as one of your steps.
```php
// In your DataflowType
public function __construct(NotModifiedContentFilter $notModifiedContentFilter)
{
$this->notModifiedContentFilter = $notModifiedContentFilter;
}
//[...]
protected function buildDataflow(DataflowBuilder $builder, array $options): void
{
//[...]
// If you want the filter log
$this->notModifiedContentFilter->setLogger($this->logger);
$builder->addStep($this->notModifiedContentFilter);
//[...]
}
```
This filter compares each field value in the `ContentUpdateStructure` received to the fields values in the existing content object. If all values are identical, this filter will return `false`, otherwise, the `ContentUpdateStructure` will be returned as is.
Not all field types are supported by this filter. Il a field type is not supported, values will be assumed different. If your dataflow is dealing with content types containing unsupported field types, it is better to simply not use the `NotModifiedContentFilter` to prevent unnecessary overhead.
### Supported field types
- ezstring
- ezauthor
- ezboolean
- ezcountry
- ezdate
- ezdatetime
- ezemail
- ezfloat
- ezisbn
- ezobjectrelation
- ezobjectrelationlist
- ezkeyword
- ezselection
- eztext
- eztime
- eztags
- novaseometas
- ezurl
- ezmatrix
- ezgmaplocation
- ezrichtext
### Add custom field comparator
If you want to add support for a field type, simply create your own comparator.
```php
<?php
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator;
use eZ\Publish\Core\FieldType\Value;
//[...]
class MyFieldComparator extends AbstractFieldComparator
{
//[...]
protected function compareValues(Value $currentValue, Value $newValue): bool
{
// Return true if values are identical, false if values are different.
}
}
```
```yaml
# Service declaration
App\FieldComparator\MyFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'my_field_type_identifier' }
```
# Admin UI
## Access to the eZ Dataflow UI

View File

@@ -41,18 +41,24 @@
}
},
"require": {
"code-rhapsodie/dataflow-bundle": "^2.0 || dev-master",
"ezsystems/ezplatform-admin-ui": "^1.0",
"ezsystems/ezpublish-kernel": "^7.0"
"php": "^7.3||^8.0",
"ext-json": "*",
"code-rhapsodie/dataflow-bundle": "^3.0||^4.0",
"ezsystems/ezplatform-admin-ui": "^2.3",
"ezsystems/ezplatform-kernel": "^1.3",
"http-interop/http-factory-guzzle": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^7||^8",
"phpunit/phpunit": "^7||^8||^9",
"doctrine/dbal": "^2.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"php-http/discovery": true
}
},
"extra": {
"branch-alias": {

View File

@@ -1,32 +1,21 @@
<?xml version = '1.0' encoding = 'UTF-8'?>
<?xml version="1.0" encoding="UTF-8"?>
<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="Tests/bootstrap.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
colors="false"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Port tests suite">
<directory suffix="Test.php">./Tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>Tests/</directory>
<directory>vendor/</directory>
</exclude>
</whitelist>
</filter>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false" bootstrap="tests/bootstrap.php" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" colors="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory>./src/</directory>
</include>
<exclude>
<directory>tests/</directory>
<directory>vendor/</directory>
</exclude>
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
</php>
<testsuites>
<testsuite name="EzDataflow tests suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle;
use CodeRhapsodie\EzDataflowBundle\DependencyInjection\CodeRhapsodieEzDataflowExtension;
use CodeRhapsodie\EzDataflowBundle\DependencyInjection\Compiler\FieldComparatorCompilerPass;
use CodeRhapsodie\EzDataflowBundle\Security\PolicyProvider;
use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\EzPublishCoreExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -23,6 +24,8 @@ class CodeRhapsodieEzDataflowBundle extends Bundle
{
parent::build($container);
$container->addCompilerPass(new FieldComparatorCompilerPass());
/** @var EzPublishCoreExtension $eZExtension */
$eZExtension = $container->getExtension('ezpublish');
$eZExtension->addPolicyProvider(new PolicyProvider());

View File

@@ -4,15 +4,15 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Controller;
use CodeRhapsodie\EzDataflowBundle\Form\CreateOneshotType;
use CodeRhapsodie\EzDataflowBundle\Form\CreateScheduledType;
use CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway;
use CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway;
use CodeRhapsodie\DataflowBundle\Entity\Job;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use CodeRhapsodie\EzDataflowBundle\Form\CreateOneshotType;
use CodeRhapsodie\EzDataflowBundle\Form\CreateScheduledType;
use CodeRhapsodie\EzDataflowBundle\Gateway\ExceptionJSONDecoderAdapter;
use CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway;
use CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway;
use Doctrine\DBAL\Query\QueryBuilder;
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute;
use EzSystems\EzPlatformAdminUi\Notification\NotificationHandlerInterface;
use EzSystems\EzPlatformAdminUiBundle\Controller\Controller;
use Pagerfanta\Adapter\DoctrineDbalAdapter;
use Pagerfanta\Pagerfanta;
@@ -27,22 +27,17 @@ class DashboardController extends Controller
{
/** @var JobGateway */
private $jobGateway;
/** @var NotificationHandlerInterface */
private $notificationHandler;
/** @var ScheduledDataflowGateway */
private $scheduledDataflowGateway;
public function __construct(JobGateway $jobGateway, NotificationHandlerInterface $notificationHandler, ScheduledDataflowGateway $scheduledDataflowGateway)
public function __construct(JobGateway $jobGateway, ScheduledDataflowGateway $scheduledDataflowGateway)
{
$this->jobGateway = $jobGateway;
$this->notificationHandler = $notificationHandler;
$this->scheduledDataflowGateway = $scheduledDataflowGateway;
}
/**
* @Route("/", name="coderhapsodie.ezdataflow.main")
*
* @return Response
*/
public function main(): Response
{
@@ -69,10 +64,6 @@ class DashboardController extends Controller
/**
* @Route("/repeating", name="coderhapsodie.ezdataflow.repeating")
*
* @param Request $request
*
* @return Response
*/
public function getRepeatingPage(Request $request): Response
{
@@ -101,10 +92,6 @@ class DashboardController extends Controller
/**
* @Route("/oneshot", name="coderhapsodie.ezdataflow.oneshot")
*
* @param Request $request
*
* @return Response
*/
public function getOneshotPage(Request $request): Response
{
@@ -118,35 +105,30 @@ class DashboardController extends Controller
public function history(Request $request): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'view'));
$filter = (int) $request->query->get('filter', JobGateway::FILTER_NONE);
return $this->render('@ezdesign/ezdataflow/Dashboard/history.html.twig', [
'pager' => $this->getPager($this->jobGateway->getListQueryForAdmin(), $request),
'pager' => $this->getPager($this->jobGateway->getListQueryForAdmin($filter), $request),
'filter' => $filter,
]);
}
/**
* @Route("/history", name="coderhapsodie.ezdataflow.history")
*
* @param Request $request
*
* @return Response
*/
public function getHistoryPage(Request $request): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'view'));
$filter = (int) $request->query->get('filter', JobGateway::FILTER_NONE);
return $this->render('@ezdesign/ezdataflow/Dashboard/history.html.twig', [
'pager' => $this->getPager($this->jobGateway->getListQueryForAdmin(), $request),
'pager' => $this->getPager($this->jobGateway->getListQueryForAdmin($filter), $request),
'filter' => $filter,
]);
}
/**
* @Route("/history/schedule/{id}", name="coderhapsodie.ezdataflow.history.workflow")
*
* @param Request $request
* @param int $id
*
* @return Response
*/
public function getHistoryForScheduled(Request $request, int $id): Response
{
@@ -160,10 +142,15 @@ class DashboardController extends Controller
private function getPager(QueryBuilder $query, Request $request): Pagerfanta
{
$pager = new Pagerfanta(new DoctrineDbalAdapter($query, function ($queryBuilder) {
return $queryBuilder->select('COUNT(DISTINCT id) AS total_results')
->setMaxResults(1);
}));
$pager = new Pagerfanta(
new ExceptionJSONDecoderAdapter(
new DoctrineDbalAdapter($query, function ($queryBuilder) {
return $queryBuilder->select('COUNT(DISTINCT id) AS total_results')
->resetQueryPart('orderBy')
->setMaxResults(1);
})
)
);
$pager->setMaxPerPage(20);
$pager->setCurrentPage($request->query->get('page', 1));

View File

@@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/ezdataflow/job")
@@ -26,7 +26,7 @@ class JobController extends Controller
private $jobGateway;
/** @var NotificationHandlerInterface */
private $notificationHandler;
/** @var TranslatorInterface */
/** @var Symfony\Component\Translation\TranslatorInterface|Symfony\Contracts\Translation\TranslatorInterface */
private $translator;
public function __construct(
@@ -41,10 +41,6 @@ class JobController extends Controller
/**
* @Route("/details/{id}", name="coderhapsodie.ezdataflow.job.details")
*
* @param int $id
*
* @return Response
*/
public function displayDetails(int $id): Response
{
@@ -55,12 +51,24 @@ class JobController extends Controller
]);
}
/**
* @Route("/details/log/{id}", name="coderhapsodie.ezdataflow.job.log")
*/
public function displayLog(int $id): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'view'));
$item = $this->jobGateway->find($id);
$log = array_map(function ($line) {
return preg_replace('~#\d+~', "\n$0", $line);
}, $item->getExceptions());
return $this->render('@ezdesign/ezdataflow/Item/log.html.twig', [
'log' => $log,
]);
}
/**
* @Route("/create", name="coderhapsodie.ezdataflow.job.create", methods={"POST"})
*
* @param Request $request
*
* @return Response
*/
public function create(Request $request): Response
{

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Controller;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use CodeRhapsodie\EzDataflowBundle\Form\CreateScheduledType;
use CodeRhapsodie\EzDataflowBundle\Form\UpdateScheduledType;
use CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway;
use CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway;
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute;
use EzSystems\EzPlatformAdminUi\Notification\NotificationHandlerInterface;
use EzSystems\EzPlatformAdminUiBundle\Controller\Controller;
@@ -16,7 +16,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/ezdataflow/scheduled_workflow")
@@ -32,8 +32,12 @@ class ScheduledDataflowController extends Controller
/** @var TranslatorInterface */
private $translator;
public function __construct(JobGateway $jobGateway, NotificationHandlerInterface $notificationHandler, ScheduledDataflowGateway $scheduledDataflowGateway, TranslatorInterface $translator)
{
public function __construct(
JobGateway $jobGateway,
NotificationHandlerInterface $notificationHandler,
ScheduledDataflowGateway $scheduledDataflowGateway,
TranslatorInterface $translator
) {
$this->jobGateway = $jobGateway;
$this->notificationHandler = $notificationHandler;
$this->scheduledDataflowGateway = $scheduledDataflowGateway;
@@ -42,10 +46,6 @@ class ScheduledDataflowController extends Controller
/**
* @Route("/create", name="coderhapsodie.ezdataflow.workflow.create", methods={"POST"})
*
* @param Request $request
*
* @return Response
*/
public function create(Request $request): Response
{
@@ -58,12 +58,12 @@ class ScheduledDataflowController extends Controller
if ($form->isSubmitted() && $form->isValid()) {
/** @var ScheduledDataflow $newWorkflow */
$newWorkflow = $form->getData();
try {
$this->scheduledDataflowGateway->save($newWorkflow);
$this->notificationHandler->success($this->translator->trans('coderhapsodie.ezdataflow.workflow.create.success'));
} catch (\Exception $e) {
$this->notificationHandler->error($this->translator->trans('coderhapsodie.ezdataflow.workflow.create.error', ['message' => $e->getMessage()]));
$this->notificationHandler->error($this->translator->trans('coderhapsodie.ezdataflow.workflow.create.error',
['message' => $e->getMessage()]));
}
return new JsonResponse(['redirect' => $this->generateUrl('coderhapsodie.ezdataflow.main')]);
@@ -79,10 +79,6 @@ class ScheduledDataflowController extends Controller
/**
* @Route("/{id}/delete", name="coderhapsodie.ezdataflow.workflow.delete", methods={"post"})
*
* @param int $id
*
* @return Response
*/
public function delete(int $id): Response
{
@@ -94,7 +90,8 @@ class ScheduledDataflowController extends Controller
return new JsonResponse(['code' => 200]);
} catch (\Exception $e) {
$this->notificationHandler->error($this->translator->trans('coderhapsodie.ezdataflow.workflow.delete.error', ['message' => $e->getMessage()]));
$this->notificationHandler->error($this->translator->trans('coderhapsodie.ezdataflow.workflow.delete.error',
['message' => $e->getMessage()]));
return new JsonResponse(['code' => $e->getCode()]);
}
@@ -102,11 +99,6 @@ class ScheduledDataflowController extends Controller
/**
* @Route("/{id}/edit", name="coderhapsodie.ezdataflow.workflow.edit")
*
* @param Request $request
* @param int $id
*
* @return Response
*/
public function edit(Request $request, int $id): Response
{
@@ -123,7 +115,8 @@ class ScheduledDataflowController extends Controller
$this->scheduledDataflowGateway->save($editDataflow);
$this->notificationHandler->success($this->translator->trans('coderhapsodie.ezdataflow.workflow.edit.success'));
} catch (\Exception $e) {
$this->notificationHandler->error($this->translator->trans('coderhapsodie.ezdataflow.workflow.edit.error', ['message' => $e->getMessage()]));
$this->notificationHandler->error($this->translator->trans('coderhapsodie.ezdataflow.workflow.edit.error',
['message' => $e->getMessage()]));
}
return new JsonResponse(['redirect' => $this->generateUrl('coderhapsodie.ezdataflow.main')]);
@@ -139,10 +132,6 @@ class ScheduledDataflowController extends Controller
/**
* @Route("/{id}/enable", name="coderhapsodie.ezdataflow.workflow.enable")
*
* @param int $id
*
* @return Response
*/
public function enableDataflow(int $id): Response
{
@@ -153,22 +142,6 @@ class ScheduledDataflowController extends Controller
return $this->redirectToRoute('coderhapsodie.ezdataflow.main');
}
/**
* @Route("/{id}/disable", name="coderhapsodie.ezdataflow.workflow.disable")
*
* @param int $id
*
* @return Response
*/
public function disableDataflow(int $id): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'edit'));
$this->changeDataflowStatus($id, false);
return $this->redirectToRoute('coderhapsodie.ezdataflow.main');
}
private function changeDataflowStatus(int $id, bool $status)
{
try {
@@ -182,4 +155,16 @@ class ScheduledDataflowController extends Controller
$this->notificationHandler->error(sprintf('An error occured : "%s".', $e->getMessage()));
}
}
/**
* @Route("/{id}/disable", name="coderhapsodie.ezdataflow.workflow.disable")
*/
public function disableDataflow(int $id): Response
{
$this->denyAccessUnlessGranted(new Attribute('ezdataflow', 'edit'));
$this->changeDataflowStatus($id, false);
return $this->redirectToRoute('coderhapsodie.ezdataflow.main');
}
}

View File

@@ -35,10 +35,6 @@ class ContentCreator implements ContentCreatorInterface
}
/**
* @param ContentCreateStructure $structure
*
* @return Content
*
* @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
* @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException
* @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
@@ -58,17 +54,21 @@ class ContentCreator implements ContentCreatorInterface
}
/**
* @param array $locations
*
* @return LocationCreateStruct[]
*/
private function getLocationCreateStructs(array $locations): array
{
$locationCreateStructs = [];
foreach ($locations as $locationOrIdOrRemoteId) {
foreach ($locations as $locationOrIdOrRemoteIdOrStruct) {
if ($locationOrIdOrRemoteIdOrStruct instanceof LocationCreateStruct) {
$locationCreateStructs[] = $locationOrIdOrRemoteIdOrStruct;
continue;
}
$locationCreateStructs[] = new LocationCreateStruct([
'parentLocationId' => $this->matcher->matchLocation($locationOrIdOrRemoteId)->id,
'parentLocationId' => $this->matcher->matchLocation($locationOrIdOrRemoteIdOrStruct)->id,
]);
}

View File

@@ -30,10 +30,6 @@ class ContentUpdater implements ContentUpdaterInterface
}
/**
* @param ContentUpdateStructure $structure
*
* @return Content
*
* @throws NoMatchFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\BadStateException
* @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException

View File

@@ -17,8 +17,6 @@ class ContentStructFieldFiller implements ContentStructFieldFillerInterface
/**
* ContentStructFieldFiller constructor.
*
* @param iterable $fieldValueCreators
*/
public function __construct(iterable $fieldValueCreators)
{
@@ -47,10 +45,7 @@ class ContentStructFieldFiller implements ContentStructFieldFillerInterface
}
/**
* @param string $fieldTypeIdentifier
* @param mixed $hash
*
* @return Value
* @param mixed $hash
*
* @throws UnsupportedFieldTypeException
*/

View File

@@ -9,10 +9,5 @@ use eZ\Publish\API\Repository\Values\ContentType\ContentType;
interface ContentStructFieldFillerInterface
{
/**
* @param ContentType $contentType
* @param ContentStruct $contentStruct
* @param array $fieldHashes
*/
public function fillFields(ContentType $contentType, ContentStruct $contentStruct, array $fieldHashes): void;
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\Field;
use eZ\Publish\API\Repository\FieldType;
use eZ\Publish\API\Repository\FieldTypeService;
use eZ\Publish\Core\FieldType\Value;

View File

@@ -8,18 +8,10 @@ use eZ\Publish\Core\FieldType\Value;
interface FieldValueCreatorInterface
{
/**
* @param string $fieldTypeIdentifier
*
* @return bool
*/
public function supports(string $fieldTypeIdentifier): bool;
/**
* @param string $fieldTypeIdentifier
* @param mixed $hash
*
* @return Value
* @param mixed $hash
*/
public function createValue(string $fieldTypeIdentifier, $hash): Value;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\API\Repository\FieldTypeService;
use eZ\Publish\API\Repository\Values\Content\Field;
use eZ\Publish\Core\FieldType\Value;
abstract class AbstractFieldComparator implements FieldComparatorInterface
{
/** @var FieldTypeService */
private $fieldTypeService;
public function __construct(FieldTypeService $fieldTypeService)
{
$this->fieldTypeService = $fieldTypeService;
}
public function compare(Field $field, $hash): bool
{
$newValue = $this->fieldTypeService->getFieldType($field->fieldTypeIdentifier)->fromHash($hash);
return $this->compareValues($field->value, $newValue);
}
/**
* Returns true if values are equals, false otherwise.
*/
abstract protected function compareValues(Value $currentValue, Value $newValue): bool;
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\API\Repository\Values\Content\Field;
class DelegatorFieldComparator implements FieldComparatorInterface
{
/** @var FieldComparatorInterface[] */
private $delegates;
/**
* FieldComparator constructor.
*/
public function __construct()
{
$this->delegates = [];
}
public function compare(Field $field, $hash): bool
{
if (isset($this->delegates[$field->fieldTypeIdentifier])) {
return $this->delegates[$field->fieldTypeIdentifier]->compare($field, $hash);
}
// No comparator to handle this field type, we assume the value is different.
return false;
}
public function registerDelegateFieldComparator(FieldComparatorInterface $typedFieldComparator, string $fieldTypeIdentifier): void
{
$this->delegates[$fieldTypeIdentifier] = $typedFieldComparator;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\API\Repository\Values\Content\Field;
interface FieldComparatorInterface
{
/**
* @return bool true if identical, false otherwise
*/
public function compare(Field $field, $hash): bool;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class MapLocationFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
return (string) $currentValue === (string) $newValue
&& (float) $currentValue->longitude === (float) $newValue->longitude
&& (float) $currentValue->latitude === (float) $newValue->latitude
;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class MatrixFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
if (count($currentValue->rows) !== count($newValue->rows)) {
return false;
}
foreach ($newValue->rows as $index => $row) {
if (count($currentValue->rows[$index]->getCells()) !== count($row->getCells())) {
return false;
}
if (!empty(array_diff_assoc($currentValue->rows[$index]->getCells(), $row->getCells()))) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class NovaSEOMetasFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
$map = [];
foreach ($currentValue->metas as $meta) {
$map[$meta->getName()] = $meta->getContent();
}
foreach ($newValue->metas as $meta) {
if (!isset($map[$meta->getName()]) || $map[$meta->getName()] !== $meta->getContent()) {
return false;
}
}
return count($currentValue->metas) === count($newValue->metas);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class SimpleFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
return (string) $currentValue === (string) $newValue;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Core\FieldComparator;
use eZ\Publish\Core\FieldType\Value;
class UrlFieldComparator extends AbstractFieldComparator
{
protected function compareValues(Value $currentValue, Value $newValue): bool
{
return $currentValue->link === $newValue->link && $currentValue->text === $newValue->text;
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\DependencyInjection\Compiler;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class FieldComparatorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has(DelegatorFieldComparator::class)) {
return;
}
$delegatorDef = $container->findDefinition(DelegatorFieldComparator::class);
foreach ($container->findTaggedServiceIds('coderhapsodie.ezdataflow.field_comparator') as $id => $tags) {
foreach ($tags as $attributes) {
if (!isset($attributes['fieldType'])) {
throw new \InvalidArgumentException(sprintf('Service "%s" must define the "fieldType" attribute on "coderhapsodie.ezdataflow.field_comparator" tags.', $id));
}
$delegatorDef->addMethodCall(
'registerDelegateFieldComparator',
[new Reference($id), $attributes['fieldType']]
);
}
}
}
}

View File

@@ -9,8 +9,6 @@ class InvalidArgumentTypeException extends \Exception
/**
* @param string|array $expectedTypes
* @param mixed $received
*
* @return InvalidArgumentTypeException
*/
public static function create($expectedTypes, $received): self
{

View File

@@ -6,12 +6,6 @@ namespace CodeRhapsodie\EzDataflowBundle\Exception;
class UnknownFieldException extends \Exception
{
/**
* @param string $fieldIdentifier
* @param string $contentTypeIdentifier
*
* @return UnknownFieldException
*/
public static function create(string $fieldIdentifier, string $contentTypeIdentifier): self
{
return new self(sprintf(

View File

@@ -19,8 +19,6 @@ final class ContentStructureFactory implements ContentStructureFactoryInterface
/**
* ContentStructureFactory constructor.
*
* @param ContentService $contentService
*/
public function __construct(ContentService $contentService)
{
@@ -28,12 +26,8 @@ final class ContentStructureFactory implements ContentStructureFactoryInterface
}
/**
* @param array $data
* @param string $remoteId
* @param string $language
* @param string $contentType
* @param mixed $parentLocations
* @param int $mode One of the constant ContentStructureFactoryInterface::MODE_*
* @param mixed $parentLocations
* @param int $mode One of the constant ContentStructureFactoryInterface::MODE_*
*
* @return false|ContentStructure
*

View File

@@ -11,10 +11,6 @@ interface ContentStructureFactoryInterface
public const MODE_UPDATE_ONLY = 3;
/**
* @param array $data
* @param string $remoteId
* @param string $language
* @param string $contentType
* @param int|string $parentLocations Int for location id or string for remote location id
* @param int $mode ContentStructureFactoryInterface
*

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Filter;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface;
use CodeRhapsodie\EzDataflowBundle\Model\ContentUpdateStructure;
use eZ\Publish\API\Repository\ContentService;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use Psr\Log\LoggerAwareTrait;
/**
* Filters ContentUpdateStructure that would not result in any actual changes in the content.
*/
class NotModifiedContentFilter
{
use LoggerAwareTrait;
/** @var ContentService */
private $contentService;
/** @var FieldComparatorInterface */
private $comparator;
public function __construct(ContentService $contentService, FieldComparatorInterface $comparator)
{
$this->contentService = $contentService;
$this->comparator = $comparator;
}
public function __invoke($data)
{
if (!$data instanceof ContentUpdateStructure) {
return $data;
}
if ($data->getId()) {
$content = $this->contentService->loadContent($data->getId(), [$data->getLanguageCode()]);
} else {
try {
$content = $this->contentService->loadContentByRemoteId($data->getRemoteId(), [$data->getLanguageCode()]);
} catch (NotFoundException $e) {
// New translation
return $data;
}
}
foreach ($data->getFields() as $identifier => $hash) {
$field = $content->getField($identifier, $data->getLanguageCode());
if (null === $field || !$this->comparator->compare($field, $hash)) {
// At least one field is different, continue the dataflow.
return $data;
}
}
// All fields are identical, filter this item out.
$this->log('info', 'Not modified content skipped', ['id' => $data->getId(), 'remote_id' => $data->getRemoteId()]);
return false;
}
private function log(string $level, string $message, array $context = [])
{
if (null === $this->logger) {
return;
}
$this->logger->log($level, $message, $context);
}
}

View File

@@ -6,7 +6,6 @@ namespace CodeRhapsodie\EzDataflowBundle\Form;
use CodeRhapsodie\DataflowBundle\Entity\Job;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -30,7 +29,7 @@ class CreateOneshotType extends AbstractType
'placeholder' => 'coderhapsodie.dataflow.options.placeholder',
],
])
->add('requestedDate', DateTimeType::class, [
->add('requestedDate', UserTimezoneAwareDateTimeType::class, [
'label' => 'coderhapsodie.dataflow.requestedDate',
'years' => range(date('Y'), date('Y') + 5),
])

View File

@@ -7,7 +7,6 @@ namespace CodeRhapsodie\EzDataflowBundle\Form;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -38,7 +37,7 @@ class CreateScheduledType extends AbstractType
],
'label' => 'coderhapsodie.dataflow.frequency',
])
->add('next', DateTimeType::class, [
->add('next', UserTimezoneAwareDateTimeType::class, [
'years' => range(date('Y'), date('Y') + 5),
'label' => 'coderhapsodie.dataflow.create.next',
])

View File

@@ -6,7 +6,6 @@ namespace CodeRhapsodie\EzDataflowBundle\Form;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -32,7 +31,7 @@ class UpdateScheduledType extends AbstractType
],
'label' => 'coderhapsodie.dataflow.frequency',
])
->add('next', DateTimeType::class, [
->add('next', UserTimezoneAwareDateTimeType::class, [
'years' => range(date('Y'), date('Y') + 5),
'label' => 'coderhapsodie.dataflow.update.next',
])

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Form;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use eZ\Publish\API\Repository\UserPreferenceService;
use Symfony\Component\Form\DataTransformerInterface;
class UserTimezoneAwareDateTimeTransformer implements DataTransformerInterface
{
/** @var UserPreferenceService */
private $userPreferenceService;
public function __construct(UserPreferenceService $userPreferenceService)
{
$this->userPreferenceService = $userPreferenceService;
}
public function transform($value)
{
if (!$value instanceof \DateTimeInterface) {
return $value;
}
return (new \DateTime('now', $this->userTimezone()))->setTimestamp($value->getTimestamp());
}
public function reverseTransform($value)
{
if (!$value instanceof \DateTimeInterface) {
return $value;
}
$dateTimeWithUserTimeZone = new \DateTime($value->format('Y-m-d H:i:s'), $this->userTimezone());
return (new \DateTime())->setTimestamp($dateTimeWithUserTimeZone->getTimestamp());
}
private function userTimezone(): \DateTimeZone
{
try {
$tz = $this->userPreferenceService->getUserPreference('timezone')->value ?? 'UTC';
} catch (NotFoundException $e) {
$tz = 'UTC';
}
return new \DateTimeZone($tz);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Form;
use eZ\Publish\API\Repository\UserPreferenceService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormBuilderInterface;
class UserTimezoneAwareDateTimeType extends AbstractType
{
/** @var UserPreferenceService */
private $userPreferenceService;
public function __construct(UserPreferenceService $userPreferenceService)
{
$this->userPreferenceService = $userPreferenceService;
}
public function getParent()
{
return DateTimeType::class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer(new UserTimezoneAwareDateTimeTransformer($this->userPreferenceService));
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Gateway;
use Pagerfanta\Adapter\AdapterInterface;
class ExceptionJSONDecoderAdapter implements AdapterInterface
{
/** @var AdapterInterface */
private $adapter;
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
public function getNbResults()
{
return $this->adapter->getNbResults();
}
public function getSlice($offset, $length)
{
$slice = $this->adapter->getSlice($offset, $length);
array_walk($slice, static function (&$value) {
if (isset($value['exceptions'])) {
$value['exceptions'] = json_decode($value['exceptions'], true, 512, JSON_THROW_ON_ERROR);
}
});
return $slice;
}
}

View File

@@ -10,6 +10,9 @@ use Doctrine\DBAL\Query\QueryBuilder;
final class JobGateway
{
public const FILTER_NONE = 0;
public const FILTER_NON_EMPTY = 1;
/** @var JobRepository */
private $jobRepository;
@@ -30,10 +33,17 @@ final class JobGateway
->addOrderBy('i.requested_date', 'DESC');
}
public function getListQueryForAdmin(): QueryBuilder
public function getListQueryForAdmin(int $filter): QueryBuilder
{
return $this->jobRepository->createQueryBuilder('w')
->addOrderBy('w.requested_date', 'DESC');
$qb = $this->jobRepository->createQueryBuilder('w')
->addOrderBy('w.requested_date', 'DESC')
;
if (self::FILTER_NON_EMPTY === $filter) {
$qb->andWhere('w.count > 0');
}
return $qb;
}
public function getListQueryForScheduleAdmin(int $id): QueryBuilder
@@ -44,9 +54,6 @@ final class JobGateway
->addOrderBy('w.requested_date', 'DESC');
}
/**
* @param Job $job
*/
public function save(Job $job)
{
$this->jobRepository->save($job);

View File

@@ -29,17 +29,12 @@ final class ScheduledDataflowGateway
->addOrderBy('s.label', 'ASC');
}
/**
* @param ScheduledDataflow $scheduledDataflow
*/
public function save(ScheduledDataflow $scheduledDataflow)
{
$this->scheduledDataflowRepository->save($scheduledDataflow);
}
/**
* @param int $id
*
* @throws \Throwable
*/
public function delete(int $id): void

View File

@@ -22,8 +22,6 @@ class LocationMatcher implements LocationMatcherInterface
/**
* @param mixed $valueToMatch
*
* @return Location
*
* @throws NoMatchFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/

View File

@@ -10,8 +10,6 @@ interface LocationMatcherInterface
{
/**
* @param mixed $valueToMatch
*
* @return Location
*/
public function matchLocation($valueToMatch): Location;
}

View File

@@ -6,6 +6,7 @@ namespace CodeRhapsodie\EzDataflowBundle\Model;
use CodeRhapsodie\EzDataflowBundle\Exception\InvalidArgumentTypeException;
use eZ\Publish\API\Repository\Values\Content\Location;
use eZ\Publish\API\Repository\Values\Content\LocationCreateStruct;
class ContentCreateStructure extends ContentStructure
{
@@ -18,17 +19,14 @@ class ContentCreateStructure extends ContentStructure
/**
* ContentCreateStructure constructor.
*
* @param string $contentTypeIdentifier
* @param string $languageCode
* @param array $locations
* A location can be any of the following:
* <ul>
* <li>an integer, the id of the Location object</li>
* <li>a string, the remote id of the Location object</li>
* <li>a Location object</li>
* </ul>
* @param array $fields
* @param string|null $remoteId
* @param array $locations
* A location can be any of the following:
* <ul>
* <li>an integer, the id of the Location object</li>
* <li>a string, the remote id of the Location object</li>
* <li>a Location object</li>
* <li>a LocationCreateStruct object</li>
* </ul>
*
* @throws InvalidArgumentTypeException
*/
@@ -41,35 +39,28 @@ class ContentCreateStructure extends ContentStructure
$this->remoteId = $remoteId;
}
/**
* @return string
*/
public function getContentTypeIdentifier(): string
{
return $this->contentTypeIdentifier;
}
/**
* @return array
*/
public function getLocations(): array
{
return $this->locations;
}
/**
* @param array $locations
*
* @throws InvalidArgumentTypeException
*/
private function setLocations(array $locations)
{
foreach ($locations as $locationOrIdOrRemoteId) {
if (!is_int($locationOrIdOrRemoteId)
&& !is_string($locationOrIdOrRemoteId)
&& !$locationOrIdOrRemoteId instanceof Location
foreach ($locations as $locationOrIdOrRemoteIdOrStruct) {
if (!is_int($locationOrIdOrRemoteIdOrStruct)
&& !is_string($locationOrIdOrRemoteIdOrStruct)
&& !$locationOrIdOrRemoteIdOrStruct instanceof Location
&& !$locationOrIdOrRemoteIdOrStruct instanceof LocationCreateStruct
) {
throw InvalidArgumentTypeException::create(['int', 'string', Location::class], $locationOrIdOrRemoteId);
throw InvalidArgumentTypeException::create(['int', 'string', Location::class, LocationCreateStruct::class], $locationOrIdOrRemoteIdOrStruct);
}
}

View File

@@ -15,25 +15,16 @@ abstract class ContentStructure
/** @var array */
protected $fields;
/**
* @return string|null
*/
public function getRemoteId(): ?string
{
return $this->remoteId;
}
/**
* @return string
*/
public function getLanguageCode(): string
{
return $this->languageCode;
}
/**
* @return array
*/
public function getFields(): array
{
return $this->fields;

View File

@@ -15,13 +15,6 @@ class ContentUpdateStructure extends ContentStructure
$this->fields = $fields;
}
/**
* @param int $id
* @param string $languageCode
* @param array $fields
*
* @return ContentUpdateStructure
*/
public static function createForContentId(int $id, string $languageCode, array $fields): self
{
$struct = new self($languageCode, $fields);
@@ -30,13 +23,6 @@ class ContentUpdateStructure extends ContentStructure
return $struct;
}
/**
* @param string $remoteId
* @param string $languageCode
* @param array $fields
*
* @return ContentUpdateStructure
*/
public static function createForContentRemoteId(string $remoteId, string $languageCode, array $fields): self
{
$struct = new self($languageCode, $fields);
@@ -45,9 +31,6 @@ class ContentUpdateStructure extends ContentStructure
return $struct;
}
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;

View File

@@ -1,3 +1,6 @@
imports:
- { resource: services/comparators.yaml }
services:
_defaults:
public: false
@@ -6,25 +9,39 @@ services:
CodeRhapsodie\EzDataflowBundle\Controller\DashboardController:
public: true
tags:
- { name: controller.service_arguments }
arguments:
$jobGateway: '@CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway'
$notificationHandler: '@EzSystems\EzPlatformAdminUi\Notification\NotificationHandlerInterface'
$scheduledDataflowGateway: '@CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway'
calls:
- ['setContainer', ['@service_container']]
- ['performAccessCheck', []]
CodeRhapsodie\EzDataflowBundle\Controller\ScheduledDataflowController:
public: true
tags:
- { name: controller.service_arguments }
arguments:
$jobGateway: '@CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway'
$notificationHandler: '@EzSystems\EzPlatformAdminUi\Notification\NotificationHandlerInterface'
$scheduledDataflowGateway: '@CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway'
$translator: '@translator'
calls:
- [ 'setContainer', [ '@service_container' ] ]
- [ 'performAccessCheck', [ ] ]
CodeRhapsodie\EzDataflowBundle\Controller\JobController:
public: true
tags:
- { name: controller.service_arguments }
arguments:
$jobGateway: '@CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway'
$notificationHandler: '@EzSystems\EzPlatformAdminUi\Notification\NotificationHandlerInterface'
$translator: '@translator'
calls:
- [ 'setContainer', [ '@service_container' ] ]
- [ 'performAccessCheck', [ ] ]
CodeRhapsodie\EzDataflowBundle\Writer\RepositoryWriter:
abstract: true
@@ -99,6 +116,11 @@ services:
CodeRhapsodie\EzDataflowBundle\Form\CreateOneshotType:
tags: ['form.type']
CodeRhapsodie\EzDataflowBundle\Form\UserTimezoneAwareDateTimeType:
arguments:
$userPreferenceService: '@eZ\Publish\API\Repository\UserPreferenceService'
tags: ['form.type']
CodeRhapsodie\EzDataflowBundle\Gateway\ScheduledDataflowGateway:
arguments:
$scheduledDataflowRepository: '@CodeRhapsodie\DataflowBundle\Repository\ScheduledDataflowRepository'
@@ -110,17 +132,48 @@ services:
CodeRhapsodie\EzDataflowBundle\Tab\RepeatingTab:
parent: EzSystems\EzPlatformAdminUi\Tab\AbstractTab
public: false
arguments:
$httpKernelRuntime: '@twig.runtime.httpkernel'
tags:
- {name: ezplatform.tab, group: coderhapsodie-ezdataflow}
CodeRhapsodie\EzDataflowBundle\Tab\OneshotTab:
parent: EzSystems\EzPlatformAdminUi\Tab\AbstractTab
public: false
arguments:
$httpKernelRuntime: '@twig.runtime.httpkernel'
tags:
- {name: ezplatform.tab, group: coderhapsodie-ezdataflow}
CodeRhapsodie\EzDataflowBundle\Tab\HistoryTab:
parent: EzSystems\EzPlatformAdminUi\Tab\AbstractTab
public: false
arguments:
$httpKernelRuntime: '@twig.runtime.httpkernel'
tags:
- {name: ezplatform.tab, group: coderhapsodie-ezdataflow}
CodeRhapsodie\EzDataflowBundle\Filter\NotModifiedContentFilter:
arguments:
$contentService: '@eZ\Publish\API\Repository\ContentService'
$comparator: '@CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface'
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface: '@CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator'
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator:
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator:
arguments:
$fieldTypeService: '@eZ\Publish\API\Repository\FieldTypeService'
abstract: true
_cr.admin_tabs.ezdataflow_group:
parent: EzSystems\EzPlatformAdminUi\Component\TabsComponent
autowire: true
autoconfigure: false
public: false
arguments:
$template: '@@ezdesign/ezdataflow/parts/tab/ezdataflow.html.twig'
$groupIdentifier: 'coderhapsodie-ezdataflow'
tags:
- { name: ezplatform.admin_ui.component, group: 'coderhapsodie-ezdataflow' }

View File

@@ -0,0 +1,42 @@
services:
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\SimpleFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezauthor' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezboolean' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezcountry' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezdate' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezdatetime' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezemail' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezfloat' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezinteger' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezisbn' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezkeyword' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezobjectrelation' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezobjectrelationlist' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezrichtext' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezselection' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'eztext' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezstring' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'eztime' }
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'eztags' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\UrlFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezurl' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\NovaSEOMetasFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'novaseometas' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\MatrixFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezmatrix' }
CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\MapLocationFieldComparator:
parent: 'CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\AbstractFieldComparator'
tags:
- { name: 'coderhapsodie.ezdataflow.field_comparator', fieldType: 'ezgmaplocation' }

View File

@@ -20,11 +20,16 @@ coderhapsodie.ezdataflow.history.title: History
coderhapsodie.ezdataflow.history.list.title: 'Executions list'
coderhapsodie.ezdataflow.history.list.name: Name
coderhapsodie.ezdataflow.history.list.request: 'Requested on'
coderhapsodie.ezdataflow.history.list.count: 'Items count'
coderhapsodie.ezdataflow.history.list.count: 'Number of successes'
coderhapsodie.ezdataflow.history.list.errors: 'Number of errors'
coderhapsodie.ezdataflow.history.list.start: 'Started on'
coderhapsodie.ezdataflow.history.list.end: 'Finished on'
coderhapsodie.ezdataflow.history.list.view: 'View details'
coderhapsodie.ezdataflow.history.list.status: Status
coderhapsodie.ezdataflow.history.filter.label: Filter results
coderhapsodie.ezdataflow.history.filter.none: All results
coderhapsodie.ezdataflow.history.filter.non_empty_only: Only non empty
coderhapsodie.ezdataflow.history.filter.with_error_only: Only with errors
coderhapsodie.ezdataflow.job.status.pending: Pending
coderhapsodie.ezdataflow.job.status.running: Running
coderhapsodie.ezdataflow.job.status.complete: Completed
@@ -36,16 +41,18 @@ coderhapsodie.ezdataflow.history.details.request: 'Requested on'
coderhapsodie.ezdataflow.history.details.status: Status
coderhapsodie.ezdataflow.history.details.start: 'Started on'
coderhapsodie.ezdataflow.history.details.end: 'Finished on'
coderhapsodie.ezdataflow.history.details.count: 'Items count'
coderhapsodie.ezdataflow.history.details.count: 'Items successfully processed'
coderhapsodie.ezdataflow.history.details.options: 'Run options'
coderhapsodie.ezdataflow.history.details.errors: Errors
coderhapsodie.ezdataflow.history.details.type: 'Name of the dataflow executed'
coderhapsodie.ezdataflow.history.details.log: 'View log'
coderhapsodie.ezdataflow.workflow.repeating.new.title: 'Add a new repeating dataflow'
coderhapsodie.ezdataflow.workflow.new.cancel: Cancel
coderhapsodie.ezdataflow.workflow.new.submit: Create
coderhapsodie.ezdataflow.history.list.empty: 'No execution yet.'
coderhapsodie.ezdataflow.workflow.list.empty: 'No repeating workflow configured yet'
coderhapsodie.ezdataflow.workflow.history.title: 'Execution history'
coderhapsodie.ezdataflow.workflow.log.title: 'Execution log'
coderhapsodie.ezdataflow.workflow.list.delete: Delete
coderhapsodie.ezdataflow.workflow.delete: 'Are you sure you want to delete this dataflow schedule?'
coderhapsodie.ezdataflow.workflow.create.success: 'Dataflow schedule successfully added.'

View File

@@ -20,7 +20,8 @@ coderhapsodie.ezdataflow.history.title: Historique
coderhapsodie.ezdataflow.history.list.title: 'Liste des exécutions'
coderhapsodie.ezdataflow.history.list.name: Nom
coderhapsodie.ezdataflow.history.list.request: 'Demandé le'
coderhapsodie.ezdataflow.history.list.count: 'Nombre d''objets'
coderhapsodie.ezdataflow.history.list.count: 'Nombre de succès'
coderhapsodie.ezdataflow.history.list.errors: 'Nombre d''erreurs'
coderhapsodie.ezdataflow.history.list.start: 'Commencé le'
coderhapsodie.ezdataflow.history.list.end: 'Terminé le'
coderhapsodie.ezdataflow.history.list.view: 'Voir le détail'
@@ -40,12 +41,14 @@ coderhapsodie.ezdataflow.history.details.count: 'Nombre d''objets mis à jour'
coderhapsodie.ezdataflow.history.details.options: 'Options de lancement'
coderhapsodie.ezdataflow.history.details.errors: Erreurs
coderhapsodie.ezdataflow.history.details.type: 'Nom du dataflow exécuté'
coderhapsodie.ezdataflow.history.details.log: 'Voir le log'
coderhapsodie.ezdataflow.workflow.repeating.new.title: 'Nouvel programmation d''un dataflow récurrent'
coderhapsodie.ezdataflow.workflow.new.cancel: Annuler
coderhapsodie.ezdataflow.workflow.new.submit: Créer
coderhapsodie.ezdataflow.history.list.empty: 'Aucune exécution pour le moment.'
coderhapsodie.ezdataflow.workflow.list.empty: 'Aucun dataflow n''a été programmé.'
coderhapsodie.ezdataflow.workflow.history.title: 'Historique des exécutions'
coderhapsodie.ezdataflow.workflow.log.title: 'Log de l''exécution'
coderhapsodie.ezdataflow.workflow.list.delete: Supprimer
coderhapsodie.ezdataflow.workflow.delete: 'Êtes-vous sûr de vouloir supprimer ce dataflow ?'
coderhapsodie.ezdataflow.workflow.create.success: 'La programmation du dataflow a bien été ajoutée.'

View File

@@ -2,6 +2,34 @@
<div class="ez-table-header">
<div class="ez-table-header__headline">{{ 'coderhapsodie.ezdataflow.history.list.title'|trans }}</div>
<div class="ez-table-header__tools">
<label for="ezdataflow_history_filter">{{ 'coderhapsodie.ezdataflow.history.filter.label'|trans }}</label>
<select id="ezdataflow_history_filter" name="filter">
<option value="0" {{ filter == 0 ? 'selected="selected"' }}>{{ 'coderhapsodie.ezdataflow.history.filter.none'|trans }}</option>
<option value="1" {{ filter == 1 ? 'selected="selected"' }}>{{ 'coderhapsodie.ezdataflow.history.filter.non_empty_only'|trans }}</option>
</select>
</div>
</div>
{% include '@ezdesign/ezdataflow/parts/tab/job_list.html.twig' with {identifier: 'ezdataflow_history_results', paginate_route: 'coderhapsodie.ezdataflow.history'} %}
{% include '@ezdesign/ezdataflow/parts/tab/job_list.html.twig' with {
identifier: 'ezdataflow_history_results',
paginate_route: 'coderhapsodie.ezdataflow.history',
paginate_params: {'filter': filter}
} %}
<script>
(function ($) {
$(document).ready(function ($) {
// Manage ajax pagination
$('#ezdataflow_history_filter').change(function (e) {
e.preventDefault();
$('#loading_ezdataflow_history_results').removeClass('d-none');
$('#ezdataflow_history_results')
.html('')
.load('{{ path('coderhapsodie.ezdataflow.history') }}?filter=' + this.value + ' #ezdataflow_history_results>*', null, function () {
$('#loading_ezdataflow_history_results').addClass('d-none');
});
});
});
})(jQuery);
</script>

View File

@@ -1,21 +1,23 @@
{% extends "@ezdesign/layout.html.twig" %}
{% extends ["@ezdesign/layout.html.twig", "@ezdesign/ui/layout.html.twig"] %}
{% block body_class %}ez-has-full-width-footer{% endblock %}
{% block breadcrumbs %}
{% include '@ezdesign/parts/breadcrumbs.html.twig' with { items: [
{% include ['@ezdesign/parts/breadcrumbs.html.twig', '@ezdesign/ui/breadcrumbs.html.twig'] with { items: [
{ value: 'breadcrumb.admin'|trans(domain='messages')|desc('Admin') },
{ value: 'coderhapsodie.ezdataflow'|trans|desc('EzDataflow') }
]} %}
{% endblock %}
{% block page_title %}
{% include '@ezdesign/parts/page_title.html.twig' with {
{% include ['@ezdesign/parts/page_title.html.twig', '@ezdesign/ui/page_title.html.twig'] with {
title: 'coderhapsodie.ezdataflow'|trans|desc('EzDataflow'),
iconName: 'workflow'
} %}
{% endblock %}
{% block content %}
{{ ez_platform_tabs('coderhapsodie-ezdataflow', {}, '@ezdesign/ezdataflow/parts/tab/ezdataflow.html.twig') }}
{{ ez_render_component_group('coderhapsodie-ezdataflow', {'filter': app.request.query.get('filter', 0)}, '@ezdesign/ezdataflow/parts/tab/ezdataflow.html.twig') }}
<div class="modal fade ez-modal show" id="ez-modal--history-details" tabindex="-1" role="dialog" aria-modal="true">
<div class="modal-dialog" role="document" style="max-width: 90%">
@@ -42,6 +44,10 @@
$('.history-details-aware').delegate('.modal-history-details', 'click', function (e) {
e.preventDefault();
$('#modal_content-details').html('');
$('#ez-modal--history-details h3').html("{{ 'coderhapsodie.ezdataflow.workflow.history.title'|trans }}");
if ($(this).hasClass('modal-history-log')) {
$('#ez-modal--history-details h3').html("{{ 'coderhapsodie.ezdataflow.workflow.log.title'|trans }}");
}
$('#ez-modal--history-details').modal('show');
$.ajax(this.href, {
success: function (result) {

View File

@@ -40,9 +40,10 @@
success: function (result) {
if (result.redirect) {
if (window.location.href === result.redirect) {
document.location.reload();
window.location.reload();
}
window.location = result.redirect;
window.location.reload();
return;
}

View File

@@ -125,9 +125,10 @@
success: function (result) {
if (result.redirect) {
if (window.location.href === result.redirect) {
document.location.reload();
window.location.reload();
}
window.location = result.redirect;
window.location.reload();
return;
}

View File

@@ -1,8 +1,7 @@
{% import '@ezdesign/ezdataflow/macros.twig' as macros %}
{% block content %}
<div class="container ez-main-container">
<div class="container ez-main-container history-details-aware">
{% if item is not null %}
<h2>{{ 'coderhapsodie.ezdataflow.history.job.title'|trans }}{{ item.id }}</h2>
@@ -21,7 +20,7 @@
</tr>
<tr>
<td>{{ 'coderhapsodie.ezdataflow.history.details.request'|trans }}</td>
<td>{{ item.requestedDate|date('d/m/Y H:i:s') }}</td>
<td>{{ date(item.requestedDate)|ez_short_datetime }}</td>
</tr>
<tr>
<td>{{ 'coderhapsodie.ezdataflow.history.details.status'|trans }}</td>
@@ -29,11 +28,11 @@
</tr>
<tr>
<td>{{ 'coderhapsodie.ezdataflow.history.details.start'|trans }}</td>
<td>{{ item.startTime|date('d/m/Y H:i:s') }}</td>
<td>{{ item.startTime ? date(item.startTime)|ez_short_datetime : '—' }}</td>
</tr>
<tr>
<td>{{ 'coderhapsodie.ezdataflow.history.details.end'|trans }}</td>
<td>{{ item.endTime|date('d/m/Y H:i:s') }}</td>
<td>{{ item.endTime ? date(item.endTime)|ez_short_datetime : '—' }}</td>
</tr>
<tr>
<td>{{ 'coderhapsodie.ezdataflow.history.details.count'|trans }}</td>

View File

@@ -0,0 +1,3 @@
{% for line in log %}
<p>{{ line|nl2br }}</p>
{% endfor %}

View File

@@ -17,6 +17,7 @@
<th>{{ 'coderhapsodie.ezdataflow.history.list.name'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.history.list.request'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.history.list.count'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.history.list.errors'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.history.list.start'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.history.list.end'|trans }}</th>
<th>{{ 'coderhapsodie.ezdataflow.history.list.status'|trans }}</th>
@@ -27,10 +28,11 @@
{% for job in pager.currentPageResults %}
<tr>
<td>{{ job.label }}</td>
<td>{{ job.requested_date|date('d/m/Y H:i:s') }}</td>
<td>{{ job.count|default('-') }}</td>
<td>{{ job.start_time ? job.start_time|date('d/m/Y H:i:s') : '-' }}</td>
<td>{{ job.end_time ? job.end_time|date('d/m/Y H:i:s') : '-' }}</td>
<td>{{ date(job.requested_date)|ez_short_datetime }}</td>
<td>{{ job.count|default('') }}</td>
<td>{{ job.exceptions|length }}</td>
<td>{{ job.start_time ? date(job.start_time)|ez_short_datetime : '' }}</td>
<td>{{ job.end_time ? date(job.end_time)|ez_short_datetime : '—' }}</td>
<td>{{ macros.translateStatus(job.status) }}</td>
<td class="ez-table__cell ez-table__cell--has-action-btns text-right">
<a href="{{ path('coderhapsodie.ezdataflow.job.details', {id: job.id}) }}"
@@ -41,6 +43,14 @@
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#about-info"></use>
</svg>
</a>
<a href="{{ path('coderhapsodie.ezdataflow.job.log', {id: job.id}) }}"
class="btn btn-icon mx-2 modal-history-details modal-history-log"
title="{{ 'coderhapsodie.ezdataflow.history.details.log'|trans }}">
<svg class="ez-icon ez-icon--small-medium">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="/bundles/ezplatformadminui/img/ez-icons.svg#article"></use>
</svg>
</a>
</td>
</tr>
{% endfor %}
@@ -58,7 +68,6 @@
</div>
<script>
(function ($) {
$(document).ready(function ($) {
// Manage ajax pagination

View File

@@ -24,7 +24,7 @@
<tr>
<td>{{ item.label }}</td>
<td>{{ item.frequency }}</td>
<td>{{ item.next|date('d/m/Y H:i:s') }}</td>
<td>{{ date(item.next)|ez_short_datetime }}</td>
<td>{{ ('coderhapsodie.ezdataflow.' ~ (item.enabled ? 'yes' : 'no'))|trans }}</td>
<td class="ez-table__cell ez-table__cell--has-action-btns text-right">
<a href="{{ path('coderhapsodie.ezdataflow.history.workflow', {id: item.id}) }}"

View File

@@ -16,7 +16,7 @@ class HistoryTab extends AbstractControllerBasedTab implements OrderedTabInterfa
*/
public function getControllerReference(array $parameters): ControllerReference
{
return new ControllerReference(DashboardController::class.'::history');
return new ControllerReference(DashboardController::class.'::history', [], $parameters);
}
/**

View File

@@ -9,9 +9,6 @@ trait UserSwitcherAwareTrait
/** @var UserSwitcherInterface */
protected $userSwitcher;
/**
* @param UserSwitcherInterface $userSwitcher
*/
public function setUserSwitcher(UserSwitcherInterface $userSwitcher): void
{
$this->userSwitcher = $userSwitcher;

View File

@@ -4,15 +4,18 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Writer;
use CodeRhapsodie\DataflowBundle\DataflowType\Writer\DelegateWriterInterface;
use CodeRhapsodie\EzDataflowBundle\Core\Content\ContentCreatorInterface;
use CodeRhapsodie\EzDataflowBundle\Core\Content\ContentUpdaterInterface;
use CodeRhapsodie\EzDataflowBundle\Model\ContentCreateStructure;
use CodeRhapsodie\EzDataflowBundle\Model\ContentStructure;
use CodeRhapsodie\EzDataflowBundle\Model\ContentUpdateStructure;
use CodeRhapsodie\DataflowBundle\DataflowType\Writer\WriterInterface;
use Psr\Log\LoggerAwareTrait;
class ContentWriter extends RepositoryWriter implements WriterInterface
class ContentWriter extends RepositoryWriter implements DelegateWriterInterface
{
use LoggerAwareTrait;
/** @var ContentCreatorInterface */
private $creator;
@@ -31,15 +34,40 @@ class ContentWriter extends RepositoryWriter implements WriterInterface
public function write($item)
{
if (!$item instanceof ContentStructure) {
$this->log('warning', 'Data is not a ContentStucture');
return;
}
if ($item instanceof ContentCreateStructure) {
$this->log('info', 'Save content', [
'content_type' => $item->getContentTypeIdentifier(),
'content_location' => $item->getLocations(),
]);
return $this->creator->createFromStructure($item);
}
if ($item instanceof ContentUpdateStructure) {
$this->log('info', 'Update content', ['id' => $item->getId(), 'remote_id' => $item->getRemoteId()]);
return $this->updater->updateFromStructure($item);
}
}
/**
* {@inheritdoc}
*/
public function supports($item): bool
{
return $item instanceof ContentStructure;
}
private function log(string $level, string $message, array $context = [])
{
if (null === $this->logger) {
return;
}
$this->logger->log($level, $message, $context);
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Writer;
use CodeRhapsodie\DataflowBundle\DataflowType\Writer\WriterInterface;
use CodeRhapsodie\EzDataflowBundle\UserSwitcher\UserSwitcherAwareInterface;
use CodeRhapsodie\EzDataflowBundle\UserSwitcher\UserSwitcherAwareTrait;
use CodeRhapsodie\DataflowBundle\DataflowType\Writer\WriterInterface;
abstract class RepositoryWriter implements WriterInterface, UserSwitcherAwareInterface
{

View File

@@ -0,0 +1,49 @@
<?php
namespace CodeRhapsodie\EzDataflowBundle\Tests\Core\FieldComparator;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\DelegatorFieldComparator;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface;
use eZ\Publish\API\Repository\Values\Content\Field;
use PHPUnit\Framework\TestCase;
class DelegatorFieldComparatorTest extends TestCase
{
/** @var DelegatorFieldComparator */
private $delegatorFieldComparator;
protected function setUp(): void
{
$type1FieldComparatorMock = $this->createMock(FieldComparatorInterface::class);
$type1FieldComparatorMock->method('compare')->willReturnCallback(function (Field $field, $hash) {
return $hash === 'rightValue1';
});
$type2FieldComparatorMock = $this->createMock(FieldComparatorInterface::class);
$type2FieldComparatorMock->method('compare')->willReturnCallback(function (Field $field, $hash) {
return $hash === 'rightValue2';
});
$this->delegatorFieldComparator = new DelegatorFieldComparator();
$this->delegatorFieldComparator->registerDelegateFieldComparator($type1FieldComparatorMock, 'type1');
$this->delegatorFieldComparator->registerDelegateFieldComparator($type2FieldComparatorMock, 'type2');
}
/**
* @dataProvider fieldProvider
*/
public function testField(string $type, bool $expected, $hash)
{
$field = new Field(['fieldTypeIdentifier' => $type]);
$return = $this->delegatorFieldComparator->compare($field, $hash);
$this->assertSame($expected, $return);
}
public function fieldProvider(): iterable
{
yield ['type1', true, 'rightValue1'];
yield ['type1', false, 'wrongValue'];
yield ['type2', true, 'rightValue2'];
yield ['type2', false, 'wrongValue'];
yield ['otherType', false, 'rightValue1'];
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\EzDataflowBundle\Tests\Filter;
use CodeRhapsodie\EzDataflowBundle\Core\FieldComparator\FieldComparatorInterface;
use CodeRhapsodie\EzDataflowBundle\Filter\NotModifiedContentFilter;
use CodeRhapsodie\EzDataflowBundle\Model\ContentUpdateStructure;
use eZ\Publish\API\Repository\ContentService;
use eZ\Publish\API\Repository\Values\Content\Field;
use eZ\Publish\Core\Repository\Values\Content\Content;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class NotModifiedContentFilterTest extends TestCase
{
/** @var ContentService|MockObject */
private $contentServiceMock;
/** @var FieldComparatorInterface|MockObject */
private $comparatorMock;
/** @var NotModifiedContentFilter */
private $notModifiedContentFilter;
protected function setUp(): void
{
$this->contentServiceMock = $this->createMock(ContentService::class);
$this->comparatorMock = $this->createMock(FieldComparatorInterface::class);
$this->notModifiedContentFilter = new NotModifiedContentFilter($this->contentServiceMock, $this->comparatorMock);
}
public function testNotContentUpdateStructure()
{
$data = 'notAStruct';
$returnValue = ($this->notModifiedContentFilter)($data);
$this->assertSame($data, $returnValue);
}
public function testIdenticalContent()
{
$id = 10;
$field1 = 'field1';
$value1 = 'value1';
$field2 = 'field2';
$value2 = 'value2';
$contentField1 = new Field();
$contentField2 = new Field();
$data = ContentUpdateStructure::createForContentId($id, 'lang', [
$field1 => $value1,
$field2 => $value2,
]);
$content = $this->createMock(Content::class);
$content
->expects($this->exactly(2))
->method('getField')
->withConsecutive([$field1], [$field2])
->willReturnOnConsecutiveCalls($contentField1, $contentField2)
;
$this->contentServiceMock
->expects($this->once())
->method('loadContent')
->with($id)
->willReturn($content)
;
$this->comparatorMock
->expects($this->exactly(2))
->method('compare')
->withConsecutive([$contentField1, $value1], [$contentField2, $value2])
->willReturn(true)
;
$return = ($this->notModifiedContentFilter)($data);
$this->assertFalse($return);
}
public function testDifferentContent()
{
$id = 10;
$field1 = 'field1';
$value1 = 'value1';
$field2 = 'field2';
$value2 = 'value2';
$field3 = 'field3';
$value3 = 'value3';
$contentField1 = new Field();
$contentField2 = new Field();
$data = ContentUpdateStructure::createForContentId($id, 'lang', [
$field1 => $value1,
$field2 => $value2,
$field3 => $value3,
]);
$content = $this->createMock(Content::class);
$content
->expects($this->exactly(2))
->method('getField')
->withConsecutive([$field1], [$field2])
->willReturnOnConsecutiveCalls($contentField1, $contentField2)
;
$this->contentServiceMock
->expects($this->once())
->method('loadContent')
->with($id)
->willReturn($content)
;
$this->comparatorMock
->expects($this->exactly(2))
->method('compare')
->withConsecutive([$contentField1, $value1], [$contentField2, $value2])
->willReturnOnConsecutiveCalls(true, false)
;
$return = ($this->notModifiedContentFilter)($data);
$this->assertSame($data, $return);
}
public function testLoadEmptyByRemoteId()
{
$remoteId = 'abc';
$data = ContentUpdateStructure::createForContentRemoteId($remoteId, 'lang', []);
$this->contentServiceMock
->expects($this->once())
->method('loadContentByRemoteId')
->with($remoteId)
->willReturn(new Content())
;
$return = ($this->notModifiedContentFilter)($data);
$this->assertFalse($return);
}
}

11
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
// Skip autoloading if already done by phpunit alias (including from meta repo if this is vendor)
if (defined('PHPUNIT_COMPOSER_INSTALL')) {
return;
}
$autoloadFile = __DIR__ . '/../vendor/autoload.php';
if (!file_exists($autoloadFile)) {
throw new RuntimeException('Install dependencies to run test suite.');
}
require_once $autoloadFile;