-
Notifications
You must be signed in to change notification settings - Fork 358
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
Add new config option, refactor coerce feature to use this, and simplify the public API #351
Add new config option, refactor coerce feature to use this, and simplify the public API #351
Conversation
I'm aware that I need to write some tests that cover the new bits - I'm working on that now! |
Quick tip: tests and new functionality come out better when the tests come first. |
You make an excellent point! Although it's worth noting that this PR isn't new functionality so much as it's just rearranging a lot of stuff. Almost all of it is already covered by existing tests. |
src/JsonSchema/Validator.php
Outdated
*/ | ||
public function validateImmutable($value, $schema = null, $checkMode = self::CHECK_MODE_NORMAL) | ||
{ | ||
$checkMode &= $this->getCheckModeImmutableMask(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this throw an exception if $checkMode
contains something that wants to modify $value
, or just silently switch off the offending options?
Currently, it just switches them off, as this behavior is the closest to how the old check()
method works in assoc mode. But I must admit, I would really rather have an exception here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this necessary? Isn't it enough that the default $checkmode is CHECK_MODE_NORMAL? I would think that as long as all of your helpers put things back the way they found them that this kind of thing is not needed...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's there because there needs to be some kind of entry point for pass-by-value, and not enforcing immutability results in inconsistent behavior between object mode and assoc mode, because objects are never passed by value. Having the mask means that no matter what the user decides to feed it for $checkMode, the result will be the same regardless of whether $value is an object or an array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not following. What exactly is the use case? How is calling this different from calling validate with no flags?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use-case is being able to pass $value as not-a-reference. If we don't care about that, then validateImmutable
can go; there's no other reason to having it. Thoughts?
The immutable mask thing is only to enforce consistency between assoc and object modes, and if this method goes then it's a non-issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, you already know my opinion. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK then - I'll get rid of validateImmutable
:-).
@@ -59,5 +59,10 @@ public function isValid(); | |||
* @param mixed $i | |||
* @throws \JsonSchema\Exception\ExceptionInterface | |||
*/ | |||
public function _check(&$value, $schema = null, JsonPointer $path = null, $i = null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it a little weird for a public method to have the _ prefix?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not part of the public API (hence why it's prefixed with _, as is convention for internal stuff). It needs to be designated a public method because it gets called from all over the place within the library, but not just from class descendants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, right, but this isn't a situation where you have to rely on the underscore convention to suggest visibility (like we often do in javascript), because we have actual visibility keywords. I think most people would agree it's sort of poor form to make a method public, but suggest that client code shouldn't call it with the underscore convention. Either it's private and you can't call it, or it's public and you can. Take a stand!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method cannot be made private (or protected), because the entire library will break as a result. The various internal callers need a public method to talk to; that's just the way the library is architected.
I agree that having it actually be a public method is undesirable, but unless you have a suggestion for how to mitigate that requirement, then I think it's just something we need to live with, and all we can do is make it look as undesirable as possible for an end-user to rely on. Re-architecting the entire library to change a single visibility keyword seems like a big waste of time - it seems reasonable in this case to just be pragmatic and leave it alone, albeit well-documented as an internal method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess another option would be moving the contents of Validator::check()
into validate()
, and leaving _check()
as an empty method (it can't be removed without also removing it from the interface, which is an important sanity check for those writing constraints). I don't much mind either way - would having Validator::_check()
be a noop be preferable for you, as opposed to the status quo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah, I'm happy to do it, seeing as my wanting to add the defaults PR is the source of this whole headache - it seems reasonable that if I want that feature, it should be my time that gets used making it happen. Code review is always appreciated though :-).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥇 By the way, just so there's no misunderstanding, I am not a project maintainer--just the most vocal contributor these days.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. Do you know whether the maintainer is likely to agree with you on this? Is there any chance of us getting their opinion on the situation?
Vocal means you care, and if you're the only contributor with a strong opinion, and the maintainer is OK with that, then I'm happy to defer to your preference somewhat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bighappyface Is the only active contributor with write privileges. He's usually pretty mellow, but no harm in getting his opinion now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have not heard anything from @bighappyface, so I'm just going to go ahead and implement this as discussed above (one top-level method Validator::check(), all others to go, refactor so that _check is not public).
src/JsonSchema/Validator.php
Outdated
|
||
/** | ||
* Validates the given data against the schema and returns an object containing the results | ||
* Both the php object and the schema are supposed to be a result of a json_decode call. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you make a point of mentioning json_decode? There are other ways to come up with an object...
$schema = (object)['foo'=>'bar'];
$schema = new stdClass();
$schema->foo = 'bar';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't - this is verbatim from master (courtesy of @digitalkaoz 5 years ago). I saw no reason to change it, as it explains the intended use-case quite well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it misleading. Not everybody is decoding json (I'm not--all of my schemas come from php files).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can certainly change it... what would you like to see instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't know. Might be enough to just remove the comment about json_decode, and document the mixed type for $value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works - I'll do that.
src/JsonSchema/Validator.php
Outdated
* | ||
* {@inheritDoc} | ||
*/ | ||
public function validate(&$value, $schema = null, $checkMode = self::CHECK_MODE_NORMAL) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea that you have sort of an advanced usage method where you can explicitly state the kinds of bells and whistles you want. I would argue that this should be the main entry point, and that everything else (coerce
, check
, forceDefaults
, etc) could be considered helpers, shortcuts, or possibly even eventually deprecated and removed entirely.
One suggestion you might consider is to convert all instances of _check
(which is confusingly public and mirroring check
) with validate
, and just pass $checkmode along everywhere. The next step after that would be to think about removing the internal $checkmode and make this whole thing completely stateless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validate()
method is supposed to be the main entry point for everything except pass-by-value calls, which use validateImmutable()
- so not sure what you're disagreeing with here? check()
and coerce()
are intended to be deprecated; the only reason they're still around is to avoid breaking things for people who are currently using them.
I would prefer to avoid conflating validate()
with _check()
- they're intended to be different things. Having the public API be the same thing as all the internal stuff seems rather messy IMO - that's what happened with coerce()
, and half the point of this PR is to remove any need for the 'pass-everything-down-the-stack' approach. Unless you're suggesting we make every part of this library thread-safe, then going back to passing stuff around seems like a backwards step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not disagreeing, really, just trying to help massage this into something a little more structurally solid. Well, I guess I do disagree with validateImmutable
--adding a method whose sole purpose is to ignore certain flags you pass in seems a little bizarro-world to me--but otherwise I like the general tack you're taking.
Really, I was trying to give you an elegant way to avoid the corner you've painted yourself into with _check
. Before you started all this we had two single public methods at the top level. I suggested reducing them to one (yes, that would conceivably break backwards compatibility for what is probably a fairly small set of people, but we're not shy about doing that in this project if the rationale is strong, and I think this qualifies). But now, instead of just check
, we now have check
. validateImmutable
, coerce
, _check
, and validate
. What happened!?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm.... maybe we see this a little differently? I don't feel like this is a corner.
We went from two public methods (check
and coerce
) to two public methods (validate
and validateImmutable
), and in doing so significantly improved the interface from an end-user perspective, plus allowing for easy addition of new functionality in the future without needing to change the public API again.
If you don't care about pass-by-value at all, then validateImmutable
can be removed; that's the entire reason it's there.
If you don't care about breaking backwards-compatibility, then check
and coerce
can certainly be dropped... but IMO keeping them is safer - they're just aliases now anyway. Perhaps having them trigger a deprecation notice when called would be a sensible middle-ground, and then remove them in a future release?
_check
is not a public method, whatever its visibility keyword says. The only reason it exists at all is because ConstraintInterface
requires it. Would making _check
be a noop be an improvement? Because the content of that method can easily be moved into validate
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I count two public methods on ConstraintInterface, check
and _check
, and three more on Validator (which is the public face of the app, and formerly was a vanilla override of Constraint), coerce
, validate
and validateImmutable
. You're arguing that you are making things simpler, but we've gone from 2 publics on the main app instance to 5, and I barely understand it and I'm trying my honest best to keep up.
You have my endorsement to just change check
. If you do it right most of the consumers of this app will never know the difference, and that'll take us down to one public method that can take all comers. We can gather some opinions from other people, but I feel like that will be much, much easier to sell than all this other stuff.
Gonna take a dinner break. Overall I'm liking the energy. Hang in there.
8ba2f94
to
0491e13
Compare
@shmax - have redone this PR more along the lines we discussed - there's now just one public method defined in I've opted to use a config array rather than just $checkMode, in order to future-proof things in case somebody wants to add a feature that can't be configured via a partial bitmask. That way the risk of needing to change the public API again in order to support new features is minimised. |
I think it's a huge improvement in a lot of ways; I like that all those
What do you think? |
Can you clarify what your issue is with having that I do feel fairly strongly that a bitmask alone is insufficient - but a bitmask is definitely still useful, which is why I turned Re backwards compatibility - yes, this definitely breaks compatibility. I thought that you decided that did not matter - but I'm quite happy to keep some backwards compatibility if you let me know which bits of the API you feel need to be not-broken so that I can ensure they maintain compatibility. |
@shmax Further to my comment above, one of the reasons I made coerce & apply-defaults as independent booleans rather than being part of the |
Sure, I'll take another stab at explaining my position: I liked it better before when $checkMode was just an int, and turning coercion on was a matter of applying CHECK_MODE_COERCE (that might have been before your time--I eventually removed it when I added the I didn't say that breaking compatibility doesn't matter--I said we're not shy about doing it when the situation warrants it. The config bag thing is not a must-have--it's more like a personal coding style that you favor, and it doesn't buy us anything in the short term (and we're a little hazy on the long term). To be fair, changing Really, I think everything you did is great, but if it were me I would get rid of the property bag, put $checkMode back the way it was (as a top-level argument to |
@shmax Also further to your point re future-proofing - it makes sense to me that if we are breaking the API anyway (which we are, because [ed: posted this before I saw your most recent comment - it's probably redundant now :-)] |
@shmax Give me a moment, and I'll see if I can make a few changes that will bring things more in line with what I think you're after. |
Righto, sounds good. |
Just a quick response to your comment about |
Hmm, I just realized something. Now that you've divorced Validator::check from Constraint::check (which, again, is awesome), we can pass $checkMode along as a third argument. That wouldn't break compatibility because existing code will only be using two arguments (with the third and fourth currently being $path and $i, which don't really have meaning from the top entry point), AND you then have true "fire and forget" functionality, with more of a one-liner feel (instead of having to keep a Factory instance around so that you can twiddle flags on it)... |
Oh, damn, I see that you already are passing ~~~$checkMode~~~ |
Heading to bed, but I think it's looking great--we're almost there. More soon. |
@shmax OK, so I've done the following - thoughts?
These changes preserve much more backwards-compatibility, still keep a simple API, allow fire-and-forget via I see your heading-to-bed comment - sleep well :-). |
Do you want an alias for It doesn't need to be more than a one-liner: public function coerce(&$value, $schema)
{
return $this->validate($value, $schema, null, array('coerce-types' => true));
} |
5fa3c19
to
e0c601a
Compare
Had a busy morning and now I need to get to work, but I'll get back to you tonight... |
So it's looking good, but I don't quite understand why If you ever do come up with a complex config option that can't be handled with $checkMode, then you can re-introduce $config at that time as the 4th argument without breaking compatibility.
|
I mainly put $checkMode as the third argument because it seemed like something that was important to you - I wouldn't go so far as to say I agree with it, but it seems to me that you care about it a lot, so I'm trying to come up with a compromise. It sounds like you're not entirely happy with the latest suggestion, so I'll tweak it again - stand by... |
@shmax How about now? The biggest point in favour of having a config array now, IMO, is the ability to change options individually without requiring the user to do a bunch of bitwise operations just to avoid clobbering the settings they're not changing. I guess I'm not really understanding why you have such a strong resistance to it - does the latest lot of changes implement it in a way you find acceptable, or am I missing something fundamental about your objection to it?
|
Hmm, interesting. I'll have to take a closer look at this tomorrow; but here's a few reasons why I object to your property bags idea in this specific case, and a few why I don't like them in general in PHP:
But anyway, let me take another look at it tomorrow with fresh eyes. G'nite. |
One last comment; I thought we had dodged this problem by passing checkMode in as an argument on every call to |
Thanks for explaining :-). Your point about typehinting is a really good one, and not something I'd considered. So is the thing with typos - at least with constants, you get an error if you typo it. Noting those two points, I think you've convinced me to ditch the config array. I still care about being able to set individual options, but that's doable with checkmode alone if I tweak it a bit. Stand by for more changes...
It's still stored internally on |
OK, what about this?
|
Still with you... had a busy day, but I'll come back to this tomorrow. Thanks for hanging in there... |
$this->errors = array(); | ||
} | ||
const CHECK_MODE_COERCE_TYPES = 0x00000004; | ||
const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you line the numeric literals up so that they're in a neat column?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorted - looks like there were tabs in there that shouldn't have been, and I have vim configured to display tabs using a 4-char tabstop, so I didn't see them. Well spotted!
* @param int $checkMode Set checkMode options | ||
* @param bool $enable Whether to enable or disable the provided options. If null, overwrites checkMode. | ||
*/ | ||
public function setConfig($checkMode = Constraint::CHECK_MODE_NORMAL, $enable = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this a little confusing. If I pass in $checkMode but $not enable, then I overwrite the whole thing. Otherwise, you either add or remove the flag, dependent on $enable. Do you really think people are going to be busily setting and unsetting specific flags over the course of a program? Wouldn't it be simpler to just drop the $enable arg and just make this a simple set like you have on line 92? If someone want to unset a flag, they would do so by just setting the exact config they want and blowing away the old config...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And if you really want the ability to add and disable specific flags, maybe you could add addConfig
and removeConfig
helpers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$enable
is now gone, and setConfig()
will always overwrite. I do really want to keep the ability to enable / disable specific flags, so have added addConfig()
and removeConfig()
per your suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you really think people are going to be busily setting and unsetting specific flags over the course of a program?
Not so much that - I had two specific use-cases in mind:
- If a flag is default-enabled internally, and the user doesn't know that, but wants to set a flag of their own -
addConfig
() is clearly safer in that situation. - If a user wishes to disable a single default-enabled flag without messing up whatever else might be in
$checkMode
.
At the moment the default is just CHECK_MODE_NORMAL
, but that may not always be the case.
@@ -23,7 +23,8 @@ public function testNullThing() | |||
$validator = new FormatConstraint(); | |||
$schema = new \stdClass; | |||
|
|||
$validator->check('10', $schema); | |||
$checkValue = 10; | |||
$validator->check($checkValue, $schema); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious; is it still necessary to pass in a variable now that top-level check no longer takes a reference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because the individual validators do require a reference. The references are needed for anything that modifies the input value, including type coercion and setting default values.
The line you've highlighted here tests FormatConstraint
directly, not the top-level Validator
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok. Sounds good.
{ | ||
$initialCheckMode = $this->factory->getConfig(); | ||
if ($checkMode !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so if I understand correctly, validate
bulldozes whatever is already set in the internal checkMode, and, respects the incoming $checkMode, then restores it afterward. If that's true, then shouldn't $checkMode default to CHECK_MODE_NORMAL
, and always call $this->factory->setConfig
whether $checkMode (the arg) is set or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. validate()
only clobbers checkMode
if the user actually passes it. If $checkMode
is left as null
, it uses whatever was already set on the factory. That is why the actual default value isn't set by this method.
The actual default is CHECK_MODE_NORMAL
- it's set as the third argument to Factory::__construct()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. I think I can live with that.
Are you ready for it..? 🥁 LGTM! Beautiful work. Sorry for being such a pain. |
Hooray! Thanks for all the time you've spent reviewing this :-). You weren't a pain - you were thinking through the ramifications of the changes from a different perspective to mine, and it was useful - I think the end result is much better because of all your input. Does anyone else need to review before merge / do we need to flag anyone to get this merged? |
This commit makes the following changes: * Split the Constraint class to make Validator independent from it * Add Validator::validate() as the main entry point * Turn Validator::coerce() and Validator::check() into aliases * Add Factory::setConfig(), getConfig(), addConfig() & removeConfig() * Make type-coercion a checkMode option, don't pass $coerce everywhere * Add some extra tests
75a109e
to
2159893
Compare
Squashed into 2159893, because the commit history for this PR was full of in-progress junk that's no longer applicable. |
Well, at this point @bighappyface needs to look it over. He might want to get a few more eyes on it... @digitalkaoz please review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
🎉 Thanks @bighappyface and congrats @erayd |
Woohoo! Thanks guys :-D |
Why
$coerce
all over the stack is risky - it's easy to lose track of it when making other changes.$path
and$i
) to users.Changes