Skip to content

[Serializer] Refactor and uniformize the config by introducing a default context #28709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 23, 2018

Conversation

dunglas
Copy link
Member

@dunglas dunglas commented Oct 3, 2018

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? yes
Tests pass? yes
Fixed tickets n/a
License MIT
Doc PR todo

This PR uniformizes how the Serializer's configuration is handled:

  • As currently, configuration options can be set using the context (options that weren't configurable using the context have been refactored to leverage it)
  • Normalizers and encoders' constructors now accept a "default context"
  • All existing global configuration flags (constructor parameters) have been deprecated in favor of this new default context
  • the stateless context is always tried first, then is the default context

Some examples:

// Configuring groups globally
// Before: not possible
// After
$normalizer = new ObjectNormalizer(/* deps */, ['groups' => 'the_default_group']);

// Escaping Excel-like formulas in CSV files
// Before
$encoder = new CsvEncoder(',', '"', '\\', '.', true);
// After
$encoder = new CsvEncoder(['csv_escape_formulas' => true]);
$encoder->normalize($data, 'csv', ['csv_escape_formulas' => false]); // Override for this call only

Benefits:

  • The DX is dramatically improved, configuration is always handled in similar way
  • The serializer can be used in fully stateless way
  • Every options can be configured globally
  • Classes that had constructors with a lot of parameters (like CsvEncoder) are now much easier to use
  • We'll be able to improve the documentation by adding a dictionary of all available context options for the whole component
  • Everything can be configured the same way

TODO in subsequent PRs:

  • Add a new option in framework bundle to configure the context globally
  • Uniformize the constants name (sometimes the name if FOO, sometimes FOO_KEY)
  • Fix the "bug" regarding the format configuration in DateTimeNormalizer::denormalize() (see comments)
  • Maybe: move $defaultContext as the first parameter (before required services?)
  • Make XmlEncoder stateless

@@ -76,7 +94,7 @@ public function supportsNormalization($data, $format = null)
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
$dateTimeFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : null;
$dateTimeFormat = $context[static::FORMAT_KEY] ?? null;
Copy link
Member Author

@dunglas dunglas Oct 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even before this PR, $this->format was ignored when denormalizing (but not when normalizing).
Changing this behavior introduces a BC break, and breaks the tests.

An alternative could be to introduce a new key, like denormalize_datetime_format, and to deprecate using datetime_format when denormalizing.

Anyway, it's out of this PR scope.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes a lot of sense!

$val = ${$arg[0]};
if (${$arg[0]} !== $arg[2]) {
$this->defaultContext[$arg[1]] = $val;
@trigger_error(sprintf('The "%s" parameter is deprecated since Symfony 4.2, use the "%s" key of the context instead.', $arg[0], $arg[1]), E_USER_DEPRECATED);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of this complex logic, I'd suggest to now deal with mixed signatures: either the 1st arg is an array - or not and then we trigger one deprecation and we build the defaultcontext out of the arguments (like done in JsonDecode)

$val = ${$arg[0]};
if (${$arg[0]} !== $arg[2]) {
$this->defaultContext[$arg[1]] = $val;
@trigger_error(sprintf('The "%s" parameter is deprecated since Symfony 4.2, use the "%s" key of the context instead.', $arg[0], $arg[1]), E_USER_DEPRECATED);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same note as for CsvEncoder

);
foreach ($xmlOptions as $xmlOption => $documentProperty) {
if (isset($context[$xmlOption])) {
$document->$documentProperty = $context[$xmlOption];
if ($contextOption = ($context[$xmlOption] ?? $this->defaultContext[$xmlOption] ?? false)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for the brackets


/**
* @deprecated since Symfony 4.2
*
* @var int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's drop the type meanwhile (same below :) )

@@ -388,8 +441,8 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref
// Don't run set for a parameter passed to the constructor
$params[] = $parameterData;
unset($data[$key]);
} elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) {
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif (null !== $param = ($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key] ?? $this->defaultContext[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key] ?? null)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra brackets

@@ -32,16 +32,19 @@
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
{
const ENABLE_MAX_DEPTH = 'enable_max_depth';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets keep the original and safe some merge conflicts

continue;
}

$attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
if ($maxDepthReached) {
$attributeValue = \call_user_func($this->maxDepthHandler, $attributeValue, $object, $attribute, $format, $context);
$attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets keep the original and safe some merge conflicts

@@ -49,17 +61,17 @@ public function normalize($object, $format = null, array $context = array())
}

$result = array(
'type' => $context['type'] ?? 'https://symfony.com/errors/validation',
'title' => $context['title'] ?? 'Validation Failed',
'type' => $context[static::TYPE] ?? $this->defaultContext[static::TYPE] ?? 'https://symfony.com/errors/validation',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self:: instead of static::?
generally we should use self:: when using constants when possible IMHO

*
* @return object|null an object if things check out, null otherwise
*/
protected function extractObjectToPopulate($class, array $context, $key = null)
{
$key = $key ?: 'object_to_populate';

$key = $key ?? 'object_to_populate';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest reverting all changes in this file

@@ -50,7 +50,7 @@ public function decodeProvider()
$assoc = array('foo' => 'bar');

return array(
array('{"foo": "bar"}', $stdClass, array()),
//array('{"foo": "bar"}', $stdClass, array()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be cleaned up I suppose?

@dunglas dunglas force-pushed the new-serializer-config branch 3 times, most recently from f1de005 to 6c2b902 Compare October 3, 2018 20:23
@dunglas
Copy link
Member Author

dunglas commented Oct 3, 2018

@nicolas-grekas thanks for the review! All comments resolved.
The fabbot's failure is a false positive (applying its patch triggers an error in PHPStorm).

@dunglas dunglas force-pushed the new-serializer-config branch 2 times, most recently from e63a99c to 83463d2 Compare October 3, 2018 21:05
@@ -32,20 +32,36 @@
use ObjectToPopulateTrait;
use SerializerAwareTrait;

const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why changing the order? (same above btw)

}

if (null !== $timezone) {
@trigger_error(sprintf('The "timezone" parameter is deprecated since Symfony 4.2, use the "%s" key of the context instead.', self::TIMEZONE_KEY), E_USER_DEPRECATED);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets merge with previous "if" as done in other cases

@dunglas dunglas force-pushed the new-serializer-config branch 3 times, most recently from b6a1369 to 108eb0e Compare October 3, 2018 21:25
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will be much cleaner! Ready on my side, thanks.

@trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', E_USER_DEPRECATED);

$defaultContext = array(
self::DELIMITER_KEY => $defaultContext,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of these for first argument should use casting, in this case (string)

$this->loadOptions = null !== $loadOptions ? $loadOptions : LIBXML_NONET | LIBXML_NOBLANKS;
$this->decoderIgnoredNodeTypes = $decoderIgnoredNodeTypes;
$this->encoderIgnoredNodeTypes = $encoderIgnoredNodeTypes;
$this->defaultContext = array(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why is this whole array not defined as default directly in property definition, like rest

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the default values contain expressions (it's not allowed in properties definitions).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which one? LIBXML_NONET | LIBXML_NOBLANKS works also as it's resolved at compile time if that's what you have in mind

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right it works! I guess it's because I was using static:: in a previous commit.

!isset($attributesMetadata[$attribute]) ||
null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
) {
return false;
}

$key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
$key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unrelated change and might break BC

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was actually not working in some cases. It's safer this way and will prevent people from relying on overriding such constants.

are deprecated, use the default context instead.
* passing configuration options directly to the constructor of `CsvEncoder`, `JsonDecode` and
`XmlEncoder` is deprecated since Symfony 4.2, use the default context instead.
>>>>>>> [Serializer] Refactor and uniformize the config by introducing a default context
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line here

private $associative;
private $recursionDepth;
/**
* True to return the result associative array, false for a nested stdClass hierarchy.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing "as an" before "associative"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the error was already is already present in the original docs but yes


public function __construct(int $bitmask = 0)
private $defaultContext = array(
'json_encode_options' => 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use the constant instead I suppose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant s/'json_encode_options'/self::JSON_ENCODE_OPTIONS/

* @param string $name Root node name
*/
public function setRootNodeName($name)
{
$this->rootNodeName = $name;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use the context instead.', __METHOD__), E_USER_DEPRECATED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4.2

* @return string
*/
public function getRootNodeName()
{
return $this->rootNodeName;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use the context instead.', __METHOD__), E_USER_DEPRECATED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4.2

}

/**
* Returns the root node name.
*
* @deprecated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deprecated since Symfony 4.2

@nicolas-grekas
Copy link
Member

(rebase needed + some comments to fix)

@dunglas dunglas force-pushed the new-serializer-config branch from e61e0f7 to cb48949 Compare October 21, 2018 10:37
@dunglas dunglas force-pushed the new-serializer-config branch from cb48949 to 52b186a Compare October 23, 2018 06:25
@dunglas dunglas merged commit 52b186a into symfony:master Oct 23, 2018
dunglas added a commit that referenced this pull request Oct 23, 2018
…roducing a default context (dunglas)

This PR was squashed before being merged into the 4.2-dev branch (closes #28709).

Discussion
----------

[Serializer] Refactor and uniformize the config by introducing a default context

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes <!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | yes <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | n/a   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | todo <!-- required for new features -->

This PR uniformizes how the Serializer's configuration is handled:

* As currently, configuration options can be set using the context (options that weren't configurable using the context have been refactored to leverage it)
* Normalizers and encoders' constructors now accept a "default context"
* All existing global configuration flags (constructor parameters) have been deprecated in favor of this new default context
* the stateless context is always tried first, then is the default context

Some examples:

```php
// Configuring groups globally
// Before: not possible
// After
$normalizer = new ObjectNormalizer(/* deps */, ['groups' => 'the_default_group']);

// Escaping Excel-like formulas in CSV files
// Before
$encoder = new CsvEncoder(',', '"', '\\', '.', true);
// After
$encoder = new CsvEncoder(['csv_escape_formulas' => true]);
$encoder->normalize($data, 'csv', ['csv_escape_formulas' => false]); // Override for this call only
```

Benefits:

* The DX is dramatically improved, configuration is always handled in similar way
* The serializer can be used in fully stateless way
* Every options can be configured globally
* Classes that had constructors with a lot of parameters (like `CsvEncoder`) are now much easier to use
* We'll be able to improve the documentation by adding a dictionary of all available context options for the whole component
* Everything can be configured the same way

TODO in subsequent PRs:

* Add a new option in framework bundle to configure the context globally
* Uniformize the constants name (sometimes the name if `FOO`, sometimes `FOO_KEY`)
* Fix the "bug" regarding the format configuration in `DateTimeNormalizer::denormalize()`  (see comments)
* Maybe: move `$defaultContext` as the first parameter (before required services?)
* Make `XmlEncoder` stateless

Commits
-------

52b186a [Serializer] Refactor and uniformize the config by introducing a default context
@dunglas dunglas deleted the new-serializer-config branch October 23, 2018 06:27
This was referenced Nov 3, 2018
mxr576 added a commit to mxr576/apigee-client-php that referenced this pull request Jan 21, 2019
symfony/symfony#28709 PR had changed the
parameter of some constructor in the serializer component therefore
PSALM started to report InvalidArgument errors.
mxr576 added a commit to mxr576/apigee-client-php that referenced this pull request Jan 21, 2019
symfony/symfony#28709 PR had changed the
parameter of some constructor in the serializer component therefore
PSALM started to report InvalidArgument errors.
mxr576 added a commit to mxr576/apigee-client-php that referenced this pull request Feb 6, 2019
symfony/symfony#28709 PR had changed the
parameter of some constructor in the serializer component therefore
PSALM started to report InvalidArgument errors.
nicolas-grekas added a commit that referenced this pull request May 30, 2019
… default context solely (ogizanagi)

This PR was squashed before being merged into the 5.0-dev branch (closes #31699).

Discussion
----------

[Serializer] Unified normalizers/encoders config through default context solely

| Q             | A
| ------------- | ---
| Branch?       | master <!-- see below -->
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | #28709   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | N/A

As planned in #28709.
Split into two commits to ease review.

Commits
-------

914577e [Serializer] Unified normalizers/encoders config through default context solely
nicolas-grekas added a commit that referenced this pull request Jun 8, 2019
…anagi)

This PR was merged into the 5.0-dev branch.

Discussion
----------

[Serializer] Remove last deprecated/obsolete paths

| Q             | A
| ------------- | ---
| Branch?       | master <!-- see below -->
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | #28316, #28709, #31030, #27020, #29896, 16f8a13#r201060750   <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | N/A <!-- required for new features -->

This should fix the last deprecations & obsolete code paths for the Serializer component.

Commits
-------

c703b35 [Serializer] Remove last deprecated/obsolete paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants