Skip to content

Commit 3dc40da

Browse files
authored
Allow ItemJob to skip items for any reason (#41)
* Replace InvalidItemException with SkipItemException and let developer what to do with these exception * Handle impact of SkipInvalidItemException on symfony/validator bridge * Handle impact of SkipInvalidItemException on symfony/serializer bridge * Enhanced documentation of sources * Assert skip item exception are updating summary
1 parent 803d7f5 commit 3dc40da

7 files changed

+252
-177
lines changed

src/SkipInvalidItemProcessor.php

Lines changed: 16 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
namespace Yokai\Batch\Bridge\Symfony\Validator;
66

7-
use DateTimeInterface;
7+
use Iterator;
88
use Symfony\Component\Validator\Constraint;
9-
use Symfony\Component\Validator\ConstraintViolationInterface;
109
use Symfony\Component\Validator\Validator\ValidatorInterface;
11-
use Yokai\Batch\Job\Item\InvalidItemException;
10+
use Yokai\Batch\Job\Item\Exception\SkipItemException;
1211
use Yokai\Batch\Job\Item\ItemProcessorInterface;
1312

13+
/**
14+
* This {@see ItemProcessorInterface} uses Symfony's validator to validate items.
15+
* When an item is not valid, it throw a {@see SkipItemException} with a {@see SkipItemOnViolations} cause.
16+
*/
1417
final class SkipInvalidItemProcessor implements ItemProcessorInterface
1518
{
1619
private ValidatorInterface $validator;
@@ -42,62 +45,25 @@ public function __construct(ValidatorInterface $validator, array $contraints = n
4245
public function process($item)
4346
{
4447
$violations = $this->validator->validate($item, $this->contraints, $this->groups);
45-
if (count($violations) === 0) {
48+
if (\count($violations) === 0) {
4649
return $item;
4750
}
4851

49-
$issues = [];
50-
/** @var ConstraintViolationInterface $violation */
51-
foreach ($violations as $violation) {
52-
$issues[] = sprintf(
53-
'%s: %s: %s',
54-
$violation->getPropertyPath(),
55-
$violation->getMessage(),
56-
$this->normalizeInvalidValue($violation->getInvalidValue())
57-
);
58-
}
59-
60-
throw new InvalidItemException(implode(PHP_EOL, $issues));
52+
throw new SkipItemException($item, new SkipItemOnViolations($violations), [
53+
'constraints' => \iterator_to_array($this->normalizeConstraints($this->contraints)),
54+
'groups' => $this->groups,
55+
]);
6156
}
6257

6358
/**
64-
* @param mixed $invalidValue
59+
* @param Constraint[]|null $constraints
6560
*
66-
* @return integer|float|string|boolean
61+
* @phpstan-return Iterator<string>
6762
*/
68-
private function normalizeInvalidValue($invalidValue)
63+
private function normalizeConstraints(?array $constraints): Iterator
6964
{
70-
if ($invalidValue === '') {
71-
return '""';
72-
}
73-
if ($invalidValue === null) {
74-
return 'NULL';
75-
}
76-
if (is_scalar($invalidValue)) {
77-
return $invalidValue;
78-
}
79-
80-
if (is_iterable($invalidValue)) {
81-
$invalidValues = [];
82-
foreach ($invalidValue as $value) {
83-
$invalidValues[] = $this->normalizeInvalidValue($value);
84-
}
85-
86-
return implode(', ', $invalidValues);
87-
}
88-
89-
if (is_object($invalidValue)) {
90-
if ($invalidValue instanceof DateTimeInterface) {
91-
return $invalidValue->format(DateTimeInterface::ISO8601);
92-
}
93-
94-
if (method_exists($invalidValue, '__toString')) {
95-
return (string)$invalidValue;
96-
}
97-
98-
return sprintf('%s:%s', get_class($invalidValue), spl_object_hash($invalidValue));
65+
foreach ($constraints ?? [] as $constraint) {
66+
yield \get_class($constraint);
9967
}
100-
101-
return gettype($invalidValue);
10268
}
10369
}

src/SkipItemOnViolations.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yokai\Batch\Bridge\Symfony\Validator;
6+
7+
use DateTimeInterface;
8+
use Symfony\Component\Validator\ConstraintViolationInterface;
9+
use Symfony\Component\Validator\ConstraintViolationListInterface;
10+
use Yokai\Batch\Job\Item\Exception\SkipItemCauseInterface;
11+
use Yokai\Batch\JobExecution;
12+
use Yokai\Batch\Warning;
13+
14+
/**
15+
* Skip item when validation fails and leave a warning with violations to the {@see JobExecution}.
16+
*/
17+
final class SkipItemOnViolations implements SkipItemCauseInterface
18+
{
19+
/**
20+
* @phpstan-var ConstraintViolationListInterface<ConstraintViolationInterface>
21+
*/
22+
private ConstraintViolationListInterface $violations;
23+
24+
/**
25+
* @phpstan-param ConstraintViolationListInterface<ConstraintViolationInterface> $violations
26+
*/
27+
public function __construct(ConstraintViolationListInterface $violations)
28+
{
29+
$this->violations = $violations;
30+
}
31+
32+
/**
33+
* @inheritdoc
34+
*/
35+
public function report(JobExecution $execution, $index, $item): void
36+
{
37+
$execution->getSummary()->increment('invalid');
38+
$violations = [];
39+
/** @var ConstraintViolationInterface $violation */
40+
foreach ($this->violations as $violation) {
41+
$violations[] = \sprintf(
42+
'%s: %s (invalid value: %s)',
43+
$violation->getPropertyPath(),
44+
$violation->getMessage(),
45+
$this->normalizeInvalidValue($violation->getInvalidValue())
46+
);
47+
}
48+
49+
$execution->addWarning(
50+
new Warning(
51+
'Violations were detected by validator.',
52+
[],
53+
['itemIndex' => $index, 'item' => $item, 'violations' => $violations]
54+
)
55+
);
56+
}
57+
58+
/**
59+
* @phpstan-return ConstraintViolationListInterface<ConstraintViolationInterface>
60+
*/
61+
public function getViolations(): ConstraintViolationListInterface
62+
{
63+
return $this->violations;
64+
}
65+
66+
/**
67+
* @param mixed $invalidValue
68+
*/
69+
private function normalizeInvalidValue($invalidValue): string
70+
{
71+
if ($invalidValue === '') {
72+
return '""';
73+
}
74+
if ($invalidValue === null) {
75+
return 'NULL';
76+
}
77+
if (\is_scalar($invalidValue)) {
78+
return (string)$invalidValue;
79+
}
80+
81+
if (\is_iterable($invalidValue)) {
82+
$invalidValues = [];
83+
foreach ($invalidValue as $value) {
84+
$invalidValues[] = $this->normalizeInvalidValue($value);
85+
}
86+
87+
return \implode(', ', $invalidValues);
88+
}
89+
90+
if (\is_object($invalidValue)) {
91+
if ($invalidValue instanceof DateTimeInterface) {
92+
return $invalidValue->format(DateTimeInterface::ATOM);
93+
}
94+
95+
if (\method_exists($invalidValue, '__toString')) {
96+
return (string)$invalidValue;
97+
}
98+
99+
return \get_class($invalidValue);
100+
}
101+
102+
return \gettype($invalidValue);
103+
}
104+
}

tests/Fixtures/EmptyClass.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yokai\Batch\Tests\Bridge\Symfony\Validator\Fixtures;
6+
7+
final class EmptyClass
8+
{
9+
}

tests/Fixtures/ObjectWithAnnotationValidation.php

Lines changed: 0 additions & 72 deletions
This file was deleted.

tests/Fixtures/StringableClass.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yokai\Batch\Tests\Bridge\Symfony\Validator\Fixtures;
6+
7+
final class StringableClass
8+
{
9+
public function __toString(): string
10+
{
11+
return '__toString';
12+
}
13+
}

0 commit comments

Comments
 (0)