Asked  7 Months ago    Answers:  5   Viewed   34 times

I know that PHP doesn't yet have native Enumerations. But I have become accustomed to them from the Java world. I would love to use enums as a way to give predefined values which IDEs' auto-completion features could understand.

Constants do the trick, but there's the namespace collision problem and (or actually because) they're global. Arrays don't have the namespace problem, but they're too vague, they can be overwritten at runtime and IDEs rarely know how to autofill their keys without additional static analysis annotations or attributes.

Are there any solutions/workarounds you commonly use? Does anyone recall whether the PHP guys have had any thoughts or decisions around enumerations?

 Answers

89

Depending upon use case, I would normally use something simple like the following:

abstract class DaysOfWeek
{
    const Sunday = 0;
    const Monday = 1;
    // etc.
}

$today = DaysOfWeek::Sunday;

However, other use cases may require more validation of constants and values. Based on the comments below about reflection, and a few other notes, here's an expanded example which may better serve a much wider range of cases:

abstract class BasicEnum {
    private static $constCacheArray = NULL;

    private static function getConstants() {
        if (self::$constCacheArray == NULL) {
            self::$constCacheArray = [];
        }
        $calledClass = get_called_class();
        if (!array_key_exists($calledClass, self::$constCacheArray)) {
            $reflect = new ReflectionClass($calledClass);
            self::$constCacheArray[$calledClass] = $reflect->getConstants();
        }
        return self::$constCacheArray[$calledClass];
    }

    public static function isValidName($name, $strict = false) {
        $constants = self::getConstants();

        if ($strict) {
            return array_key_exists($name, $constants);
        }

        $keys = array_map('strtolower', array_keys($constants));
        return in_array(strtolower($name), $keys);
    }

    public static function isValidValue($value, $strict = true) {
        $values = array_values(self::getConstants());
        return in_array($value, $values, $strict);
    }
}

By creating a simple enum class that extends BasicEnum, you now have the ability to use methods thusly for simple input validation:

abstract class DaysOfWeek extends BasicEnum {
    const Sunday = 0;
    const Monday = 1;
    const Tuesday = 2;
    const Wednesday = 3;
    const Thursday = 4;
    const Friday = 5;
    const Saturday = 6;
}

DaysOfWeek::isValidName('Humpday');                  // false
DaysOfWeek::isValidName('Monday');                   // true
DaysOfWeek::isValidName('monday');                   // true
DaysOfWeek::isValidName('monday', $strict = true);   // false
DaysOfWeek::isValidName(0);                          // false

DaysOfWeek::isValidValue(0);                         // true
DaysOfWeek::isValidValue(5);                         // true
DaysOfWeek::isValidValue(7);                         // false
DaysOfWeek::isValidValue('Friday');                  // false

As a side note, any time I use reflection at least once on a static/const class where the data won't change (such as in an enum), I cache the results of those reflection calls, since using fresh reflection objects each time will eventually have a noticeable performance impact (Stored in an assocciative array for multiple enums).

Now that most people have finally upgraded to at least 5.3, and SplEnum is available, that is certainly a viable option as well--as long as you don't mind the traditionally unintuitive notion of having actual enum instantiations throughout your codebase. In the above example, BasicEnum and DaysOfWeek cannot be instantiated at all, nor should they be.

Wednesday, March 31, 2021
 
Exoon
answered 7 Months ago
89

Disclaimer: I'm not a PHP Internals expert (yet?) so this is all from my understanding, and not guaranteed to be 100% correct or complete. :)

So, firstly, the PHP 7 behaviour - which, I note, is also followed by HHVM - appears to be correct, and PHP 5 has a bug here. There should be no extra assign by reference behaviour here, because regardless of execution order, the result of the two calls to ++$i should never be the same.

The opcodes look fine; crucially, we have two temp variables $2 and $3, to hold the two increment results. But somehow, PHP 5 is acting as though we'd written this:

$i = 2;
$i++; $temp1 =& $i;
$i++; $temp2 =& $i;
echo $temp1 + $temp2; 

Rather than this:

$i = 2;
$i++; $temp1 = $i;
$i++; $temp2 = $i;
echo $temp1 + $temp2; 

Edit: It was pointed out on the PHP Internals mailing list that using multiple operations that modify a variable within a single statement is generally considered "undefined behaviour", and ++ is used as an example of this in C/C++.

As such, it's reasonable for PHP 5 to return the value it does for implementation / optimisation reasons, even if it is logically inconsistent with a sane serialization into multiple statements.

The (relatively new) PHP language specification contains similar language and examples:

Unless stated explicitly in this specification, the order in which the operands in an expression are evaluated relative to each other is unspecified. [...] (For example,[...] in the full expression $j = $i + $i++, whether the value of $i is the old or new $i, is unspecified.)

Arguably, this is a weaker claim than "undefined behaviour", since it implies they are evaluated in some particular order, but we're into nit-picking now.

phpdbg investigation (PHP 5)

I was curious, and want to learn more about the internals, so did some playing around using phpdbg.

No references

Running the code with $j = $i in place of $j =& $i, we start with 2 variables sharing an address, with a refcount of 2 (but no is_ref flag):

