How do you enforce your PHP method arguments? - php

How do you validate/manage your PHP method arguments and why do you do it this way?

Well, assuming that you're talking about type-checking method arguments, it depends:
If it's expecting an object, I use type-hinting with an interface:
public function foo(iBar $bar)
If it's expecting an array only, I use type-hinting with the array keyword.
public function foo(array $bar)
If it's expecting a string, int, bool or float, I cast it:
public function foo($bar) {
$bar = (int) $bar;
}
If it's expecting mixed, I just check in a cascade:
public function foo($bar) {
if (is_string($bar)) {
//handle string case
} elseif (is_array($bar)) {
//...
} else {
throw new InvalidArgumentException("invalid type");
}
}
Lastly, if it's expecting an iterable type, I don't use type-hinting. I check if it's an array first, then re-load the iterator:
public function foo($bar) {
if (is_array($bar)) {
$bar = new ArrayIterator($bar);
}
if (!$bar instanceof Traversable) {
throw new InvalidArgumentException("Not an Iterator");
}
}
If it's expecting a filename or directory, just confirm it with is_file:
public function foo($bar) {
if (!is_file($bar)) {
throw new InvalidArgumentException("File doesn't exist");
}
}
I think that handles most of the cases. If you think of any others, I'll gladly try to answer them...

