• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wol-soft / php-json-schema-model-generator / 24554558572

17 Apr 2026 08:00AM UTC coverage: 98.033% (-0.6%) from 98.654%
24554558572

Pull #125

github

wol-soft
cleanup
Pull Request #125: attributes

1793 of 1839 new or added lines in 80 files covered. (97.5%)

1 existing line in 1 file now uncovered.

4535 of 4626 relevant lines covered (98.03%)

609.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.64
/src/Model/Validator/FilterValidator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Model\Validator;
6

7
use PHPModelGenerator\Exception\Filter\IncompatibleFilterException;
8
use PHPModelGenerator\Exception\Filter\InvalidFilterValueException;
9
use PHPModelGenerator\Exception\SchemaException;
10
use PHPModelGenerator\Filter\FilterInterface;
11
use PHPModelGenerator\Filter\TransformingFilterInterface;
12
use PHPModelGenerator\Model\GeneratorConfiguration;
13
use PHPModelGenerator\Model\Property\PropertyInterface;
14
use PHPModelGenerator\Utils\FilterReflection;
15
use PHPModelGenerator\Utils\RenderHelper;
16
use PHPModelGenerator\Utils\TypeCheck;
17
use ReflectionException;
18

19
/**
20
 * Class FilterValidator
21
 *
22
 * @package PHPModelGenerator\Model\Validator
23
 */
