Counterpart: Object Matching for PHP¶
Counterpart is a matching framework for PHP and is used to compare values in an object oriented way.
Some example use cases:
- A testing framework could use Counterpart’s matchers and Assert class for assertions.
- A mock object library could use Counterpart to match argument expectations.
- A validation library could use Counterpart to check that values match expectations.
Contents:
Matchers¶
This document is a brief overview of all the matchers that counter part provides. For a more in depth look at the matchers, head over to the api documentation.
For information about custom matchers see Custom Matchers.
Built in Matchers¶
- Anything: Match literally anything.
- Callback: Run the actual value through a user defined callback. If the callback returns a truthy value it’s a match.
- Contains: Check if an array of Traversable contains a value
- Count: Check if an array, Traversable, or Countable matches an expected count.
- FileExists: Check if a file exists.
- GreaterThan: Check if a value is greater than an expected number.
- HasKey: Check if an array or ArrayAccess contains a given key.
- HasProperty: Check if an object has a given property. This can be configured to only match public properties.
- IsEmpty: Check if a value is empty (eg. empty($actual)).
- IsEqual: Check if two values are equal – can be configured to use strict equality.
- IsFalse: Check if an actual value is exactly equal to false.
- IsFalsy: Check if an actual value is falsy. These are things like "no", 0, or "0".
- IsInstanceOf: Check of an actual value is an instance of a given class or interfact.
- IsJson: Check if an actual value is a valid JSON <http://www.json.org/> string.
- IsNull: Check if an actual value is exactly equal to null.
- IsTrue: Check if an actual value is exactly equal to true.
- IsTruthy: Check if an actual value is truthy. These are things like "yes", 1, or "1".
- IsType: Check if an object is an internal type.
- LessThan: Check if an actual value is less than a given number.
- LogicalAnd: Combine one or more matchers with a conjuction. See Logical Combinations. Will match if all sub-matchers match.
- LogicalNot: Negate a matcher. See Logical Combinations.
- LogicalOr: Combine one or more matchers with a disjunction. See Logical Combinations. Will match if an only if one of its sub-matchers matches.
- LogicalXor: Combine one or more matchers with an XOR. LogicalXor will match if an only if exactly one of its sub-matchers matches.
- MatchesRegex: Checks a string (or object with a __toString method) against a regular expression.
- PhptFormat: Checks a string against a phpt format <http://qa.php.net/phpt_details.php#expectf_section>.
- StringContains: Checks to see if a string contains an expected value.
Logical Combinations¶
Counterpart provides a set of matchers that allow users to create logical combinations of one or more matchers.
Logical Not (Negating Matchers)¶
A matcher can be negated with LogicalNot.
<?php
use Counterpart\Matchers;
$matcher = Matchers::logicNot(Matchers::hasKey('a_key'));
$matcher->matches(['a_key' => '']); // false
echo $matcher; // "is an array or ArrayAccess without the key a_key"
There are a fair amount negative matcher factories already set up. The above could be more simply written.
<?php
use Counterpart\Matchers;
$matcher = Matchers::doesNotHaveKey('a_key'));
$matcher->matches(['a_key' => '']); // false
echo $matcher; // "is an array or ArrayAccess without the key a_key"
Logical And¶
LogicalAnd can be used to combine one or more matchers with an AND or conjuction. When all sub-matchers match a value, LogicalAnd will return true. Checking to see if a value is in a range is a great example of this.
<?php
use Counterpart\Matchers;
$matcher = Matchers::logicalAnd(
Matchers::greaterThan(10),
Matchers::lessThan(100)
);
$matcher->matches(11); // true
$matchers->matches(101); // false
Logical Or¶
LogicalOr can be used to combine one or more matchers with an OR or disjunction. If at least one sub-matcher matches the value, LogicalOr will also match. Checking that a value is greater than or equal to another is a great example of this.
<?php
use Counterpart\Matchers;
// same as Matchers::greaterThanOrEqual(10);
$matcher = Matchers::logicalOr(
Matchers::equalTo(10),
Matchers::greaterThan(10)
);
$matcher->matches(10); // true
$matcher->matches(20); // true
$matcher->matches(9); // false
Logical Xor¶
LogicalXor can be used to combine one or more matchers with an XOR. LogicalXor will return true if one and only one of the sub-matchers matches. The above greater than or equal to example could be written using logicalXor.
<?php
use Counterpart\Matchers;
$matcher = Matchers::logicalXor(
Matchers::equalTo(10),
Matchers::greaterThan(10)
);
$matcher->matches(10); // true
$matcher->matches(20); // true
$matcher->matches(9); // false
Custom Matchers¶
One of the goals of Counterpart is to make it easy to create custom matchers. The Counterpart\Matcher interface only contains two methods: matches and __toString.
Let’s make a custom matcher that checks to see if a value is in a range.
<?php
namespace Acme\CounterpartExample;
use Counterpart\Matcher;
/**
* Matches a value if its between $min and $max
*/
class RangeMatcher implements Matcher
{
private $min;
private $max;
public function __construct($min, $max)
{
$this->min = $min;
$this->max = $max;
}
/**
* Matches checks an actual value against the expectations.
*/
public function matches($actual)
{
return $actual > $this->min && $actual < $this->max;
}
/**
* This should return a textual description of the what the matcher
* is trying to accomplish.
*/
public function __toString()
{
return sprintf('is a value between %d and %d', $this->min, $this->max);
}
}
Better Negation Messages¶
By default Counterpart’s LogicalNot will replace the starting is in a matchers description with is not. That’s not always so great for generating a negation error message.
For a more customized negative message, a matcher can implement Counterpart\Negative.
<?php
namespace Acme\CounterpartExample;
use Counterpart\Matcher;
use Counterpart\Negative;
/**
* Matches a value if its between $min and $max
*/
class RangeMatcher implements Matcher, Negative
{
// all the stuff above
/**
* `LogicalNot` will call this this to generate a nice negative message.
*/
public function negativeMessage()
{
// this is what Counterpart would have done anyway
return sprintf('is not a value between %d and %d', $this->min, $this->max);
}
}
Mismatch Descriptions¶
When Counterpart does assertions it will call a matchers __toString method as part of the error description. Sometimes this isn’t enough – sometimes it doesn’t provide enough context for the user or developer to take action.
Custom matchers may implement Counterpart\Describer to generate a more thorough description of a mismatch.
<?php
namespace Acme\CounterpartExample;
use Counterpart\Matcher;
use Counterpart\Negative;
use Counterpart\Describer;
/**
* Matches a value if its between $min and $max
*/
class RangeMatcher implements Matcher, Negative, Describer
{
// all the stuff above
/**
* `Counterpart\Assert::assertThat` will call this method to to generate
* a more thorough error description.
*/
public function describeMismatch($actual)
{
if ($actual < $this->min) {
return 'the value was below the minimum';
}
if ($actual > $this->max) {
return 'the value was above the maximum';
}
// the method doesn't know what to do, so decline to do anything.
return Describer::DECLINE_DESCRIPTION;
}
}
Using Custom Matchers for Assertions¶
Simply pass an instance of the custom matcher as the first argument to Counterpart\Assert::assertThat.
<?php
use Counterpart\Assert;
use Acme\CounterpartExample\RangeMatcher;
$actualValue = 9;
Assert::assertThat(new RangeMatcher(1, 10), $actualValue);
Quickstart¶
Counterpart can be installed via composer, just add it it to your composer.json.
{
"name": "somevendor/someproject",
"require": {
"counterpart/counterpart": "~1.4"
}
}
Then simply composer install or composer update.
Counterpart provides two traits full of static factory and helper methods.
- Counterpart\Matchers
- Counterpart\Assert
Matchers¶
The Counterpart\Matchers trait comes with a set of static factory methods to make using matchers easy.
<?php
use Counterpart\Matchers;
$matcher = Matchers::hasKey('a_key');
$matcher->matches(['a_key' => '']); // true
echo $matcher; // "is an array or ArrayAccess with the key a_key"
Every matcher object implements Counterpart\Matcher whose match method does all the heavy lifting. The matcher interface also includes a __toString method which will return a textual description of what’s being looked for.
Logical Combinations¶
Counterpart provides a set of matchers that allow users to create logical combinations of one or more matchers.
See Logical Combinations for more.
Assertions¶
The Counterpart\Assert trait provides assertions: matchers wrapped up in a helper that throws a Counterpart\Exception\AssertionFailed exception when the matcher fails.
<?php
use Counterpart\Assert;
Assert::assertEquals(10, 10, "two values that are equal are not matching as equal, something is wrong");
Assert::assertFileExists(__FILE__);
It’s also passible to use a custom matcher with the Assert trait directly. Simple paces an instance of Counterpart\Matcher as the first argument to Assert::assertThat.
<?php
use Counterpart\Assert;
use Counterpart\Matcher\IsEqual;
Assert::assertThat(new IsEqual(1), 1, "1 != 1, something is very broken");