Skip to content

Commit

Permalink
Merge pull request #1088 from GrahamCampbell/php8
Browse files Browse the repository at this point in the history
[1.3] Improved PHP 8.0 support
  • Loading branch information
davedevelopment committed Aug 11, 2020
2 parents 5b27e6b + dabc53c commit d55b877
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 85 deletions.
1 change: 1 addition & 0 deletions .styleci.yml
Expand Up @@ -9,4 +9,5 @@ disabled:

finder:
not-name:
- Php80LanguageFeaturesTest.php
- SemiReservedWordsAsMethods.php
8 changes: 5 additions & 3 deletions .travis.yml
Expand Up @@ -58,7 +58,7 @@ before_install:
' >> ~/.phpenv/versions/"$(phpenv version-name)"/etc/conf.d/travis.ini
fi
if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
composer require --dev --no-update "phpunit/phpunit:^8.5|^9.0"
composer require --dev --no-update "phpunit/phpunit:^9.3.2"
fi
install:
Expand All @@ -68,9 +68,11 @@ install:
script:
- |
if [[ $TRAVIS_PHP_VERSION == 5.6 ]]; then
./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP56";
./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP5";
elif [[ $TRAVIS_PHP_VERSION == 'nightly' ]]; then
./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP8";
else
./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite";
./vendor/bin/phpunit --coverage-text --coverage-clover="build/logs/clover.xml" --testsuite="Mockery Test Suite PHP7";
fi
after_success:
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Expand Up @@ -36,7 +36,7 @@
"hamcrest/hamcrest-php": "^2.0.1"
},
"require-dev": {
"phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0|~9.0"
"phpunit/phpunit": "^5.7.10|^6.5|^7.5|^8.5|^9.3"
},
"autoload": {
"psr-0": {
Expand All @@ -48,6 +48,9 @@
"test\\": "tests/"
}
},
"config": {
"preferred-install": "dist"
},
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
Expand Down
128 changes: 60 additions & 68 deletions library/Mockery/Container.php
Expand Up @@ -120,80 +120,72 @@ public function mock(...$args)
$builder->setConstantsMap(\Mockery::getConfiguration()->getConstantsMap());

