Skip to content

Commit

Permalink
Merge pull request #31 from 123inkt/Add_option_to_opt_out_of_parent_m…
Browse files Browse the repository at this point in the history
…ethod_checks

Added option to exclude class methods or all parent methods
  • Loading branch information
frankdekker authored Jul 18, 2021
2 parents d042975 + bc4583e commit 7ebae9a
Show file tree
Hide file tree
Showing 63 changed files with 827 additions and 115 deletions.
26 changes: 26 additions & 0 deletions src/AccessorPairAsserter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@

trait AccessorPairAsserter
{
/**
* Assert the constructor parameter - method pairs. Will use default configuration
* with ConstraintConfig::setAssertPropertyDefaults(true)
*
* @param string $object The fully qualified name of the class that should be tested
* @param string $message Custom PHPUnit error message in case of constraint failure
*/
public static function assertAccessorPropertyDefaults(string $object, string $message = ''): void
{
$config = (new ConstraintConfig())->setAssertPropertyDefaults(true);
Assert::assertThat($object, new AccessorPairConstraint($config), $message);
}

/**
* @param string $object The fully qualified name of the class that should be tested
* @param ConstraintConfig|null $config Configuration of the constraint.
Expand All @@ -23,4 +36,17 @@ public static function assertAccessorPairs(string $object, ConstraintConfig $con

Assert::assertThat($object, new AccessorPairConstraint($config), $message);
}

/**
* Only assert accessor pairs from the class itself and none of it's parents. Will use default configuration
* with ConstraintConfig::setAssertParentMethods(false)
*
* @param string $object The fully qualified name of the class that should be tested
* @param string $message Custom PHPUnit error message in case of constraint failure
*/
public static function assertOwnAccessorPairs(string $object, string $message = ''): void
{
$config = (new ConstraintConfig())->setAssertParentMethods(false);
Assert::assertThat($object, new AccessorPairConstraint($config), $message);
}
}
4 changes: 2 additions & 2 deletions src/Constraint/AccessorPairConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class AccessorPairConstraint extends Constraint

public function __construct(ConstraintConfig $config)
{
$this->accessorPairProvider = new AccessorPairProvider();
$this->constructorPairProvider = new ConstructorPairProvider();
$this->accessorPairProvider = new AccessorPairProvider($config);
$this->constructorPairProvider = new ConstructorPairProvider($config);
$this->valueProviderFactory = new ValueProviderFactory();
$this->config = $config;

Expand Down
41 changes: 41 additions & 0 deletions src/Constraint/ConstraintConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ class ConstraintConfig
/** @var bool */
private $assertPropertyDefaults = false;

/** @var bool */
private $assertParentMethods = true;

/** @var string[] */
private $excludedMethods = [];

/** @var null|callable(): mixed[] */
private $constructorCallback = null;

Expand Down Expand Up @@ -69,6 +75,41 @@ public function setAssertPropertyDefaults(bool $assertPropertyDefaults): self
return $this;
}

public function isAssertParentMethods(): bool
{
return $this->assertParentMethods;
}

/**
* Enabled by default.
* When disabled, only the direct class methods will be asserted and none of the parent's
* class methods.
*/
public function setAssertParentMethods(bool $assertParentMethods): self
{
$this->assertParentMethods = $assertParentMethods;

return $this;
}

/**
* @return string[]
*/
public function getExcludedMethods(): array
{
return $this->excludedMethods;
}

/**
* @param string[] $excludedMethods A list of exact method names that should be excluded from the assertions.
*/
public function setExcludedMethods(array $excludedMethods): self
{
$this->excludedMethods = $excludedMethods;

return $this;
}