24
class FilterValidator extends PropertyTemplateValidator
25
{
26
    /**
27
     * FilterValidator constructor.
28
     *
29
     * @throws SchemaException
30
     * @throws ReflectionException
31
     */
32
    public function __construct(
147✔
33
        GeneratorConfiguration $generatorConfiguration,
34
        protected FilterInterface $filter,
35
        PropertyInterface $property,
36
        protected array $filterOptions = [],
37
        private readonly ?TransformingFilterInterface $transformingFilter = null,
38
    ) {
39
        $this->isResolved = true;
147✔
40

41
        $acceptedTypes = FilterReflection::getAcceptedTypes($this->filter, $property);
147✔
42

43
        $this->transformingFilter !== null
146✔
44
            ? $this->validateFilterCompatibilityWithTransformedType(
9✔
45
                $acceptedTypes,
9✔
46
                $this->transformingFilter,
9✔
47
                $property,
9✔
48
            )
9✔
49
            : $this->runCompatibilityCheck($acceptedTypes, $property);
146✔
50

51
        parent::__construct(
135✔
52
            $property,
135✔
53
            DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'Filter.phptpl',
135✔
54
            [
135✔
55
                'skipTransformedValuesCheck' => $this->transformingFilter !== null ? '!$transformationFailed' : '',
135✔
56
                'isTransformingFilter' => $this->filter instanceof TransformingFilterInterface,
135✔
57
                // Positive type guard: the filter only executes when the value's runtime type
58
                // matches one of the acceptedTypes. Non-matching values skip the filter entirely
59
                // (the && short-circuits before the filter function is called).
60
                // Empty acceptedTypes means "run for all types" — no guard needed.
61
                'typeCheck' => !empty($acceptedTypes)
135✔
62
                    ? TypeCheck::buildCompound($acceptedTypes)
110✔
63
                    : '',
135✔
64
                'filterClass' => $this->filter->getFilter()[0],
135✔
65
                'filterMethod' => $this->filter->getFilter()[1],
135✔
66
                'filterOptions' => var_export($this->filterOptions, true),
135✔
67
                'filterValueValidator' => new PropertyValidator(
135✔
68
                    $property,
135✔
69
                    '',
135✔
70
                    InvalidFilterValueException::class,
135✔
71
                    [$this->filter->getToken(), '&$filterException'],
135✔
72
                ),
135✔
73
                'viewHelper' => new RenderHelper($generatorConfiguration),
135✔
74
            ],
135✔
75
            IncompatibleFilterException::class,
135✔
76
            [$this->filter->getToken()],
135✔
77
        );
135✔
78
    }
79

80
    /**
81
     * Track if a transformation failed. If a transformation fails don't execute subsequent filter as they'd fail with
82
     * an invalid type
83
     */
84
    public function getValidatorSetUp(): string
127✔
85
    {
86
        return $this->filter instanceof TransformingFilterInterface
127✔
87
            ? '$transformationFailed = false;'
64✔
88
            : '';
127✔
89
    }
90

91
    /**
92
     * Make sure the filter is only executed if a non-transformed value is provided.
93
     * This is required as a setter (eg. for a string property which is modified by the DateTime filter into a DateTime
94
     * object) also accepts a transformed value (in this case a DateTime object).
95
     * If transformed values are provided neither filters defined before the transforming filter in the filter chain nor
96
     * the transforming filter must be executed as they are only compatible with the original value
97
     *
98
     * @throws ReflectionException
99
     * @throws SchemaException
100
     */
101
    public function addTransformedCheck(TransformingFilterInterface $filter, PropertyInterface $property): self
69✔
102
    {
103
        $returnTypeNames = FilterReflection::getReturnTypeNames($filter, $property);
69✔
104

105
        if (empty($returnTypeNames)) {
69✔
NEW
106
            return $this;
×
107
        }
108

109
        $acceptedTypes = FilterReflection::getAcceptedTypes($filter, $property);
69✔
110
        $nonAccepted = array_values(array_diff($returnTypeNames, $acceptedTypes));
69✔
111

112
        if (!empty($nonAccepted)) {
69✔
113
            $this->templateValues['skipTransformedValuesCheck'] = TypeCheck::buildNegatedCompound($nonAccepted);
68✔
114
        }
115

116
        return $this;
69✔
117
    }
118

119
    /**
120
     * Check if the given filter is compatible with the base type of the property defined in the schema.
121
     *
122
     * A filter is compatible when:
123
     * - it accepts all types (empty acceptedTypes derived from callable's first parameter type hint), or
124
     * - the property is untyped (any non-empty acceptedTypes has overlap with the infinite type space), or
125
     * - the property's types have at least one overlap with the filter's acceptedTypes.
126
     *
127
     * Only a complete zero overlap on a typed property is an error, because the filter could never
128
     * execute under any circumstances. Partial overlap is fine: the runtime typeCheck guard in the
129
     * generated code already skips the filter for non-matching value types.
130
     *
131
     * @param string[] $acceptedTypes Pre-computed accepted types of the filter.
132
     *
133
     * @throws SchemaException
134
     */
135
    private function runCompatibilityCheck(array $acceptedTypes, PropertyInterface $property): void
146✔
136
    {
137
        if (empty($acceptedTypes)) {
146✔
138
            return;
25✔
139
        }
140

141
        if ($property->getType() === null && $property->getNestedSchema() === null) {
121✔
142
            return;
3✔
143
        }
144

145
        $typeNames = $property->getNestedSchema() !== null
118✔
146
            ? ['object']
2✔
147
            : $property->getType()->getNames();
116✔
148
        $isNullable = $property->getType()?->isNullable() ?? false;
118✔
149

150
        $hasOverlap = !empty(array_intersect($typeNames, $acceptedTypes))
118✔
151
            || ($isNullable && in_array('null', $acceptedTypes, true));
118✔
152

153
        if (!$hasOverlap) {
118✔
154
            throw new SchemaException(
11✔
155
                sprintf(
11✔
156
                    'Filter %s is not compatible with property type %s for property %s in file %s',
11✔
157
                    $this->filter->getToken(),
11✔
158
                    implode('|', array_merge($typeNames, $isNullable ? ['null'] : [])),
11✔
159
                    $property->getName(),
11✔
160
                    $property->getJsonSchema()->getFile(),
11✔
161
                )
11✔
162
            );
11✔
163
        }
164
    }
165

166
    /**
167
     * Check if the given filter is compatible with the result of the given transformation filter.
168
     *
169
     * All parts of the transformed output (including null when nullable) must be accepted by
170
     * the subsequent filter. Any unhandled return type is an error.
171
     *
172
     * @param string[] $filterAcceptedTypes Pre-computed accepted types of the current filter.
173
     *
174
     * @throws ReflectionException
175
     * @throws SchemaException
176
     */
177
    private function validateFilterCompatibilityWithTransformedType(
9✔
178
        array $filterAcceptedTypes,
179
        TransformingFilterInterface $transformingFilter,
180
        PropertyInterface $property,
181
    ): void {
182
        $returnTypeNames = FilterReflection::getReturnTypeNames($transformingFilter, $property);
9✔
183
        $returnNullable = FilterReflection::isReturnNullable($transformingFilter);
9✔
184

185
        if (empty($returnTypeNames) && !$returnNullable) {
9✔
186
            // Return type is mixed or null-only — subsequent filter must accept all types.
NEW
187
            if (!empty($filterAcceptedTypes)) {
×
NEW
188
                throw new SchemaException(
×
NEW
189
                    sprintf(
×
NEW
190
                        'Filter %s is not compatible with the unconstrained output of'
×
NEW
191
                            . ' transforming filter %s for property %s in file %s'
×
NEW
192
                            . ' (not all types are accepted)',
×
NEW
193
                        $this->filter->getToken(),
×
NEW
194
                        $transformingFilter->getToken(),
×
NEW
195
                        $property->getName(),
×
NEW
196
                        $property->getJsonSchema()->getFile(),
×
NEW
197
                    )
×
NEW
198
                );
×
199
            }
200

NEW
201
            return;
×
202
        }
203

204
        if (empty($filterAcceptedTypes)) {
9✔
205
            // Next filter accepts everything — always compatible.
NEW
206
            return;
×
207
        }
208

209
        // All parts of the return type must be handled by the next filter's accepted types.
210
        $allReturnTypes = $returnNullable
9✔
211
            ? array_merge($returnTypeNames, ['null'])
9✔
NEW
212
            : $returnTypeNames;
×
213
        $unhandled = array_diff($allReturnTypes, $filterAcceptedTypes);
9✔
214

215
        if (!empty($unhandled)) {
9✔
216
            $displayTypes = $returnNullable
2✔
217
                ? array_merge(['null'], $returnTypeNames)
2✔
NEW
218
                : $returnTypeNames;
×
219
            $typeDisplay = count($displayTypes) > 1
2✔
220
                ? '[' . implode(', ', $displayTypes) . ']'
2✔
NEW
221
                : $displayTypes[0];
×
222

223
            throw new SchemaException(
2✔
224
                sprintf(
2✔
225
                    'Filter %s is not compatible with transformed property type %s for property %s in file %s',
2✔
226
                    $this->filter->getToken(),
2✔
227
                    $typeDisplay,
2✔
228
                    $property->getName(),
2✔
229
                    $property->getJsonSchema()->getFile(),
2✔
230
                )
2✔
231
            );
2✔
232
        }
233
    }
234

235
    public function getFilter(): FilterInterface
111✔
236
    {
237
        return $this->filter;
111✔
238
    }
239

240
    public function getFilterOptions(): array
26✔
241
    {
242
        return $this->filterOptions;
26✔
243
    }
244
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc