Skip to content
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

Implemented dynamic exclusion using symfony expression language #673

Merged
merged 38 commits into from
Jan 19, 2017

Conversation

goetas
Copy link
Collaborator

@goetas goetas commented Nov 4, 2016

This PR allows to use dynamic expressions to decide if exclude or not a field during the serialization process.

Under the hood it uses http://symfony.com/doc/current/components/expression_language.html

class Person
{
    /**
     * @Expose(if="service('some.cool.service').isAllowed(object)")
     */
    public $gender;
    /**
     * @Exclude(if="service('some.cool.service').isAllowed(object)")
     */
    public $gender;
}

the "gender" filed will be serialized if the expression service('some.cool.service').isAllowed(object) returns false or true depending if the annotation used is exclude or expose.

Closes #540

@@ -170,6 +182,9 @@ public function fromArray(array $data, $type, DeserializationContext $context =

private function visit(VisitorInterface $visitor, Context $context, $data, $format, array $type = null)
{
if (null !== $this->expressionEvaluator) {
$context->addExclusionStrategy(new DynamicExclusionStrategy($this->expressionEvaluator));
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not really proud on how this is implemented

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@schmittjoh any idea on how to do this in a better way?

@goetas
Copy link
Collaborator Author

goetas commented Nov 4, 2016

@lsmith77 @schmittjoh @Ener-Getick @xabbuh @willdurand @stof

Some of your opensource projects relies on the serializer, I would be very grateful if somebody of you can have a look at it and review it a bit?

@goetas goetas added this to the v1.5 milestone Nov 4, 2016
@mvrhov
Copy link

mvrhov commented Nov 4, 2016

This and more is implemented in https://github.com/willdurand/Hateoas#exclusion

@goetas
Copy link
Collaborator Author

goetas commented Nov 4, 2016

@mvrhov I took inspiration from it :)

@xabbuh
Copy link
Contributor

xabbuh commented Nov 16, 2016

@goetas I am sorry, but I am not really familiar with the JMS Serializer internals. So I am not of much help here.

Copy link
Owner

@schmittjoh schmittjoh left a comment

Choose a reason for hiding this comment

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

I think it would be good to have an exception if someone uses the new capabilities, but does not have the exclusion strategy enabled to enforce it.

Alternatively, we could also think about automatically enabling the exclusion strategy in some way if it encounters an if expression if it's not already active.

In general, I like the changes 👍

@@ -158,8 +159,15 @@ public function loadMetadataForClass(\ReflectionClass $class)
$propertyMetadata->serializedName = $annot->name;
} elseif ($annot instanceof Expose) {
$isExpose = true;
if (null !== $annot->if){
$propertyMetadata->excludeIf = "!" . $annot->if;
Copy link
Owner

Choose a reason for hiding this comment

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

Probably need to wrap this in parentheses to support something like a && b.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

class ExpressionEvaluator implements ExpressionEvaluatorInterface
Copy link
Owner

Choose a reason for hiding this comment

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

Is there a need to support another evaluator? Even if there were one, we already have the strategy interface which could be implemented, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done, removed the classes and interfaces, now the dynamic exclusion strategy (renamed in expression language exlusion strategy) uses directly the symfony expression language

@goetas goetas force-pushed the exclude-if branch 2 times, most recently from 491e752 to 0c30f52 Compare November 21, 2016 11:51
@goetas
Copy link
Collaborator Author

goetas commented Nov 21, 2016

@schmittjoh can't find any good strategy to throw some exception if the expression language is used for expose/exclude annotation, so i have added this documentation part 65311c4

enabling it automatically is also not possible because the expression language requires always some configuratuion

@goetas
Copy link
Collaborator Author

goetas commented Nov 21, 2016

@schmittjoh i consider this as ready for review

@schmittjoh
Copy link
Owner

schmittjoh commented Nov 21, 2016

Regarding the exception, I think you could add a flag to the class metadata (true/false) whether expressions are used, and then also have a similar flag on the Context whether the strategy is enabled. After loading metadata in the GraphNavigator, you can then compare the flags to see if everything is ok.

It's a bit of a code smell to hard code something for a single exclusion strategy in those classes, but I think it's worth it in this case since exposing data that should not be exposed can pose a serious security issue, and it's also better for DX. What do you think?

Another question, how does this behave with deserializing? I think it's currently only designed for serialization?

@goetas
Copy link
Collaborator Author

goetas commented Nov 22, 2016

@schmittjoh regarding the user experience, I definitely agree with you that is way better throwing an exception in the serialization process instead of just ignoring it.

I have implemented it with 264f32a but from the code point of view im really not happy...

What do you think... should we keep it hard-coded as it is now... putting the user experience first, and the the code cleanness ?

Copy link
Owner

@schmittjoh schmittjoh left a comment

Choose a reason for hiding this comment

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

I think hard-coding is ok for this.

What about deserialization, did you think about that?

public function hasExclusionStrategy($class)
{
if (isset($this->hasExclusionStrategy[$class])) {
return $class;
Copy link
Owner

Choose a reason for hiding this comment

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

Should return $this->hasExclusionStrategy[$class] here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

my, bad, fixed

if ($strategy instanceof $class) {
return $this->hasExclusionStrategy[$class] = true;
} elseif ($strategy instanceof DisjunctExclusionStrategy) {
$ref = new \ReflectionProperty($strategy, 'delegates');
Copy link
Owner

Choose a reason for hiding this comment

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

Better add a getter to that class, maybe even a separate interface like DelegatingStrategyInterface which other classes could also implement and which exposes a getDelegates.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i prefer to add just the method... DelegatingStrategyInterface looks an overkill to me

@@ -158,8 +158,17 @@ public function loadMetadataForClass(\ReflectionClass $class)
$propertyMetadata->serializedName = $annot->name;
} elseif ($annot instanceof Expose) {
$isExpose = true;
if (null !== $annot->if) {
$propertyMetadata->excludeIf = "!(" . $annot->if . ")";
$classMetadata->usingExpression = true;
Copy link
Owner

Choose a reason for hiding this comment

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

Better move this to the addPropertyMetadata method, then we do not have to check this in each driver.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good tip, done

@goetas
Copy link
Collaborator Author

goetas commented Nov 22, 2016

I think hard-coding is ok for this.

ok, then lets stay with it

What about deserialization, did you think about that?

It was not my usecase so i have just skipped it.

Probably for consistency should be implemented something. My personal opinion is that rarely in deserialization you need to skip some field, so the possible scenario are really few

@goetas
Copy link
Collaborator Author

goetas commented Nov 23, 2016

@schmittjoh There is one disadvantage in my approach, when using the context factory to add the exclusion strategy... from now on Context::create() become useless and not enough... now starts to be fundamental having access to the context factories to create a new "good" context.

So probably Context::create() should be deprecated.

I have already prepared a PR for JMS Serializer Bundle and FOS Rest and will submit it soon.

@goetas
Copy link
Collaborator Author

goetas commented Nov 27, 2016

@schmittjoh can you give me your feedback on aa4c951 ?

Now the creation if contextes is pretty important, and can't be done with a simple static method anymore... what do you think?

@schmittjoh
Copy link
Owner

I've been thinking whether we can change the factory to something that can take a context and populate it with default values where nothing has been explicitly given. I would like to avoid additional complexity for end users if possible.

@goetas
Copy link
Collaborator Author

goetas commented Dec 5, 2016

Do not have any really good idea on how to do it.
The problem i see is that exclusion strategies are part of the "context", and a dynamic exclusion strategy needs a wide set of dependencies.

One detail that should be considered is the amount of users using jms serializer via FOS rest bundle, and without FOS.

  • If they are using FOS rest, this will be fully transparent, since will be really easy to add the context factory via symfony DI container (fos has its own contextes and context factories).

  • If users are not using FOS, then configuring the expression language is not trivial at all, this means that users will have to figure out how to build contextes anyway.

On the other side, really do not like the static method for creating new contextes. Does not play well with future dependencies (this is more my personal opinion and for the Developer Experience, i'm ready to sacrifice my point of view).

@schmittjoh
Copy link
Owner

We should probably remove exclusion strategies from the context altogether and rather move them to the serializer. The context was intended as a lightweight data object, but with more complex exclusion strategies it is becoming a lot heavier.

With regard to static methods, we can actually deprecate them (they were only needed for older PHP versions). Now, it's just as easy to do (new SerializationContext)->somethingHere().

@goetas
Copy link
Collaborator Author

goetas commented Dec 6, 2016

The only solution i see is adding an extra "if" condition in the GraphNavigator (exactly at here).

The approach should work. In that way, the "expression language" will become part of the graph navigator implementation.

Is this an acceptable solution?

@goetas
Copy link
Collaborator Author

goetas commented Dec 30, 2016

@schmittjoh Implemented as you suggested.
Now:

  • there is an exception if the expression language is used but not configured
  • context creation is lightweight

Implementation:

  • expression language is added to serializer-builder and propagated via constructors to the graph-navigator.
  • check is done explicitly in the property loop

This implementation does not require changes on the fos rest bundle :)

@goetas goetas merged commit 3850ef0 into master Jan 19, 2017
@goetas
Copy link
Collaborator Author

goetas commented Jan 19, 2017

was time to merge it :)

@@ -38,6 +38,7 @@
"symfony/validator": "^2.2",
"symfony/form": "~2.1",
"symfony/filesystem": "^2.1",
"symfony/expression-language": "^2.6|^3.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

You add some dependency to composer.json but not to composer.lock, so I pull the latest master, run composer install & then run ./vendor/bin/phpunit:

Fatal error: Class 'Symfony\Component\ExpressionLanguage\ExpressionFunction' not found in .../tests/JMS/Serializer/Tests/Serializer/BaseSerializationTest.php on line 243

I think this should be fixed.

Copy link
Contributor

Choose a reason for hiding this comment

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

The composer.lock file is not committed to git. So you need to run composer update instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

My bad, I've seen this file in my IDE and think that it's commited to git.

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.

5 participants