public function getConstructorCallback(): ?callable
{
return $this->constructorCallback;
Expand Down
55 changes: 40 additions & 15 deletions src/Constraint/MethodPair/AccessorPair/AccessorPairProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

namespace DigitalRevolution\AccessorPairConstraint\Constraint\MethodPair\AccessorPair;

use DigitalRevolution\AccessorPairConstraint\Constraint\ConstraintConfig;
use DigitalRevolution\AccessorPairConstraint\Constraint\MethodPair\ClassMethodProvider;
use DigitalRevolution\AccessorPairConstraint\Constraint\Typehint\TypehintResolver;
use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\InflectorFactory;
use LogicException;
use phpDocumentor\Reflection\Types\Array_;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;

class AccessorPairProvider
Expand All @@ -20,8 +21,12 @@ class AccessorPairProvider
/** @var Inflector */
private $inflector;

public function __construct()
/** @var ConstraintConfig */
private $config;

public function __construct(ConstraintConfig $config)
{
$this->config = $config;
$this->inflector = InflectorFactory::create()->build();
}

Expand All @@ -30,13 +35,12 @@ public function __construct()
* Loops over the public methods, and for each "getter" it tries to find the corresponding "set" and/or "add" method
*
* @return AccessorPair[]
* @throws ReflectionException
* @throws LogicException
*/
public function getAccessorPairs(ReflectionClass $class): array
{
$pairs = [];
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
foreach ((new ClassMethodProvider($this->config))->getMethods($class) as $method) {
// Check multiple "getter" prefixes, add each getter method with corresponding setter to the inspectionMethod list
$methodName = $method->getName();
foreach (static::GET_PREFIXES as $getterPrefix) {
Expand All @@ -47,17 +51,7 @@ public function getAccessorPairs(ReflectionClass $class): array
// Try and find the corresponding set/add method
$baseMethodNames = $this->getMethodBaseNames($methodName, $getterPrefix);
foreach ($baseMethodNames as $baseMethodName) {
foreach (static::SET_PREFIXES as $setterPrefix) {
$setterName = $setterPrefix . $baseMethodName;
if ($class->hasMethod($setterName) === false) {
continue;
}

$setterMethod = $class->getMethod($setterName);
if ($setterMethod->isPublic() === false) {
continue;
}

foreach ($this->getSetters($class, $baseMethodName) as $setterMethod) {
$accessorPair = new AccessorPair($class, $method, $setterMethod);
if ($this->validateAccessorPair($accessorPair)) {
$pairs[] = $accessorPair;
Expand Down Expand Up @@ -85,6 +79,37 @@ protected function getMethodBaseNames(string $methodName, string $getterPrefix):
return $baseMethodNames;
}

/**
* @return ReflectionMethod[]
*/
protected function getSetters(ReflectionClass $class, string $baseMethodName): array
{
$setters = [];
foreach (static::SET_PREFIXES as $setterPrefix) {
$setterName = $setterPrefix . $baseMethodName;
if ($class->hasMethod($setterName) === false) {
continue;
}

$setterMethod = $class->getMethod($setterName);
if ($setterMethod->isPublic() === false) {
continue;
}

if (in_array($setterMethod->getName(), $this->config->getExcludedMethods(), true)) {
continue;
}

if ($this->config->isAssertParentMethods() === false && $class->getName() !== $setterMethod->getDeclaringClass()->getName()) {
continue;
}

$setters[] = $setterMethod;
}

return $setters;
}

/**
* @throws LogicException
*/
Expand Down
45 changes: 45 additions & 0 deletions src/Constraint/MethodPair/ClassMethodProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);

namespace DigitalRevolution\AccessorPairConstraint\Constraint\MethodPair;

use DigitalRevolution\AccessorPairConstraint\Constraint\ConstraintConfig;
use ReflectionClass;
use ReflectionMethod;

class ClassMethodProvider
{
/** @var ConstraintConfig */
private $config;

public function __construct(ConstraintConfig $config)
{
$this->config = $config;
}

/**
* @return ReflectionMethod[]
*/
public function getMethods(ReflectionClass $class): array
{
$excludeParentMethods = $this->config->isAssertParentMethods() === false;
$excludedMethods = $this->config->getExcludedMethods();

$methods = [];
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
// exclude all methods that are not from the class' parent class.
if ($excludeParentMethods && $class->getName() !== $method->getDeclaringClass()->getName()) {
continue;
}

// method is specifically excluded
if (in_array($method->getName(), $excludedMethods, true)) {
continue;
}

$methods[] = $method;
}

return $methods;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

namespace DigitalRevolution\AccessorPairConstraint\Constraint\MethodPair\ConstructorPair;

use DigitalRevolution\AccessorPairConstraint\Constraint\ConstraintConfig;
use DigitalRevolution\AccessorPairConstraint\Constraint\MethodPair\ClassMethodProvider;
use DigitalRevolution\AccessorPairConstraint\Constraint\Typehint\TypehintResolver;
use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\InflectorFactory;
use LogicException;
use phpDocumentor\Reflection\Types\Array_;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;

class ConstructorPairProvider
Expand All @@ -19,8 +20,12 @@ class ConstructorPairProvider
/** @var Inflector */
private $inflector;

public function __construct()
/** @var ConstraintConfig */
private $config;

public function __construct(ConstraintConfig $config)
{
$this->config = $config;
$this->inflector = InflectorFactory::create()->build();
}

Expand All @@ -36,7 +41,7 @@ public function getConstructorPairs(ReflectionClass $class): array
}

$pairs = [];
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
foreach ((new ClassMethodProvider($this->config))->getMethods($class) as $method) {
// Check multiple "getter" prefixes, add each getter method with corresponding setter to the inspectionMethod list
$methodName = $method->getName();

Expand Down Expand Up @@ -75,6 +80,12 @@ protected function getParameters(ReflectionClass $class): array
return [];
}

// skip parent constructor
$excludeParentMethods = $this->config->isAssertParentMethods() === false;
if ($excludeParentMethods && $constructor->getDeclaringClass()->getName() !== $class->getName()) {
return [];
}

$parameters = [];
foreach ($constructor->getParameters() as $parameter) {
$parameters[strtolower($parameter->getName())] = $parameter;
Expand Down
8 changes: 8 additions & 0 deletions tests/Integration/AccessorPairAsserterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ public function testMatchesSuccessInitialState(object $class): void
static::assertAccessorPairs(get_class($class), (new ConstraintConfig())->setAssertPropertyDefaults(true));
}

/**
* @dataProvider successInitialStateDataProvider
*/
public function testMatchesSuccessInitialStateWithDefaultMethod(object $class): void
{
static::assertAccessorPropertyDefaults(get_class($class));
}

/**
* When turning off the propertyDefaultCheck, we can safely pass classes we know will fail the constraint
*
Expand Down
4 changes: 4 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public function getClassDataProvider(string $path, string $namespacePrefix): Gen
continue;
}

if (strpos($file->getFilename(), 'Abstract') === 0) {
continue;
}

require_once $file->getPathname();

$key = str_replace([$path, '/'], ['', '\\'], $file->getPath()) . '\\' . $file->getBasename('.php');
Expand Down
9 changes: 9 additions & 0 deletions tests/Unit/Constraint/ConstraintConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class ConstraintConfigTest extends TestCase
* @covers ::hasAssertConstructor
* @covers ::setAssertPropertyDefaults
* @covers ::hasPropertyDefaultCheck
* @covers ::setAssertParentMethods
* @covers ::isAssertParentMethods
* @covers ::setExcludedMethods
* @covers ::getExcludedMethods
* @covers ::getConstructorCallback
* @covers ::setConstructorCallback
*/
Expand All @@ -27,16 +31,21 @@ public function testConfig(): void
static::assertTrue($config->hasAccessorPairCheck());
static::assertTrue($config->hasAssertConstructor());
static::assertFalse($config->hasPropertyDefaultCheck());
static::assertTrue($config->isAssertParentMethods());
static::assertSame([], $config->getExcludedMethods());

$config = new ConstraintConfig();
static::assertFalse($config->setAssertAccessorPair(false)->hasAccessorPairCheck());
static::assertFalse($config->setAssertConstructor(false)->hasAssertConstructor());
static::assertFalse($config->setAssertPropertyDefaults(false)->hasPropertyDefaultCheck());
static::assertFalse($config->setAssertParentMethods(false)->isAssertParentMethods());
static::assertSame(['foobar'], $config->setExcludedMethods(['foobar'])->getExcludedMethods());

$config = new ConstraintConfig();
static::assertTrue($config->setAssertAccessorPair(true)->hasAccessorPairCheck());
static::assertTrue($config->setAssertConstructor(true)->hasAssertConstructor());
static::assertTrue($config->setAssertPropertyDefaults(true)->hasPropertyDefaultCheck());
static::assertTrue($config->setAssertParentMethods(true)->isAssertParentMethods());

$config = new ConstraintConfig();
$callback = static function (): array {
Expand Down
29 changes: 29 additions & 0 deletions tests/Unit/Constraint/MethodPair/AbstractDataClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);

namespace DigitalRevolution\AccessorPairConstraint\Tests\Unit\Constraint\MethodPair;

use DigitalRevolution\AccessorPairConstraint\Constraint\ConstraintConfig;

/**
* @suppressWarnings(PHPMD.NumberOfChildren)
*/
abstract class AbstractDataClass
{
/**
* Return the config used for the AccessorPairProvider test
*/
public function getConfig(): ConstraintConfig
{
return new ConstraintConfig();
}

/**
* Expect to return:
* - method name of getter
* - method name of setter
* - true if the method has multi setter
* @return array<int, array{0:string, 1:string, 2:bool}>
*/
abstract public function getExpectedPairs(): array;
}
Loading

0 comments on commit 7ebae9a

Please sign in to comment.