4 Commits
v2.1.0 ... v2.x

Author SHA1 Message Date
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
28 changed files with 713 additions and 13 deletions

View File

@@ -1,5 +1,15 @@
# 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,7 +41,8 @@
}
},
"require": {
"code-rhapsodie/dataflow-bundle": "^2.0 || dev-master",
"php": "^7.1",
"code-rhapsodie/dataflow-bundle": "^2.1 || dev-master",
"ezsystems/ezplatform-admin-ui": "^1.0",
"ezsystems/ezpublish-kernel": "^7.0"
},

View File

@@ -4,7 +4,7 @@
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="Tests/bootstrap.php"
bootstrap="tests/bootstrap.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
@@ -14,16 +14,16 @@
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Port tests suite">
<directory suffix="Test.php">./Tests</directory>
<testsuite name="EzDataflow tests suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<directory>./src/</directory>
<exclude>
<directory>Tests/</directory>
<directory>tests/</directory>
<directory>vendor/</directory>
</exclude>
</whitelist>

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

@@ -55,6 +55,26 @@ class JobController extends Controller
]);
}
/**
* @Route("/details/log/{id}", name="coderhapsodie.ezdataflow.job.log")
*
* @param int $id
*
* @return Response
*/
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"})
*

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

@@ -0,0 +1,64 @@
<?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 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 {
$content = $this->contentService->loadContentByRemoteId($data->getRemoteId(), [$data->getLanguageCode()]);
}
foreach ($data->getFields() as $identifier => $hash) {
$field = $content->getField($identifier, $data->getLanguageCode());
if ($field === null || !$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 ($this->logger === null) {
return;
}
$this->logger->log($level, $message, $context);
}
}

View File

@@ -1,3 +1,6 @@
imports:
- { resource: services/comparators.yaml }
services:
_defaults:
public: false
@@ -124,3 +127,16 @@ services:
public: false
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

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

@@ -36,16 +36,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

@@ -40,12 +40,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

@@ -42,6 +42,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

@@ -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>

View File

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

View File

@@ -41,6 +41,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 %}

View File

@@ -4,15 +4,19 @@ 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;
class ContentWriter extends RepositoryWriter implements WriterInterface
use Psr\Log\LoggerAwareTrait;
class ContentWriter extends RepositoryWriter implements DelegateWriterInterface
{
use LoggerAwareTrait;
/** @var ContentCreatorInterface */
private $creator;
@@ -31,15 +35,37 @@ 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 ($this->logger === null) {
return;
}
$this->logger->log($level, $message, $context);
}
}

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;