After a refactoring, we had something like this in one of our classes:
class FooBar
{
// $foo was $bla before
private $foo;
public function setBlubbOnArrayOnlyOnce($value)
{
// $this->bla was forgotten during refactoring. Must be $this->foo
if(!isset($this->bla['blubb'])) {
$this->foo['blubb'] = $value;
}
}
}
So in the end $this->foo['blubb'] was always set, not only once. This happens because of the magic methods of PHP. We don't want it to be possible to access fields dynamically, so I thought I just add a codesniffer rule. But I didn't found any and asked me why.
PHPStorm shows a field declared dynamically notice there, but I want this to automatically fail with codesniffer (or something similar) during our deployment cycle.
Has anybody an idea on this? Is there a good rule? Should I write my own and how? Or would it be bad practice to disable it?
Disclaimer: We use tests, but sometimes you miss things... It would be good to prevent this in the first place. Also, please don't come up with overwriting the magic methods. I don't want to have a trait/abstract whatever in every class.
The token- and scope-based approach used by PHP_CodeSniffer makes it easy to check all switch -cases for top-level continue -statements (i.e. not within a nested looping structure) and see if they have a numeric 'level'-argument. We disallow any such continue -statements without an explicit number of looping levels to jump over:
Dynamic properties are property that are not declared in the class, but in the code in your class, you want to use a not declared property: In the example above, in the constructor method I accessed the age property, through $this->age. In this case PHP will create dynamically the property. // Assigns declared property User::$name.
Using the ->{$nid_property} notation says, evaluate the variable $nid_property, and substitute its value with the name of the property to access. So PHP eventually interprets this as $row->my_nid. The beauty is that $nid_property can be set based on some other logic that determines, through whatever means, what the property name ought to be.
This is not a codesniffer or phpstorm problem. And you cant want fix this problem with codesniffer or IDE. IDE, codesniffer, phpdocumentor, etc. -- this is "statically" analyse. And for dynamic analyse you can use e.g. phpunit.
If you want check existence of property you must use property_exists() function.
class X
{
public function __get($name)
{
$this->{$name} = null;
return $this->{$name};
}
}
$x = new X();
var_dump(property_exists($x, 'foo')); // false
var_dump($x->foo); // NULL
var_dump(property_exists($x, 'foo')); // true
Or may be you can use reflection for property http://php.net/manual/en/class.reflectionproperty.php
If you want check for "isset" you must known:
var_dump(isset($x), $x); // false + NULL with notice
$x = null;
var_dump(isset($x), $x); // false + NULL
unset($x);
var_dump(isset($x), $x); // false + NULL without notice
When you shure for this case of check you can use isset()
But you should always first check for existence of property. Otherwise you can have undefined behaviour of your code.
After a refactoring
It would be good to prevent this in the first place.
You can only catch these kind of refactoring errors by running tests after each refactoring step. This error will also bubble up, because foo['blubb']
is set to a specific value and this should cause an unwanted effect in another test - not only in the test for the setter logic.
We use tests, but sometimes you miss things...
Yes, its quite common that the coverage is not high enough. That's why having a good test coverage is the starting point for all refactorings.
These two lines were not "green" in your coverage report:
if(!isset($this->bla['blubb'])) {
$this->foo['blubb'] = $value;
Also, please don't come up with overwriting the magic methods. I don't want to have a trait/abstract whatever in every class.
You have excluded it, but that's one way to catch the properties: by using the magic function __set()
(for inaccessible vars) or property_exists()
or the use of Reflection*
classes to find.
Now, that its too late, you want another tool to catch the error, ok:
The tool would need to parse the PHP file and its parents (because of variable scope) and find $this->bla
without a prior public|private|protected
variable (class property) declaration. This will not indicate the exact type of error, just that "bla" was accessed without declaration.
Its possible to implement this as a CodeSniffer rule.
You can also give http://phpmd.org/ or https://scrutinizer-ci.com/ a try. And, in case you are using PHP7: https://github.com/etsy/phan
tl;tr
Its complicated to determine the exact error and its context without running, evaluating and analyzing the underlying code. Just think about "dynamic variable names" and you know why: you don't even know the name of the property by looking at the source-code, because its build dynamically during program flow. A static analyzer wouldn't catch that.
A dynamical analyzer has to track all things, here $this->
accesses and would take the context into account: !isset(x). Context evaluation can find lots of common coding mistakes. In the end you can build a report: saying that $this->bla was accessed only 1 time and that indicates either that
Now in 2017, you'are looking for tool PHPStan. I link short intro I wrote for first time users.
It does exactly what you need!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With