while (count($args) > 0) {
$arg = current($args);
$arg = array_shift($args);
// check for multiple interfaces
if (is_string($arg) && strpos($arg, ',') && !strpos($arg, ']')) {
$interfaces = explode(',', str_replace(' ', '', $arg));
$builder->addTargets($interfaces);
array_shift($args);

continue;
} elseif (is_string($arg) && substr($arg, 0, 6) == 'alias:') {
$name = array_shift($args);
$name = str_replace('alias:', '', $name);
$builder->addTarget('stdClass');
$builder->setName($name);
continue;
} elseif (is_string($arg) && substr($arg, 0, 9) == 'overload:') {
$name = array_shift($args);
$name = str_replace('overload:', '', $name);
$builder->setInstanceMock(true);
$builder->addTarget('stdClass');
$builder->setName($name);
continue;
} elseif (is_string($arg) && substr($arg, strlen($arg)-1, 1) == ']') {
$parts = explode('[', $arg);
if (!class_exists($parts[0], true) && !interface_exists($parts[0], true)) {
throw new \Mockery\Exception('Can only create a partial mock from'
. ' an existing class or interface');
}
$class = $parts[0];
$parts[1] = str_replace(' ', '', $parts[1]);
$partialMethods = array_filter(explode(',', strtolower(rtrim($parts[1], ']'))));
$builder->addTarget($class);
foreach ($partialMethods as $partialMethod) {
if ($partialMethod[0] === '!') {
$builder->addBlackListedMethod(substr($partialMethod, 1));
continue;
if (is_string($arg)) {
foreach (explode('|', $arg) as $type) {
if ($arg === 'null') {
// skip PHP 8 'null's
} elseif (strpos($type, ',') && !strpos($type, ']')) {
$interfaces = explode(',', str_replace(' ', '', $type));
$builder->addTargets($interfaces);
} elseif (substr($type, 0, 6) == 'alias:') {
$type = str_replace('alias:', '', $type);
$builder->addTarget('stdClass');
$builder->setName($type);
} elseif (substr($type, 0, 9) == 'overload:') {
$type = str_replace('overload:', '', $type);
$builder->setInstanceMock(true);
$builder->addTarget('stdClass');
$builder->setName($type);
} elseif (substr($type, strlen($type)-1, 1) == ']') {
$parts = explode('[', $type);
if (!class_exists($parts[0], true) && !interface_exists($parts[0], true)) {
throw new \Mockery\Exception('Can only create a partial mock from'
. ' an existing class or interface');
}
$class = $parts[0];
$parts[1] = str_replace(' ', '', $parts[1]);
$partialMethods = array_filter(explode(',', strtolower(rtrim($parts[1], ']'))));
$builder->addTarget($class);
foreach ($partialMethods as $partialMethod) {
if ($partialMethod[0] === '!') {
$builder->addBlackListedMethod(substr($partialMethod, 1));
continue;
}
$builder->addWhiteListedMethod($partialMethod);
}
} elseif (class_exists($type, true) || interface_exists($type, true) || trait_exists($type, true)) {
$builder->addTarget($type);
} elseif (!\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && (!class_exists($type, true) && !interface_exists($type, true))) {
throw new \Mockery\Exception("Mockery can't find '$type' so can't mock it");
} else {
if (!$this->isValidClassName($type)) {
throw new \Mockery\Exception('Class name contains invalid characters');
}
$builder->addTarget($type);
}
$builder->addWhiteListedMethod($partialMethod);
}
array_shift($args);
continue;
} elseif (is_string($arg) && (class_exists($arg, true) || interface_exists($arg, true) || trait_exists($arg, true))) {
$class = array_shift($args);
$builder->addTarget($class);
continue;
} elseif (is_string($arg) && !\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && (!class_exists($arg, true) && !interface_exists($arg, true))) {
throw new \Mockery\Exception("Mockery can't find '$arg' so can't mock it");
} elseif (is_string($arg)) {
if (!$this->isValidClassName($arg)) {
throw new \Mockery\Exception('Class name contains invalid characters');
break; // unions are "sum" types and not "intersections", and so we must only process the first part
}
$class = array_shift($args);
$builder->addTarget($class);
continue;
} elseif (is_object($arg)) {
$partial = array_shift($args);
$builder->addTarget($partial);
continue;
} elseif (is_array($arg) && !empty($arg) && array_keys($arg) !== range(0, count($arg) - 1)) {
// if associative array
if (array_key_exists(self::BLOCKS, $arg)) {
$blocks = $arg[self::BLOCKS];
}
unset($arg[self::BLOCKS]);
$quickdefs = array_shift($args);
continue;
$builder->addTarget($arg);
} elseif (is_array($arg)) {
$constructorArgs = array_shift($args);
continue;
if (!empty($arg) && array_keys($arg) !== range(0, count($arg) - 1)) {
// if associative array
if (array_key_exists(self::BLOCKS, $arg)) {
$blocks = $arg[self::BLOCKS];
}
unset($arg[self::BLOCKS]);
$quickdefs = $arg;
} else {
$constructorArgs = $arg;
}
} else {
throw new \Mockery\Exception(
'Unable to parse arguments sent to '
. get_class($this) . '::mock()'
);
}

throw new \Mockery\Exception(
'Unable to parse arguments sent to '
. get_class($this) . '::mock()'
);
}

$builder->addBlackListedMethods($blocks);
Expand Down
2 changes: 2 additions & 0 deletions library/Mockery/Generator/Parameter.php
Expand Up @@ -46,6 +46,8 @@ public function __call($method, array $args)
* This will be null if there was no type, or it was a scalar or a union.
*
* @return \ReflectionClass|null
*
* @deprecated since 1.3.3 and will be removed in 2.0.
*/
public function getClass()
{
Expand Down
54 changes: 44 additions & 10 deletions library/Mockery/Reflector.php
Expand Up @@ -66,11 +66,11 @@ public static function getTypeHint(\ReflectionParameter $param, $withoutNullable
}

$type = $param->getType();
$declaringClass = $param->getDeclaringClass()->getName();
$declaringClass = $param->getDeclaringClass();
$typeHint = self::typeToString($type, $declaringClass);

// PHP 7.1+ supports nullable types via a leading question mark
return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? sprintf('?%s', $typeHint) : $typeHint;
return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint;
}

/**
Expand All @@ -89,11 +89,11 @@ public static function getReturnType(\ReflectionMethod $method, $withoutNullable
}

$type = $method->getReturnType();
$declaringClass = $method->getDeclaringClass()->getName();
$declaringClass = $method->getDeclaringClass();
$typeHint = self::typeToString($type, $declaringClass);

// PHP 7.1+ supports nullable types via a leading question mark
return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? sprintf('?%s', $typeHint) : $typeHint;
return (!$withoutNullable && \PHP_VERSION_ID >= 70100 && $type->allowsNull()) ? self::formatNullableType($typeHint) : $typeHint;
}

/**
Expand Down Expand Up @@ -151,6 +151,8 @@ private static function getLegacyTypeHint(\ReflectionParameter $param)
/**
* Compute the class name using legacy APIs, if possible.
*
* This method MUST only be called on PHP 5.
*
* @param \ReflectionParameter $param
*
* @return string|null
Expand Down Expand Up @@ -181,12 +183,12 @@ private static function getLegacyClassName(\ReflectionParameter $param)
*
* This method MUST only be called on PHP 7+.
*
* @param \ReflectionType $type
* @param string $declaringClass
* @param \ReflectionType $type
* @param \ReflectionClass $declaringClass
*
* @return string|null
*/
private static function typeToString(\ReflectionType $type, $declaringClass)
private static function typeToString(\ReflectionType $type, \ReflectionClass $declaringClass)
{
// PHP 8 union types can be recursively processed
if ($type instanceof \ReflectionUnionType) {
Expand All @@ -198,8 +200,40 @@ private static function typeToString(\ReflectionType $type, $declaringClass)
// PHP 7.0 doesn't have named types, but 7.1+ does
$typeHint = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type;

// 'self' needs to be resolved to the name of the declaring class and
// 'static' is a special type reserved as a return type in PHP 8
return ($type->isBuiltin() || $typeHint === 'static') ? $typeHint : sprintf('\\%s', $typeHint === 'self' ? $declaringClass : $typeHint);
// builtins and 'static' can be returned as is
if (($type->isBuiltin() || $typeHint === 'static')) {
return $typeHint;
}

// 'self' needs to be resolved to the name of the declaring class
if ($typeHint === 'self') {
$typeHint = $declaringClass->getName();
}

// 'parent' needs to be resolved to the name of the parent class
if ($typeHint === 'parent') {
$typeHint = $declaringClass->getParentClass()->getName();
}

// class names need prefixing with a slash
return sprintf('\\%s', $typeHint);
}

/**
* Format the given type as a nullable type.
*
* This method MUST only be called on PHP 7.1+.
*
* @param string $typeHint
*
* @return string
*/
private static function formatNullableType($typeHint)
{
if (\PHP_VERSION_ID < 80000) {
return sprintf('?%s', $typeHint);
}

return $typeHint === 'mixed' ? 'mixed' : sprintf('%s|null', $typeHint);
}
}
12 changes: 9 additions & 3 deletions phpunit.xml.dist
Expand Up @@ -4,20 +4,26 @@
verbose="true"
>
<testsuites>
<testsuite name="Mockery Test Suite">
<testsuite name="Mockery Test Suite PHP8">
<directory suffix="Test.php">./tests</directory>
<directory phpVersion="8.0.0" phpVersionOperator=">=">./tests/PHP80</directory>
</testsuite>

<testsuite name="Mockery Test Suite PHP7">
<directory suffix="Test.php">./tests</directory>
<directory phpVersion="7.0.0" phpVersionOperator=">=">./tests/PHP70</directory>
<directory phpVersion="7.1.0" phpVersionOperator=">=">./tests/PHP71</directory>
<directory phpVersion="7.2.0" phpVersionOperator=">=">./tests/PHP72</directory>
<exclude>./tests/PHP80</exclude>
</testsuite>

<testsuite name="Mockery Test Suite PHP56">
<testsuite name="Mockery Test Suite PHP5">
<directory suffix="Test.php">./tests</directory>
<directory suffix="Test.php">./tests/PHP56</directory>

<exclude>./tests/PHP70</exclude>
<exclude>./tests/PHP71</exclude>
<exclude>./tests/PHP72</exclude>
<exclude>./tests/PHP80</exclude>
</testsuite>
</testsuites>

Expand Down

0 comments on commit d55b877

Please sign in to comment.