Throw exception for conditional flow methods without underscore prefix#110
Conversation
…prefix on PropelConditionalProxy Previously, __call() silently returned $this for ALL method calls, including conditional flow methods called without the underscore prefix (e.g. endif() instead of _endif()). This made such typos impossible to detect — the proxy would swallow the call and all subsequent chained calls, producing silently incorrect query results. Now detects calls to conditional flow methods (if, elseif, else, endif) without the underscore prefix and throws a BadMethodCallException with a helpful message. All other method calls are still silently skipped as before, since Criteria subclasses use __call for virtual methods (filterByX, orderByX, etc.).
fdffe9e to
83e364a
Compare
|
Wow! Awesome you have spotted this! Strange though that this has gone unnoticed for so long. Do you think it also makes sense to add |
|
@kasparsatke yea, not sure. I know I've run into this issue before as well. It's always been strange to me that underscore methods were chosen for these, instead of just making them reserved. Another option would be to add non-underscore aliases for them. But either way, this will prevent these mistakes going forward. I added |
mringler
left a comment
There was a problem hiding this comment.
Very nice! Though I am not sure if this line of user-friendliness and forward thinking actually fits with the inherited project guidelines. Wouldn't we rob later users of that hour of fun you had figuring out why the damn query is not working? Are we sure users don't actually like it when they feel tricked?
But seriously, I think there is also an issue with typing here. Shouldn't the conditional methods be typed to return static (even though they don`t)?
Currently, it goes:
FooQuery::create()
->filterByFizz() // FooQuery
->_if(...) // Criteria|ConditionalProxy
->filterByBuzz() // unknown method ("passed to _call of Criteria")
->_else() // don't know what this is even called on
->filterByFizzBuzz() // hope you know
->end() // what you are doing
->filterByFoo() // good luck and have fun typing it out!Should we use the occasion and think about improving this too?
| public function __call(string $name, array $arguments) | ||
| { | ||
| if (in_array($name, ['if', 'elseif', 'else', 'endif', 'or', 'and'], true)) { | ||
| throw new BadMethodCallException(sprintf( |
There was a problem hiding this comment.
I'm constantly replacing these sprintf calls in the code, if I ran into this, I'd almost mechanically change it to:
$className = get_class($this->criteria);
throw new BadMethodCallException("Call to undefined method {$classname}::{$name}(). Did you mean '_{$name}' (with underscore prefix)?"Easier to see what goes where, fewer lines, no nested call.
Is there a case for sprintf that I am not seeing?
There was a problem hiding this comment.
The only use case where I ever use sprintf() is in combination with gettext() when a translatable string has parameters to be replaced. Other than that I never use sprintf().
So if you are not getting an itch right now to internationalize our exception messages, just proceed changing ...
There was a problem hiding this comment.
Yea, no reason really. I tend to use both. You can't use method chained calls with string interpolation, which often gets me when trying to use it.
Updated it - how's that? You can see by my commits how string interpolation in PHP can be frustrating to deal with. It's unfortunate. I really wish we had Javascripts template strings.
There was a problem hiding this comment.
I think in interpolation, you can do method calls on objects, but nothing static and no global functions. For the holy cow of readability, I do prefer variables anyway. Something like "The car '{$car->getName()}' is {$car->getColor()}" is ok, but just not as good as "The car '$carName' is $carColor".
No need for newlines around the message IMO, it is too long anyway, and it creates the appearance that something unexpected is going on there. Admittedly this gets into preference/pedant territory. It's fine! But it's again something that I might change almost mechanically when passing by.
Oh, by the way, talking about testing via commits, that's alright of course, but in case you missed the announcement, there are docker container configurations at perplorm/perpl-test-docker that should make it ease to run the tests locally. Works great for me, far less hassle.
There was a problem hiding this comment.
@mringler nice, I'll test those those Docker containers next time.
On the variables vs method calls. Yes, sometimes a variable name can add clarity. However, it also takes up memory, so it's something to consider.
Chained method calls don't work in string interpolation, $car->getBrand()->getName()
This should probably be addressed as well. But I think it should be another PR. |
|
Alright, let's tackle the types in another PR |
Summary
PropelConditionalProxy::__call()now detects calls to conditional flow methods (if,elseif,else,endif) without the required underscore prefix and throws aBadMethodCallExceptionfilterByX,orderByX, etc.) handled byCriteria::__call()Problem
The previous
__call()implementation silently returned$thisfor all method calls. This meant that calling->endif()(instead of->_endif()) would be silently swallowed by the proxy, along with all subsequent chained calls (filterBy*,find(), etc.), producing silently incorrect query results.This is an extremely difficult bug to diagnose — the query appears to execute normally but returns wrong data because the conditional block is never properly closed.
Solution
Added an explicit check for the four conditional flow method names (
if,elseif,else,endif) called without their underscore prefix. These are the only methods where calling without the prefix is always a mistake (the real methods are_if,_elseif,_else,_endif).All other arbitrary method names continue to be silently skipped, since
CriteriaandModelCriteriause__callfor virtual/magic methods likefilterByCompanyId,orderByName, etc.Test plan
if,elseif,else,endif) without underscore throwBadMethodCallExceptionCriteriaFluidConditionTest::testIfpasses (uses->foo()as arbitrary method)