Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.3] Improved PHP 8.0 support #1088

Merged
merged 2 commits into from Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions .styleci.yml
@@ -1,7 +1,13 @@
preset: psr12

enabled:
- symfony_braces

disabled:
- const_visibility_required
- psr12_braces

finder:
not-name:
- MethodWithHHVMReturnType.php
- MockingParameterAndReturnTypesTest.php
- 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
Expand Up @@ -154,9 +154,7 @@ private function getOriginalParameters($code, Method $method)
}

$groupMatches = end($parameterMatches);
$parameterNames = is_array($groupMatches) ?
$groupMatches :
array($groupMatches);
$parameterNames = is_array($groupMatches) ? $groupMatches : [$groupMatches];

return $parameterNames;
}
Expand Down
1 change: 0 additions & 1 deletion library/Mockery/Matcher/MultiArgumentClosure.php
Expand Up @@ -22,7 +22,6 @@

class MultiArgumentClosure extends MatcherAbstract implements ArgumentListMatcher
{

/**
* Check if the actual value matches the expected.
* Actual passed by reference to preserve reference trail (where applicable)
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
2 changes: 1 addition & 1 deletion tests/Mockery/Fixtures/SemiReservedWordsAsMethods.php
Expand Up @@ -2,7 +2,7 @@

namespace Mockery\Fixtures;

class SemiReservedWordsAsMethods
class SemiReservedWordsAsMethods
{
function callable() {}
function class() {}
Expand Down