Typechecking is something you should do at the development stage, not in production. So the appropriate syntactic feature for that would be:
function xyz($a, $b) {
assert(is_array($a));
assert(is_scalar($b));
However I'll try to avoid it, or use type coercion preferrably. PHP being dynamically typed does quite well adapting to different values. There are only few spots where you want to turndown the basic language behaviour.

Related

Check if callable can receive class as parameter in php

I have a callable $f and I would like to know if it can receive an instance of a certain class Foo as input.
At the moment I'm doing something like
try {
$f($foo);
} catch (\TypeError $e) {
throw new \InvalidArgumentException('The provided function can not evaluate inputs of this type');
}
Is there a way to check this WITHOUT actually invoking the callable? Maybe with reflection or some other dark magic?
If you want to be able to reflect any kind of callable, you'll need to wrap up the logic in a small function. Depending on whether you've got an array, a function name or an anonymous function, you need to create either a ReflectionFunction or ReflectionMethod. Fortunately, these both extend ReflectionFunctionAbstract, so we can type-hint the return value.
function reflectCallable($arg): ReflectionFunctionAbstract {
if (is_array($arg)) {
$ref = new ReflectionMethod(...$arg);
} elseif (is_callable($arg)) {
$ref = new ReflectionFunction($arg);
}
return $ref;
}
This will return you the appropriate object for your callable value, which you can then use to fetch the parameters and act accordingly:
function definedFunc(Foo $foo) {}
$callable = function(Foo $foo) {};
class Bar { public function baz(Foo $foo) {} }
foreach (['definedFunc', $callable, ['Bar', 'baz']] as $callable) {
$reflected = reflectCallable($callable);
if ((string) $reflected->getParameters()[0]->getType() === 'Foo') {
echo 'Callable takes Foo', PHP_EOL;
}
}
See https://3v4l.org/c5vmM
Note that this doesn't do any error handling - you'll probably get warnings/notices if the callable doesn't take any parameters or the first parameter doesn't have a type. It also requires PHP 7+, but hopefully that's not an issue.
It doesn't currently support objects that implement __invoke or static calls defined as "Foo::bar", but they wouldn't be too hard to add if necessary. I've just found something very similar in the source of Twig, which does a more thorough job: https://github.com/twigphp/Twig/blob/v2.8.0/src/Node/Expression/CallExpression.php#L280
You can with ReflectionParameter::getType:
$f = function(Foo $foo) {};
$reflectionFunc = new ReflectionFunction($f);
$reflectionParams = $reflectionFunc->getParameters();
$reflectionType1 = $reflectionParams[0]->getType();
echo $reflectionType1;
output:
Foo

How much type-inference can you possibly cram out of vanilla PHP?

I want to make a type-inferring linter for a subset of PHP, but am unsure about how much type information you can get out of it without having to enforce phpdoc annotations or similar. Consider this example:
function a() {
b(10); // This is wrong, but we don't know the type of b()
}
function b($c) {
print($c . " hallo"); // Only allow concatenations with strings
}
Without the in OCaml present and for mutual recursion, things have to be defined in order of use. The way to get around it is to make a two-stage type-checker, where the first stage checks interfaces, but type-inferring needs to go through the body of the functions.
One way to get around this is to enforce phpdoc docblocks:
function a() {
b(10); // Type error: b() expects a string
}
/**
* #param string $c
*/
function b($c) {
print($c . " hallo");
}
Enforcing docblocks to be used in type-checking feels... wrong. Is there any way around this? Of course the scalar type-hints v5, using declare(strict_types=1), would allow scalar type-hints in function signatures, but that's too far into the future.
Edit: I'm stupid, b() could of course be inferred from it's usage in a(), so we would have:
function a() {
b(10); // b() inferred to int -> void
}
function b($c) {
print($c . " hallo"); // Wrong, $c used as a string
}
You could create classes that represent the scalar values you want to type hint.
class String {
protected $string;
public function __construct($value) {
if (!is_string($value)) {
throw new Exception('Not a string');
}
$this->string = $value;
}
public function __toString() {
return $this->string;
}
}
Then in your function declaration:
function b(String $c) {
...
}
b(new String('Some String'));

PHP Argument Order Issue: Bug or Errata?

I am new to PHP and encountered some odd behavior that may or may not be a bug in my version of PHP (5.4.13). I found a case where the sequence of function arguments matters in the function declaration. The in the following example:
class Book {
function __construct() { }
public static function withDescriptors($isNotOrmObject, $isArray, $isScalar) {
$instance = new self();
// initialization here
return $instance;
}
}
Invoking withDescriptors causes an 'array to string conversion' exception. The error is thrown when withDescriptors is invoked, ie. withDescriptors is never actually executed. However, switching the object parameter with the array parameter solves the problem. I.e.
public static function withDescriptors($isArray, $isNotOrmObject, $isScalar){ ....
Is this a known characteristic of PHP or is this a bug?
More explicitly:
class Book {
function __construct() { }
public static function withDescriptors($isNotOrmObject, $isArray, $isScalar) {
$instance = new self();
// initialization here
return $instance;
}
}
$book = Book::withDescriptors($isNotORMobject, $isArray, $isScalar);
FAILS and
class Book {
function __construct() { }
public static function withDescriptors($isArray, $isNotORMobject, $isScalar) {
$instance = new self();
// initialization here
return $instance;
}
}
$book = Book::withDescriptors($isArray, $isNotORMobject, $isScalar);
WORKS great. The ONLY difference is the parameter sequence, the "initialization here" code is identical.
The reason you don't get the warning in your second example, is that the Object you're passing as the second parameter is implementing a magic __toString() method. PHP is not a strong typed language, but more recent versions have limited capabilities of type hinting.
To illustrate the warning;
function saySomething($shouldBeAString)
{
echo $shouldBeAString
}
saySomething(array('hello'));
Outputs
'Array'
and a warning array to string conversion
class Foo {
public function __toString() {
return 'Hi I am Foo';
}
}
$bar = new Foo();
saySomething($bar);
Will output
'Hi I am Foo'
Without warning
As mentioned, PHP offers limited type hinting. You can specify a required 'class/object type' and 'array' as accepted arguments, but not scalar types, like 'string', 'int', 'bool' etc.
function (array $myarray) {
// will only accept arrays
// or something that implements array-access
}
function (Foo $var) {
// Will only accept 'Foo' objects
}
Polymorphism / Function overloading
Some other languages allow defining the same method/function multiple times, but with a different signature (other argument types). PHP does not explicitly support this.
For example, in other languages, this is allowed:
function doSomething(string $arg1) { ......}
function doSomething(array $arg1) { .... }
function doSomething(string $arg1, string $arg2) { ... }
In those languages, depending on the type and number of arguments, variant 1, 2 or 3 will be executed. PHP does not support this as it requires functions/methods to have a unique name. PHP will therefore complain that function doSomething() is already defined.
However, you can create something similar in PHP in several ways;
// rename the functions/methods so that they have a unique name
function _doSomething1(string $arg1) { ......}
function _doSomething2(array $arg1) { .... }
function _doSomething3(string $arg1, string $arg2) { ... }
// create the 'wrapper' function *without arguments specified*
function doSomething() {
// determin which variant should be executed
$numargs = func_num_args();
$args = func_get_args();
if ($numargs == 2) {
// 2 arguments -> variant 3
return call_user_func_array('_doSomething3', $args);
} else if {$numargs == 1) {
if (is_array($args[0]) {
// first argument is an array
return call_user_func_array('_doSomething2', $args);
} else {
return call_user_func_array('_doSomething1', $args);
}
}
}
Note: code above is 'fake' code, just to illustrate the idea!
In general, order matters in function arguments.
The only time it doesn't is when arguments are the same type or your function implements something like get opt long.
There are some good answers here but this particular situation arose from a bug in the PHP interpreter.

Instancing an object and calling a method in one line?

php 5.3
Is there a way to do this (viable in java for example)
(new MyClass())->myMethod();
i am receving: Parse error: syntax error, unexpected T_OBJECT_OPERATOR in D.. on line 7
Add
I really need that RFC to be implemented in the next PHP version!
http://wiki.php.net/rfc/instance-method-call
Is there a way we can subscribe to it so it can get more attention?
No, its not possible. There is a RFC for that
http://wiki.php.net/rfc/instance-method-call
But no one knows, when this will come to the userland.
Jacob mentioned the static method. There are other more or less useful methods to achieve the same
function instanciate($className, $arg1 = null) {
$args = func_get_args();
array_shift($args);
$c = new ReflectionClass($className);
return $c->newInstanceArgs($c);
}
instanciate('Classname', 1, 2, 3)->doSomething();
However, I prefer the temporary variable (like in the question).
Update:
I can swear there where an example for the temporary variable stuff in the question in the past. However, I meant this
$x = new Class;
$x->method();
where $x is the temporary variable.
That is not valid syntax. A handy way to achieve what you want is to use a static method to create the object.
In your MyClass:
public static function create() {
return new MyClass();
}
Then you can use:
MyClass::create()->myMethod();
However it is extra code that you have to maintain, if for example the constructor is changed or the class is extended. So you need to weigh up the benefits.
You can do something like this:
function chain_statements($statement1, $statement2) { return $statement2; }
class TClass { public Method() { ...; return $this; } }
$b = chain_statements($a = new TClass(), $a->Method());
... or more generalized:
function chain_statements(array $statements) { return end($statements); }
For example:
function chain_statements($statement1, $statement2) { return $statement2; }
function chain_statements2(array $statements) { return end($statements); }
class TClass
{
public $a = 0;
public function Method1() { $this->a = $this->a + 1; return $this; }
public function Method2() { $this->a = $this->a + 2; return $this; }
}
$b = chain_statements($c = new TClass(), $c->Method1()); echo($b->a);
$b = chain_statements2(array($c = new TClass(), $c->Method1(), $c->Method2())); echo($b->a);
... or even better:
function call_method($object) { return $object; }
$b = call_method(new TClass())->Method2(); echo($b->a);
Not as such. In PHP new is not an expression, but a language construct. The common workaround is to provide a static instantiation method for MyClass::get()->... use.
A more concise alternative is a hybrid factory function:
function MyClass() { return new MyClass; }
class MyClass {
...
}
Which then simplifies the instantiation to MyClass()->doSomething();
You can put it in one statement if you really wanted to. Use eval() ;p
But you probably shouldn't.
I had this same problem a while ago but I found this simple solution which is pretty readable too. I like the fact it uses only the standard PHP functions. There's no need to create any utility functions of your own.
call_user_func(
array(new ClassToInstance(), 'MethodName'),
'Method arguments', 'go here'
);
You can also use call_user_func_array to pass the arguments as an array.
call_user_func_array(
array(new ClassToInstance(), 'MethodName'),
array('Method arguments', 'go here')
);

Is there an is_iterable or similar function for PHP? [duplicate]

I have a lot of functions that either have type hinting for arrays or use is_array() to check the array-ness of a variable.
Now I'm starting to use objects that are iterable. They implement Iterator or IteratorAggregate. Will these be accepted as arrays if they pass through type hinting, or undergo is_array()?
If I have to modify my code, is there a generic sort of is_iterable(), or must I do something like:
if ( is_array($var) OR $var instance_of Iterable OR $var instanceof IteratorAggregate ) { ... }
What other iterable interfaces are out there?
I think you mean instanceof Iterator, PHP doesn't have an Iterable interface. It does have a Traversable interface though. Iterator and IteratorAggregate both extend Traversable (and AFAIK they are the only ones to do so).
But no, objects implementing Traversable won't pass the is_array() check, nor there is a built-in is_iterable() function. A check you could use is
function is_iterable($var) {
return (is_array($var) || $var instanceof Traversable);
}
To be clear, all php objects can be iterated with foreach, but only some of them implement Traversable. The presented is_iterable function will therefore not detect all things that foreach can handle.
PHP 7.1.0 has introduced the iterable pseudo-type and the is_iterable() function, which is specially designed for such a purpose:
This […] proposes a new iterable pseudo-type. This type is analogous to callable, accepting multiple types instead of one single type.
iterable accepts any array or object implementing Traversable. Both of these types are iterable using foreach and can be used with yield from within a generator.
function foo(iterable $iterable) {
foreach ($iterable as $value) {
// ...
}
}
This […] also adds a function is_iterable() that returns a boolean: true if a value is iterable and will be accepted by the iterable pseudo-type, false for other values.
var_dump(is_iterable([1, 2, 3])); // bool(true)
var_dump(is_iterable(new ArrayIterator([1, 2, 3]))); // bool(true)
var_dump(is_iterable((function () { yield 1; })())); // bool(true)
var_dump(is_iterable(1)); // bool(false)
var_dump(is_iterable(new stdClass())); // bool(false)
I actually had to add a check for stdClass, as instances of stdClass do work in foreach loops, but stdClass does not implement Traversable:
function is_iterable($var) {
return (is_array($var) || $var instanceof Traversable || $var instanceof stdClass);
}
I use a simple (and maybe a little hackish) way to test for "iterability".
function is_iterable($var) {
set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext)
{
throw new \ErrorException($errstr, null, $errno, $errfile, $errline);
});
try {
foreach ($var as $v) {
break;
}
} catch (\ErrorException $e) {
restore_error_handler();
return false;
}
restore_error_handler();
return true;
}
When you try to loop a non iterable variable, PHP throws a warning. By setting a custom error handler prior the attempt to iterate, you can transform an error into an exception thus enabling you to use a try/catch block. Afterwards you restore the previous error handler to not disrupt the program flow.
Here's a small test case (tested in PHP 5.3.15):
class Foo {
public $a = 'one';
public $b = 'two';
}
$foo = new Foo();
$bar = array('d','e','f');
$baz = 'string';
$bazinga = 1;
$boo = new StdClass();
var_dump(is_iterable($foo)); //boolean true
var_dump(is_iterable($bar)); //boolean true
var_dump(is_iterable($baz)); //boolean false
var_dump(is_iterable($bazinga)); //bolean false
var_dump(is_iterable($boo)); //bolean true
Unfortunately you won't be able to use type hints for this and will have to do the is_array($var) or $var instanceof ArrayAccess
stuff. This is a known issue but afaik it is still not resolved. At least it doesn't work with PHP 5.3.2 which I just tested.
You CAN use type hinting if you switch to using iterable objects.
protected function doSomethingWithIterableObject(Iterator $iterableObject) {}
or
protected function doSomethingWithIterableObject(Traversable $iterableObject) {}
However, this can not be used to accept iterable objects and arrays at the same time. If you really want to do that could try building a wrapper function something like this:
// generic function (use name of original function) for old code
// (new code may call the appropriate function directly)
public function doSomethingIterable($iterable)
{
if (is_array($iterable)) {
return $this->doSomethingIterableWithArray($iterable);
}
if ($iterable instanceof Traversable) {
return $this->doSomethingIterableWithObject($iterable);
}
return null;
}
public function doSomethingIterableWithArray(array $iterable)
{
return $this->myIterableFunction($iterable);
}
public function doSomethingIterableWithObject(Iterator $iterable)
{
return $this->myIterableFunction($iterable);
}
protected function myIterableFunction($iterable)
{
// no type checking here
$result = null;
foreach ($iterable as $item)
{
// do stuff
}
return $result;
}

Categories