Address         Refs    Type            Variable
0x7f3272a83be8  2       (integer)       $i
0x7f3272a83be8  2       (integer)       $j

But as soon as you pre-increment, the zvals are separated, and only one temp var is sharing with $i, giving a refcount of 2:

Address         Refs    Type            Variable
0x7f189f9ecfc8  2       (integer)       $i
0x7f189f859be8  1       (integer)       $j

With reference assignment

When the variables have been bound together, they share an address, with a refcount of 2, and a by-ref marker:

Address         Refs    Type            Variable
0x7f9e04ee7fd0  2       (integer)       &$i
0x7f9e04ee7fd0  2       (integer)       &$j

After the pre-increments (but before the addition), the same address has a refcount of 4, showing the 2 temp vars erroneously bound by reference:

Address         Refs    Type            Variable
0x7f9e04ee7fd0  4       (integer)       &$i
0x7f9e04ee7fd0  4       (integer)       &$j

The source of the issue

Digging into the source on http://lxr.php.net, we can find the implementation of the ZEND_PRE_INC opcode:

  • PHP 5.6
  • PHP 7.0

PHP 5

The crucial line is this:

 SEPARATE_ZVAL_IF_NOT_REF(var_ptr);

So we create a new zval for the result value only if it is not currently a reference. Further down, we have this:

if (RETURN_VALUE_USED(opline)) {
    PZVAL_LOCK(*var_ptr);
    EX_T(opline->result.var).var.ptr = *var_ptr;
}

So if the return value of the decrement is actually used, we need to "lock" the zval, which following a whole series of macros basically means "increment its refcount", before assigning it as the result.

If we created a new zval earlier, that's fine - our refcount is now 2, 1 for the actual variable, plus 1 for the operation result. But if we decided not to, because we needed to hold a reference, we're just incrementing the existing reference count, and pointing at a zval which may be about to be changed again.

PHP 7

So what's different in PHP 7? Several things!

Firstly, the phpdbg output is rather boring, because integers are no longer reference counted in PHP 7; instead, a reference assignment creates an extra pointer, which itself has a refcount of 1, to the same address in memory, which is the actual integer. The phpdbg output looks like this:

Address            Refs    Type      Variable
0x7f175ca660e8     1       integer   &$i
int (2)
0x7f175ca660e8     1       integer   &$j
int (2)

Secondly, there is a special code path in the source for integers:

if (EXPECTED(Z_TYPE_P(var_ptr) == IS_LONG)) {
    fast_long_increment_function(var_ptr);
    if (UNEXPECTED(RETURN_VALUE_USED(opline))) {
        ZVAL_COPY_VALUE(EX_VAR(opline->result.var), var_ptr);
    }
    ZEND_VM_NEXT_OPCODE();
}

So if the variable is an integer (IS_LONG) and not a reference to an integer (IS_REFERENCE) then we can just increment it in place. If we then need the return value, we can copy its value into the result (ZVAL_COPY_VALUE).

If it's a reference, we won't hit that code, but rather than keeping references bound together, we have these two lines:

ZVAL_DEREF(var_ptr);
SEPARATE_ZVAL_NOREF(var_ptr);

The first line says "if it's a reference, follow it to its target"; this takes us from our "reference to an integer" to the integer itself. The second - I think - says "if it's something refcounted, and has more than one reference, create a copy of it"; in our case, this will do nothing, because the integer doesn't care about refcounts.

So now we have an integer we can decrement, that will affect all by-reference associations, but not by-value ones for refcounted types. Finally, if we want the return value of the increment, we again copy it, rather than just assigning it; and this time with a slightly different macro which will increase the refcount of our new zval if necessary:

ZVAL_COPY(EX_VAR(opline->result.var), var_ptr);
Wednesday, March 31, 2021
 
fret
answered 7 Months ago
16

Apparently in my DbConnect class my PHPDoc comments stated that I was returning 'database' when in fact I was returning a 'mysqli' datatype. This was what was causing the error. The simply fix to this problem was to change be PHPDoc annotation to 'mysqli' and the code completions started to work again.

Saturday, May 29, 2021
 
viper
answered 5 Months ago
88

Silly mistake on my part... simply forgot to add phpunit as a dependency in the project. For anyone else that gets this error, to composer.json add:

"require-dev": {
    "phpunit/phpunit": "3.7.*"
},

And then run:

composer update

That solved the problem.

Friday, September 17, 2021
 
Samir Sabri
answered 1 Month ago
59

In order to have PHAR archive indexed by IDE it has to have .phar extension (that's a requirement).

The easiest solution is to place phpunit.phar somewhere in your project (usually it would be PROJECT_ROOT/vendor/ folder).

If having local copy is not desired (for whatever reason; although composer and other similar kind of tools (bower/npm/etc) are primarily aimed at keeping dependency stuff/packages locally), you may use symbolic links: either create a symbolic link to that file locally (e.g. PROJECT_ROOT/phpunit.phar --> /usr/bin/phpunit) .. or place a full copy (or such symbolic link) in separate folder somewhere on your disk and then reference it via PhpStorm's Settings | PHP | Include Paths functionality.

Saturday, September 18, 2021
 
kensil
answered 1 Month ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :