Skip to content

Commit dc41c00

Browse files
committed
Add custom serializer functionality for transforming filters
Move serialization methods into trait loaded from the production repository
1 parent 2213173 commit dc41c00

File tree

10 files changed

+205
-84
lines changed

10 files changed

+205
-84
lines changed

docs/source/nonStandardExtensions/filter.rst

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ As the required check is executed before the filter a filter may transform a req
5555

5656
The return type of the last applied filter will be used to define the type of the property inside the generated model (in the example one section above given above the method **getCreated** will return a DateTime object). Additionally the generated model also accepts the transformed type as input type. So **setCreated** will accept a string and a DateTime object. If an already transformed value is provided the filter which transforms the value will **not** be executed.
5757

58-
For working models you must define the return type of your filter function as the implementation uses Reflection methods to determine whether a filter transforms a value or keeps the type (like the **trim** filter).
58+
If you write a custom transforming filter you must define the return type of your filter function as the implementation uses Reflection methods to determine to which type a value is transformed by a filter.
5959

6060
Builtin filter
6161
--------------
@@ -163,6 +163,7 @@ Option Default value Description
163163
convertNullToNow false If null is provided a DateTime object with the current time will be created (works only if the property isn't required as null would be denied otherwise before the filter is executed)
164164
denyEmptyValue false An empty string value will be denied (by default an empty string value will result in a DateTime object with the current time)
165165
createFromFormat null Provide a pattern which is used to parse the provided value (DateTime object will be created via DateTime::createFromFormat if a format is provided)
166+
outputFormat DATE_ISO8601 The output format if serialization is enabled and toArray or toJSON is called on a transformed property. If a createFromFormat is defined but no outputFormat the createFromFormat value will override the default value
166167
================ ============= ===========
167168

168169
Custom filter
@@ -271,3 +272,53 @@ The option will be available if your JSON-Schema uses the object-notation for th
271272
}
272273
}
273274
}
275+
276+
Custom transforming filter
277+
^^^^^^^^^^^^^^^^^^^^^^^^^^
278+
279+
If you want to provide a custom filter which transforms a value (eg. redirect data into a manually written model) you must implement the **PHPModelGenerator\\PropertyProcessor\\Filter\\TransformingFilterInterface**. This interface adds the **getSerializer** method to your filter. The method is similar to the **getFilter** method. It must return a callable which is available during the render process as well as during code execution. The returned callable must return null or a string and undo a transformation (eg. the serializer method of the builtin **dateTime** filter transforms a DateTime object back into a formatted string). The serializer method will be called with the current value of the property as the first argument and with the (optionally provided) additional options of the filter as the second argument. Your custom transforming filter might look like:
280+
281+
282+
.. code-block:: php
283+
284+
namespace MyApp\Model\Generator\Filter;
285+
286+
use MyApp\Model\ManuallyWrittenModels\Customer;
287+
use PHPModelGenerator\PropertyProcessor\Filter\TransformingFilterInterface;
288+
289+
class CustomerFilter implements TransformingFilterInterface
290+
{
291+
// Let's assume you have written a Customer model manually eg. due to advanced validations
292+
// and you want to use the Customer model as a part of your generated model
293+
public static function instantiateCustomer(?array $data, array $additionalOptions): ?Customer
294+
{
295+
return $data !== null ? new Customer($data, $additionalOptions) : null;
296+
}
297+
298+
// $customer will contain the current value of the property the filter is applied to
299+
// $additionalOptions will contain all additional options from the JSON Schema
300+
public static function instantiateCustomer(?Customer $customer, array $additionalOptions): ?string
301+
{
302+
return $data !== null ? $customer->serialize($additionalOptions) : null;
303+
}
304+
305+
public function getAcceptedTypes(): array
306+
{
307+
return ['object'];
308+
}
309+
310+
public function getToken(): string
311+
{
312+
return 'uppercase';
313+
}
314+
315+
public function getFilter(): array
316+
{
317+
return [self::class, 'instantiateCustomer'];
318+
}
319+
320+
public function getSerializer(): array
321+
{
322+
return [self::class, 'serializeCustomer'];
323+
}
324+
}

src/Model/GeneratorConfiguration.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPModelGenerator\Exception\InvalidFilterException;
88
use PHPModelGenerator\PropertyProcessor\Filter\DateTimeFilter;
99
use PHPModelGenerator\PropertyProcessor\Filter\FilterInterface;
10+
use PHPModelGenerator\PropertyProcessor\Filter\TransformingFilterInterface;
1011
use PHPModelGenerator\PropertyProcessor\Filter\TrimFilter;
1112
use PHPModelGenerator\Utils\ClassNameGenerator;
1213
use PHPModelGenerator\Utils\ClassNameGeneratorInterface;
@@ -73,6 +74,16 @@ public function addFilter(FilterInterface $filter): self
7374
throw new InvalidFilterException("Invalid filter callback for filter {$filter->getToken()}");
7475
}
7576

77+
if ($filter instanceof TransformingFilterInterface) {
78+
if (!(count($filter->getSerializer()) === 2) ||
79+
!is_string($filter->getSerializer()[0]) ||
80+
!is_string($filter->getSerializer()[1]) ||
81+
!is_callable($filter->getSerializer())
82+
) {
83+
throw new InvalidFilterException("Invalid serializer callback for filter {$filter->getToken()}");
84+
}
85+
}
86+
7687
$this->filter[$filter->getToken()] = $filter;
7788

7889
return $this;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\Model\Property\Serializer;
6+
7+
use PHPMicroTemplate\Exception\FileSystemException;
8+
use PHPMicroTemplate\Exception\SyntaxErrorException;
9+
use PHPMicroTemplate\Exception\UndefinedSymbolException;
10+
use PHPMicroTemplate\Render;
11+
use PHPModelGenerator\Model\GeneratorConfiguration;
12+
use PHPModelGenerator\PropertyProcessor\Filter\TransformingFilterInterface;
13+
use PHPModelGenerator\Utils\RenderHelper;
14+
15+
/**
16+
* Class TransformingFilterSerializer
17+
*
18+
* @package PHPModelGenerator\Model\Property\Serializer
19+
*/
20+
class TransformingFilterSerializer
21+
{
22+
/** @var string */
23+
protected $propertyName;
24+
/** @var TransformingFilterInterface */
25+
protected $filter;
26+
/** @var array */
27+
private $filterOptions;
28+
29+
/**
30+
* TransformingFilterSerializer constructor.
31+
*
32+
* @param string $propertyName
33+
* @param TransformingFilterInterface $filter
34+
* @param array $filterOptions
35+
*/
36+
public function __construct(
37+
string $propertyName,
38+
TransformingFilterInterface $filter,
39+
array $filterOptions
40+
) {
41+
$this->propertyName = $propertyName;
42+
$this->filter = $filter;
43+
$this->filterOptions = $filterOptions;
44+
}
45+
46+
/**
47+
* @param GeneratorConfiguration $generatorConfiguration
48+
*
49+
* @return string
50+
*
51+
* @throws FileSystemException
52+
* @throws SyntaxErrorException
53+
* @throws UndefinedSymbolException
54+
*/
55+
public function getSerializer(GeneratorConfiguration $generatorConfiguration): string
56+
{
57+
return (new Render(join(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', '..', 'Templates']) . DIRECTORY_SEPARATOR))
58+
->renderTemplate(
59+
DIRECTORY_SEPARATOR . 'Serializer' . DIRECTORY_SEPARATOR . 'TransformingFilterSerializer.phptpl',
60+
[
61+
'viewHelper' => new RenderHelper($generatorConfiguration),
62+
'property' => $this->propertyName,
63+
'serializerClass' => $this->filter->getSerializer()[0],
64+
'serializerMethod' => $this->filter->getSerializer()[1],
65+
'serializerOptions' => var_export($this->filterOptions, true),
66+
]
67+
);
68+
}
69+
}

src/Model/RenderJob.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ protected function renderClass(GeneratorConfiguration $generatorConfiguration):
132132
'class' => $this->className,
133133
'baseValidators' => $this->schema->getBaseValidators(),
134134
'properties' => $this->schema->getProperties(),
135+
'customSerializer' => $this->schema->getCustomSerializer(),
135136
'generatorConfiguration' => $generatorConfiguration,
136137
'viewHelper' => new RenderHelper($generatorConfiguration),
137138
'initialClass' => $this->initialClass,

src/Model/Schema.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PHPModelGenerator\Model;
66

77
use PHPModelGenerator\Model\Property\PropertyInterface;
8+
use PHPModelGenerator\Model\Property\Serializer\TransformingFilterSerializer;
89
use PHPModelGenerator\Model\SchemaDefinition\SchemaDefinitionDictionary;
910
use PHPModelGenerator\Model\Validator\PropertyValidatorInterface;
1011
use PHPModelGenerator\Model\Validator\SchemaDependencyValidator;
@@ -31,6 +32,8 @@ class Schema
3132
protected $usedClasses = [];
3233
/** @var SchemaNamespaceTransferDecorator[] */
3334
protected $namespaceTransferDecorators = [];
35+
/** @var TransformingFilterSerializer[] */
36+
protected $customSerializer = [];
3437

3538
/** @var SchemaDefinitionDictionary */
3639
protected $schemaDefinitionDictionary;
@@ -191,4 +194,24 @@ public function getUsedClasses(array $visitedSchema = []): array
191194

192195
return $usedClasses;
193196
}
197+
198+
/**
199+
* @param TransformingFilterSerializer $serializer
200+
*
201+
* @return $this
202+
*/
203+
public function addCustomSerializer(TransformingFilterSerializer $serializer): self
204+
{
205+
$this->customSerializer[] = $serializer;
206+
207+
return $this;
208+
}
209+
210+
/**
211+
* @return TransformingFilterSerializer[]
212+
*/
213+
public function getCustomSerializer(): array
214+
{
215+
return $this->customSerializer;
216+
}
194217
}

src/PropertyProcessor/Filter/DateTimeFilter.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,37 @@
1313
*
1414
* @package PHPModelGenerator\PropertyProcessor\Filter
1515
*/
16-
class DateTimeFilter implements FilterInterface
16+
class DateTimeFilter implements TransformingFilterInterface
1717
{
1818
/**
19-
* Return a list of accepted data types for the filter (eg. ['string', 'int']). If the filter is applied to a
20-
* value which doesn't match an accepted type an exception will be thrown
21-
*
22-
* @return array
19+
* @inheritDoc
2320
*/
2421
public function getAcceptedTypes(): array
2522
{
2623
return ['string'];
2724
}
2825

2926
/**
30-
* Return the token for the filter
31-
*
32-
* @return string
27+
* @inheritDoc
3328
*/
3429
public function getToken(): string
3530
{
3631
return 'dateTime';
3732
}
3833

3934
/**
40-
* Return the filter to apply. Make sure the returned array is a callable which is also callable after the
41-
* render process
42-
*
43-
* @return array
35+
* @inheritDoc
4436
*/
4537
public function getFilter(): array
4638
{
4739
return [DateTime::class, 'filter'];
4840
}
41+
42+
/**
43+
* @inheritDoc
44+
*/
45+
public function getSerializer(): array
46+
{
47+
return [DateTime::class, 'serialize'];
48+
}
4949
}

src/PropertyProcessor/Filter/FilterProcessor.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPModelGenerator\Exception\SchemaException;
88
use PHPModelGenerator\Model\GeneratorConfiguration;
99
use PHPModelGenerator\Model\Property\PropertyInterface;
10+
use PHPModelGenerator\Model\Property\Serializer\TransformingFilterSerializer;
1011
use PHPModelGenerator\Model\Schema;
1112
use PHPModelGenerator\Model\Validator;
1213
use PHPModelGenerator\Model\Validator\FilterValidator;
@@ -63,7 +64,7 @@ public function process(
6364
}
6465

6566
// check if the last applied filter has changed the type of the property
66-
if ($filter) {
67+
if ($filter instanceof TransformingFilterInterface) {
6768
$typeAfterFilter = (new ReflectionMethod($filter->getFilter()[0], $filter->getFilter()[1]))
6869
->getReturnType();
6970

@@ -74,6 +75,10 @@ public function process(
7475
$this->extendTypeCheckValidatorToAllowTransformedValue($property, $schema, $typeAfterFilter);
7576

7677
$property->setType($property->getType(), $typeAfterFilter->getName());
78+
79+
$schema->addCustomSerializer(
80+
new TransformingFilterSerializer($property->getAttribute(), $filter, $filterOptions)
81+
);
7782
}
7883
}
7984
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace PHPModelGenerator\PropertyProcessor\Filter;
6+
7+
interface TransformingFilterInterface extends FilterInterface
8+
{
9+
/**
10+
* Return the serializer to apply to transformed values.
11+
* Make sure the returned array is a callable which is also callable after the render process
12+
*
13+
* @return array
14+
*/
15+
public function getSerializer(): array;
16+
}

src/Templates/Model.phptpl

Lines changed: 5 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ declare(strict_types = 1);
1919
class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface
2020
{% if generatorConfiguration.hasSerializationEnabled() %}, \PHPModelGenerator\Interfaces\SerializationInterface{% endif %}
2121
{
22+
{% if generatorConfiguration.hasSerializationEnabled() %}use \PHPModelGenerator\Traits\SerializableTrait;{% endif %}
23+
2224
{% foreach properties as property %}
2325
/** @var {{ property.getTypeHint(true) }}{% if not property.isRequired() %}|null{% endif %}{% if property.getDescription() %} {{ property.getDescription() }}{% endif %} */
2426
protected ${{ property.getAttribute() }}{% if not viewHelper.isNull(property.getDefaultValue()) %} = {{ property.getDefaultValue() }}{% endif %};
@@ -171,75 +173,8 @@ class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface
171173
{% endforeach %}
172174

173175
{% if generatorConfiguration.hasSerializationEnabled() %}
174-
/**
175-
* Get an array representation of the current state
176-
*
177-
* @param int $depth the maximum level of object nesting. Must be greater than 0
178-
*
179-
* @return array|false
180-
*/
181-
public function toArray(int $depth = 512)
182-
{
183-
if ($depth < 1) {
184-
return false;
185-
}
186-
187-
$depth--;
188-
$modelData = [];
189-
190-
foreach (get_object_vars($this) as $key => $value) {
191-
if (in_array($key, ['rawModelDataInput', 'errorRegistry'])) {
192-
continue;
193-
}
194-
195-
if (is_array($value)) {
196-
$subData = [];
197-
foreach ($value as $subKey => $element) {
198-
$subData[$subKey] = $this->evaluateAttribute($element, $depth);
199-
}
200-
$modelData[$key] = $subData;
201-
} else {
202-
$modelData[$key] = $this->evaluateAttribute($value, $depth);
203-
}
204-
}
205-
206-
return $modelData;
207-
}
208-
209-
private function evaluateAttribute($attribute, int $depth)
210-
{
211-
if (!is_object($attribute)) {
212-
return $attribute;
213-
}
214-
215-
if ($depth === 0 && method_exists($attribute, '__toString')) {
216-
return (string) $attribute;
217-
}
218-
219-
return (0 >= $depth)
220-
? null
221-
: (
222-
method_exists($attribute, 'toArray')
223-
? $attribute->toArray($depth - 1)
224-
: get_object_vars($attribute)
225-
);
226-
}
227-
228-
/**
229-
* Get a JSON representation of the current state
230-
*
231-
* @param int $options Bitmask for json_encode
232-
* @param int $depth the maximum level of object nesting. Must be greater than 0
233-
*
234-
* @return string|false
235-
*/
236-
public function toJSON(int $options = 0, int $depth = 512)
237-
{
238-
if ($depth < 1) {
239-
return false;
240-
}
241-
242-
return json_encode($this->toArray($depth), $options, $depth);
243-
}
176+
{% foreach customSerializer as serializer %}
177+
{{ serializer.getSerializer(generatorConfiguration) }}
178+
{% endforeach %}
244179
{% endif %}
245180
}

0 commit comments

Comments
 (0)