Prior to PHP 7.2 using count() on a scalar value or non-countable object would return 1 or 0.
For example: https://3v4l.org/tGRDE
var_dump(count(123)); //int(1)
var_dump(count(new stdclass)); //int(1)
var_dump(count('hello world')); //int(1)
var_dump(count(null)); //int(0)
In the updates to PHP 7.2+, using count() as demonstrated above will emit a warning message.
An E_WARNING will now be emitted when attempting to count() non-countable types (this includes the sizeof() alias function).
Warning: count(): Parameter must be an array or an object that implements Countable
[sic]
As a result many popular Frameworks will elevate the E_WARNING and throw an Exception instead.
[ErrorException] count(): Parameter must be an array or an object that implements Countable
The error elevation behavior was also commented on by the PHP developers.
Environments that display warnings or convert them to more severe errors/exceptions would be affected, but this should just bring attention to a bug in the code.
How can the previous behavior of count() be achieved in PHP 7.2+, that does not emit an E_WARNING, without modifying the error reporting setting and without using #count()?
As we discussed, there are multiple ways to achieve the original functionality of count() and not emit an E_WARNING.
In PHP 7.3 a new function was added is_countable, specifically to address the E_WARNING issue and the prevalence of applications adopting is_array($var) || $var instanceof \Countable in their code.
In PHP 7.2, a Warning was added while trying to count uncountable
things. After that, everyone was forced to search and change their
code, to avoid it. Usually, the following piece of code became
standard:
if (is_array($foo) || $foo instanceof Countable) {
// $foo is countable
}
https://wiki.php.net/rfc/is-countable
Custom Function Replacement
For that reason it seems the best method of resolving the issue, is to perform the same functionality that PHP is doing with is_countable and creating a custom function to ensure compliance with the original functionality of count.
Example https://3v4l.org/8M0Wd
function countValid($array_or_countable, $mode = \COUNT_NORMAL)
{
if (
(\PHP_VERSION_ID >= 70300 && \is_countable($array_or_countable)) ||
\is_array($array_or_countable) ||
$array_or_countable instanceof \Countable
) {
return \count($array_or_countable, $mode);
}
return null === $array_or_countable ? 0 : 1;
}
Result
array: 3
string: 1
number: 1
iterator: 3
countable: 3
zero: 1
string_zero: 1
object: 1
stdClass: 1
null: 0
empty: 1
boolt: 1
boolf: 1
Notice: Undefined variable: undefined in /in/8M0Wd on line 53
undefined: 0
Shim is_countable() function
Using the above replacement function, it is also possible to shim is_countable in PHP <= 7.2, so it is only used when needed, with minimal overhead.
Example https://3v4l.org/i5KWH
if (!\function_exists('is_countable')) {
function is_countable($value)
{
return \is_array($value) || $value instanceof \Countable;
}
}
function countValid($array_or_countable, $mode = \COUNT_NORMAL)
{
if (\is_countable($array_or_countable)) {
return \count($array_or_countable, $mode);
}
return null === $array_or_countable ? 0 : 1;
}
Ignore count() Warnings
As the functionality of count() has not changed and not did not typically emit warnings in the past. An alternative to using a custom function, is to ignore the warning outright by using the # Error Control Operator
Warning: This approach has the impact of treating undefined variables as NULL and not displaying Notice: Undefined variable:
message.
Example https://3v4l.org/nmWmE
#count($var);
Result
array: 3
string: 1
number: 1
iterator: 3
countable: 3
zero: 1
string_zero: 1
object: 1
stdClass: 1
null: 0
empty: 1
boolt: 1
boolf: 1
---
Undefined: 0
Replace count() using APD extension
As for replacing the internal PHP function count(). There is a PECL extension APD (Advanced PHP Debugger), that allows for override_function that works on core PHP functions. As the extension name suggests, it is technically meant for debugging, but is a viable alternative to replacing all instances of count for a custom function.
Example
\rename_function('count', 'old_count');
\override_function('count', '$array_or_countable,$mode', 'return countValid($array_or_countable,$mode);');
if (!\function_exists('is_countable')) {
function is_countable($value)
{
return \is_array($value) || $value instanceof \Countable;
}
}
function countValid($array_or_countable, $mode = \COUNT_NORMAL)
{
if (\is_countable($array_or_countable)) {
return \old_count($array_or_countable, $mode);
}
return null === $array_or_countable ? 0 : 1;
}
The problem is that calling count() on a scalar or object that doesn't implement the Countable interface returns 1, which can easily hide bugs.
Given the following:
function handle_records(iterable $iterable)
{
if (count($iterable) === 0) {
return handle_empty();
}
foreach ($iterable as $value) {
handle_value($value);
}
}
Passing a Generator that yields nothing would not call handle_empty() nor handle_value().
Also, no indication would be given that neither were called.
By default, this will still return 1, though will additionally log a warning. If anything, this warning will bring attention to potential bugs in the code.
See Counting Non-Countables for further information.
You can solve it by using "??"-operator. If the left side is null, the right side will be used. So as we have a empty array, our result will be zero.
count(null ?? [])
Another way would be to typecast it as an array.
count((array) null)
Related
I've taken over a larger code base, that has several warnings and errors.
I keep running into statements like this:
foreach( $object->keys => $key ){
...
}
Where I can see in the error log, that $object is null.
What is the simplest way, tot check that above-written statement won't break?
To be 100% sure, I would do something like this:
if( isset( $object ) && is_array( $object->keys ) ){
foreach( $object->keys => $key ){
...
}
}
But the readability decreases significantly.
I could also make a helper function, that did it, like so:
if( myHelper::canBeForeached( $object, 'keys' ) ){ // naming would obviously be changed
foreach( $object->keys => $key ){
...
}
}
But that too seems bad.
What is the simplest/prettiest way to achieve this, to also maintain readability of the code?
There are a number of ways to approach this. Here's the shortest approach.
If you use the ?? null-coalescing operator for default value, in the event that either $object or ->keys is undefined, then [] or an empty array will be passed for iteration, and obviously nothing happens. This is one way to avoid wrapping your foreach loop inside a condition.
foreach($object->keys ?? [] as $key) {
// If $object or ->keys is undefined, nothing happens.
}
Now, some may object that this is unclear or unreadable; but at least it's not a ton of boilerplate, and it's very quick to add in. This approach assumes that if $object exists and has the property keys, then keys will be iterable. If not, it will result in a warning on must be of type array|object.
Other than that, you'd have to wrap all your loops with a condition check, isset($object), or alternatively, gettype($object) === 'object' if you want to ensure that it's an object. For some reason, is_object() will give a warning for an undefined variable. Again, is_null() returns true if the variable is undefined, essentially behaving like !isset() when negated.
As noted above, if your legacy code has objects with plural-named properties that are not iterable but also not null (e.g. ->keys = 'duck'), you'll have to do a check on your $object->keys with is_iterable() before it enters the loop for an attempt at iteration.
If you pass an undefined object into is_iterable, it's a warning again. You can, however, use the power of null-coalescing once more, where if either $object or ->keys is undefined, the check below reads as is_iterable(false) (or any other non-iterable value to evaluate):
if(is_iterable($object->keys ?? false)) {
foreach($object->keys as $key) {
//...
}
}
...and that could be the sole condition that wraps your loop.
P.S. On the possible solutions in OP: A helper function results in warnings if used with an undefined variable. Also, your if( isset( $object ) && is_array( $object->keys ) ) will result in a warning if object is defined but doesn't have the ->keys property.
I have a boolean field that is represented by 0 and 1 in my database.
if ($request->input('submitted')) {
// do code
}
This has been working because it's only been setting the field to 1 (true) but now there's a new flow that can revert the submission.
It has not been setting it back to 0 when I pass the value 0 in from the frontend and I assume it's because that condition is getting skipped since 0 would eval to null.
Is the best way to handle it:
if (isset($request->input('submitted'))) {
// do code
}
or would strictly checking against null work better:
if ($request->input('submitted') !== null) {
// do code
}
The simply approach parse your input to a boolean.
if ((bool) $request->input('submitted')) {
This will create the following results. Therefor removing your edge case.
(bool) "1" // true
(bool) "1" // false
An alternative approach is to use inbuilt PHP filter, it will parse a lot of cases more notably "true" and "false" to true and false.
if (filter_var($request->input('submitted'), FILTER_VALIDATE_BOOLEAN)) {
Goal
I would like to write a function with variable number of parameters (using ...) that calls another function with the same arguments and a new one at the end. Order is important! The example below is just for demonstration.
What I tried
function foo(...$params) {
$extraVariable = 6;
var_dump(...$params, $extraVariable);
}
foo(2, 4, 1, 4);
Problem
When I run it, I get the following error message:
PHP Fatal error: Cannot use positional argument after argument unpacking in /home/user/main.php on line 3
How can I achieve my goal?
tl;dr
Unpacking after arguments is not allowed by design, but there are 3 workarounds:
Create an array from the new element and unpack that as Paul suggested:
function foo(...$params) {
$extraVariable = 6;
var_dump(...$params, ...[$extraVariable]);
}
Push the new element to the params:
function foo(...$params) {
$extraVariable = 6;
$params[] = $extraVariable;
var_dump(...$args);
}
If the wrapped function has named params, just add the extra argument as a named one as James suggested:
// needs PHP 8.1
function foo(...$params) {
$extraVariable = true;
array_search(...$params, strict: $extraVariable);
}
Explanation
PHP simply doesn't support this. You can see the unit test that checks this behavior:
--TEST--
Positional arguments cannot be used after argument unpacking
--FILE--
<?php
var_dump(...[1, 2, 3], 4);
?>
--EXPECTF--
Fatal error: Cannot use positional argument after argument unpacking in %s on line %d
There is a workaround. You cannot use positional arguments after unpacked one, but you can use several unpacked arguments; so you can just wrap your variable(s) in array literal and unwrap it like this:
var_dump(...$params, ...[$extraVariable]);
See the bolded word?
PHP Fatal error: Cannot use positional argument after argument unpacking in /home/user/main.php on line 3
So use it before unpacking.
var_dump($extraVariable, ...$params);
This is now possible with PHP 8.1 and named parameters.
function foo(...$params) {
$this->otherfunction($params, otherParam: 'some value');
}
foo(2, 4, 1);
In otherfunction() you'll have the expanded values of $params as an array [0 => 2, 1 => 4, 2 => 1], and your $otherParam will have the value "some value".
Note: You stated:
I would like to write a function with variable number of parameters
(using ...) that calls another function with the same arguments and a
new one at the end
Which the above with named params resolves, but your example uses var_dump(). Spread doesn't work in var_dump() but then you would just dump the array itself as that's what var_dump does.
Working on a WordPress site and using Advanced Custom Fields. I am looping through an flexible content array and creating an array from the return.
The issue is I need to return differently named images in an array but the images might be null (they can be empty).
This currently works:
"images" => [
"image_one" => ( $l['image_one']['url'] ? $l['image_one']['url'] : NULL ),
/* etc */
]
But this is in a switch statement so I wanted to be able to pass the:
$l['image_one']['url']
To a function and only return the URL if there is one. However I could have a array where $l['image_three']['url'] is not set and not in the array returned so I will always get undefined offset notices.
I can carry on the way I am but its getting repetive and would rather be able to do e.g.:
"image_one" => imageExists($l['image_one']['url'])
But of course I am already calling a key that doesn't exist (possibly). Is there an other methods of tidying up my shorthand if?
Normally I would just do it inline but since you are looking for a function:
function imageExists($arr = null) {
return (empty($arr['url'])) ? null : $arr['url'];
}
imageExists($l['image_one']);
Use isset() on your ternary condition:
function imageExists($image) {
return isset($image['url']) ? $image['url'] : NULL;
}
And invoke with:
imageExists($l['image_one']);
If you're on a version >= PHP 7.0, you can use the null coalescing operator (search here.) For:
function imageExists($image) {
return $image['url'] ?? NULL;
}
My validate function looks like that
function validate($data, $data2 = 0, $type)
{
...
Function call example
if ($result = validate($lname, 'name') !== true)
response(0, $result, 'lname');
As you see, my validate function has 3 input vars. I'm not using second var - $data2 often, that's why set it to 0 by default. But when I'm calling this function as given example (as far as I know it means $data=$lname, $data2=0, $type='name') getting error message
Missing argument 3 ($type) for validate()
How can I fix that?
Missing argument 3 ($type) for validate() [1]
Always list optional arguments as the last arguments, never before non-optional arguments.
Since PHP doesn't have named parameters1 nor "overloading ala Java", that's the only way:
function validate($data, $type, $data2 = 0) {
}
1 Error with severity E_WARNING until PHP 7.0 (including); Uncaught ArgumentCountError starting with PHP 7.1rfc (and starting with PHP 8.0 as well for internal functionsrfc).
2 before PHP 8.0, see Named Arguments
You should at least set the $type in this line:
function validate($data, $data2 = 0, $type)
at NULL or '' as you can see here:
function validate($data, $data2 = 0, $type = null)
PHP let you to set a value for the parameters, but you can't define a parameter WITHOUT a preset value AFTER parameter(s) which HAVE a preset value. So if you need to always specify the third param, you have to switch the second and the third like this:
function validate($data, $type, $data2 = 0)
From http://php.net/manual/en/functions.arguments.php
Note that when using default arguments, any defaults should be on the right side of any non-default arguments; otherwise, things will not work as expected
Your should switch the second and third arguments of the function, making the optional argument the last one. So it becomes:
function validate($data, $type, $data2 = 0)
{ ....
function validate($data, $data2, $data3, $data4, $data5)
im a beginner but i think that you can use a thousand arguments
as long as you call like that
if ($result = validate($lname, 'name','','','') !== true)
Notice that starting with PHP 7.1 this will throw a PHP Fatal error, not just a warning:
PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function validate(), 2 passed in /path/to/file.php on line X and exactly 3 expected
More info: http://php.net/manual/en/migration71.incompatible.php