GitHub - coduo/php-matcher: The easiest way to match data structures like JSON/PlainText/XML against readable patterns. Sandbox: (original) (raw)

PHP Matcher

Type Coverage

Library created for testing all kinds of JSON/XML/TXT/Scalar values against patterns.

API:

PHPMatcher::match($value = '{"foo": "bar"}', $pattern = '{"foo": "@string@"}') : bool; PHPMatcher::backtrace() : Backtrace; PHPMatcher::error() : ?string;

It was built to simplify API's functional testing.

Latest Stable Version Total Downloads Latest Unstable Version License

Sandbox

Feel free to play first with Sandbox

Installation

Require new dev dependency using composer:

composer require --dev "coduo/php-matcher"

Basic usage

Direct PHPMatcher usage

match=match = match=matcher->match("lorem ipsum dolor", "@string@"); if (!$match) { echo "Error: " . $matcher->error(); echo "Backtrace: \n"; echo (string) $matcher->backtrace(); } ### PHPUnit extending PHPMatcherTestCase [](#phpunit-extending-phpmatchertestcase) assertMatchesPattern('{"name": "@string@"}', '{"name": "Norbert"}'); } } ### PHPUnit using PHPMatcherAssertions trait [](#phpunit-using-phpmatcherassertions-trait) assertMatchesPattern('{"name": "@string@"}', '{"name": "Norbert"}'); } } ### Available patterns [](#available-patterns) * `@string@` * `@integer@` * `@number@` * `@double@` * `@boolean@` * `@time@` * `@date@` * `@datetime@` * `@timezone@` || `@tz` * `@array@` * `@array_previous@` \- match next array element using pattern from previous element * `@array_previous_repeat@` \- match all remaining array elements using pattern from previous element * `@...@` \- _unbounded array_, once used matcher will skip any further array elements * `@null@` * `@*@` || `@wildcard@` * `expr(expression)` \- **optional**, requires `symfony/expression-language: ^2.3|^3.0|^4.0|^5.0` to be present * `@uuid@` * `@ulid@` * `@json@` * `@string@||@integer@` \- string OR integer ### Available pattern expanders [](#available-pattern-expanders) * `startsWith($stringBeginning, $ignoreCase = false)` * `endsWith($stringEnding, $ignoreCase = false)` * `contains($string, $ignoreCase = false)` * `notContains($string, $ignoreCase = false)` * `isDateTime()` * `isInDateFormat($format)` \- example `"@datetime@.isInDateFormat('Y-m-d H:i:s')` * `before(string $date)` \- example `"@string@.isDateTime().before(\"2020-01-01 00:00:00\")"` * `after(string $date)` \- example `"@string@.isDateTime().after(\"2020-01-01 00:00:00\")"` * `isTzOffset()` * `isTzIdentifier()` * `isTzAbbreviation()` * `isEmail()` * `isUrl()` * `isIp()` * `isEmpty()` * `isNotEmpty()` * `lowerThan($boundry)` * `greaterThan($boundry)` * `inArray($value)` \- example `"@array@.inArray(\"ROLE_USER\")"` * `hasProperty($propertyName)` \- example `"@json@.hasProperty(\"property_name\")"` * `oneOf(...$expanders)` \- example `"@string@.oneOf(contains('foo'), contains('bar'), contains('baz'))"` * `matchRegex($regex)` \- example `"@string@.matchRegex('/^lorem.+/')"` * `optional()` \- work's only with `ArrayMatcher`, `JsonMatcher` and `XmlMatcher` * `count()` \- work's only with `ArrayMatcher` \- example `"@array@.count(5)"` * `repeat($pattern, $isStrict = true)` \- example `'@array@.repeat({"name": "foe"})'` or `"@array@.repeat('@string@')"` * `match($pattern)` \- example `{"image":"@json@.match({\"url\":\"@string@.isUrl()\"})"}` ## Example usage [](#example-usage) ### Scalar matching [](#scalar-matching) match(1, 1); $matcher->match('string', 'string'); ### String matching [](#string-matching) match('Norbert', '@string@'); $matcher->match("lorem ipsum dolor", "@string@.startsWith('lorem').contains('ipsum').endsWith('dolor')"); ### Time matching [](#time-matching) match('00:00:00', '@time@'); $matcher->match('00:01:00.000000', '@time@'); $matcher->match('00:01:00', '@time@.after("00:00:00")'); $matcher->match('00:00:00', '@time@.before("01:00:00")'); ### Date matching [](#date-matching) match('2014-08-19', '@date@'); $matcher->match('2020-01-11', '@date@'); $matcher->match('2014-08-19', '@date@.before("2016-08-19")'); $matcher->match('2014-08-19', '@date@.before("today").after("+ 100year")'); ### DateTime matching [](#datetime-matching) match('2014-08-19', '@datetime@'); $matcher->match('2020-01-11 00:00:00', '@datetime@'); $matcher->match('2014-08-19', '@datetime@.before("2016-08-19")'); $matcher->match('2014-08-19', '@datetime@.before("today").after("+ 100year")'); ### TimeZone matching [](#timezone-matching) match('Europe/Warsaw', '@timezone@'); $matcher->match('Europe/Warsaw', '@tz@'); $matcher->match('GMT', '@tz@'); $matcher->match('01:00', '@tz@'); $matcher->match('01:00', '@tz@.isTzOffset()'); $matcher->match('GMT', '@tz@.isTzAbbreviation()'); $matcher->match('Europe/Warsaw', '@tz@.isTzIdentifier()'); ### Integer matching [](#integer-matching) match(100, '@integer@'); $matcher->match(100, '@integer@.lowerThan(200).greaterThan(10)'); ### Number matching [](#number-matching) match(100, '@number@'); $matcher->match('200', '@number@'); $matcher->match(1.25, '@number@'); $matcher->match('1.25', '@number@'); $matcher->match(0b10100111001, '@number@'); ### Double matching [](#double-matching) match(10.1, "@double@"); $matcher->match(10.1, "@double@.lowerThan(50.12).greaterThan(10)"); ### Boolean matching [](#boolean-matching) match(true, "@boolean@"); $matcher->match(false, "@boolean@"); ### Wildcard matching [](#wildcard-matching) match("@integer@", "@*@"); $matcher->match("foobar", "@*@"); $matcher->match(true, "@*@"); $matcher->match(6.66, "@*@"); $matcher->match(array("bar"), "@wildcard@"); $matcher->match(new \stdClass, "@wildcard@"); ### Expression matching [](#expression-matching) match(new \DateTime('2014-04-01'), "expr(value.format('Y-m-d') == '2014-04-01'"); $matcher->match("Norbert", "expr(value === 'Norbert')"); ### UUID matching [](#uuid-matching) match('9f4db639-0e87-4367-9beb-d64e3f42ae18', '@uuid@'); ### ULID matching [](#ulid-matching) match('01BX5ZZKBKACTAV9WEVGEMMVS0', '@ulid@'); ### Array matching [](#array-matching) match( array( 'users' => array( array( 'id' => 1, 'firstName' => 'Norbert', 'lastName' => 'Orzechowicz', 'roles' => array('ROLE_USER'), 'position' => 'Developer', ), array( 'id' => 2, 'firstName' => 'Michał', 'lastName' => 'Dąbrowski', 'roles' => array('ROLE_USER') ), array( 'id' => 3, 'firstName' => 'Johnny', 'lastName' => 'DąbrowsBravoki', 'roles' => array('ROLE_HANDSOME_GUY') ) ), true, 6.66 ), array( 'users' => array( array( 'id' => '@integer@.greaterThan(0)', 'firstName' => '@string@', 'lastName' => 'Orzechowicz', 'roles' => '@array@', 'position' => '@string@.optional()' ), array( 'id' => '@integer@', 'firstName' => '@string@', 'lastName' => 'Dąbrowski', 'roles' => '@array@' ), '@...@' ), '@boolean@', '@double@' ) ); ### Array Previous [](#array-previous) > @array\_previous@ can also be used when matching JSON's and XML's match( array( 'users' => array( array( 'id' => 1, 'firstName' => 'Norbert', 'lastName' => 'Orzechowicz', 'roles' => array('ROLE_USER'), 'position' => 'Developer', ), array( 'id' => 2, 'firstName' => 'Michał', 'lastName' => 'Dąbrowski', 'roles' => array('ROLE_USER') ), array( 'id' => 3, 'firstName' => 'Johnny', 'lastName' => 'DąbrowsBravoki', 'roles' => array('ROLE_HANDSOME_GUY') ) ), true, 6.66 ), array( 'users' => array( array( 'id' => '@integer@.greaterThan(0)', 'firstName' => '@string@', 'lastName' => 'Orzechowicz', 'roles' => '@array@', 'position' => '@string@.optional()' ), '@array_previous@', '@array_previous@' ), '@boolean@', '@double@' ) ); ### Array Previous Repeat [](#array-previous-repeat) > @array\_previous\_repeat@ can also be used when matching JSON's and XML's match( array( 'users' => array( array( 'id' => 1, 'firstName' => 'Norbert', 'lastName' => 'Orzechowicz', 'roles' => array('ROLE_USER'), 'position' => 'Developer', ), array( 'id' => 2, 'firstName' => 'Michał', 'lastName' => 'Dąbrowski', 'roles' => array('ROLE_USER') ), array( 'id' => 3, 'firstName' => 'Johnny', 'lastName' => 'DąbrowsBravoki', 'roles' => array('ROLE_HANDSOME_GUY') ) ), true, 6.66 ), array( 'users' => array( array( 'id' => '@integer@.greaterThan(0)', 'firstName' => '@string@', 'lastName' => 'Orzechowicz', 'roles' => '@array@', 'position' => '@string@.optional()' ), '@array_previous_repeat@' ), '@boolean@', '@double@' ) ); ### Json matching [](#json-matching) match( '{ "users":[ { "firstName": "Norbert", "lastName": "Orzechowicz", "created": "2014-01-01", "roles":["ROLE_USER", "ROLE_DEVELOPER"] } ] }', '{ "users":[ { "firstName": "@string@", "lastName": "@string@", "created": "@string@.isDateTime()", "roles": "@array@", "position": "@string@.optional()" } ] }' ); ### Json matching with unbounded arrays and objects [](#json-matching-with-unbounded-arrays-and-objects) match( '{ "users":[ { "firstName": "Norbert", "lastName": "Orzechowicz", "created": "2014-01-01", "roles":["ROLE_USER", "ROLE_DEVELOPER"], "attributes": { "isAdmin": false, "dateOfBirth": null, "hasEmailVerified": true }, "avatar": { "url": "http://avatar-image.com/avatar.png" } }, { "firstName": "Michał", "lastName": "Dąbrowski", "created": "2014-01-01", "roles":["ROLE_USER", "ROLE_DEVELOPER", "ROLE_ADMIN"], "attributes": { "isAdmin": true, "dateOfBirth": null, "hasEmailVerified": true }, "avatar": null } ] }', '{ "users":[ { "firstName": "@string@", "lastName": "@string@", "created": "@string@.isDateTime()", "roles": [ "ROLE_USER", "@...@" ], "attributes": { "isAdmin": @boolean@, "@*@": "@*@" }, "avatar": "@json@.match({\"url\":\"@string@.isUrl()\"})" } , @...@ ] }' ); ### Xml matching [](#xml-matching) **Optional** \- requires `openlss/lib-array2xml: ^1.0` to be present. match(<<

<soap:Envelope xmlns:soap="http://www.w3.org/2001/12/soap-envelope" soap:encodingStyle="" title="undefined" rel="noopener noreferrer">http://www.w3.org/2001/12/soap-encoding">

<soap:Body xmlns:m="" title="undefined" rel="noopener noreferrer">http://www.example.org/stock"> <m:GetStockPrice> <m:StockName>IBM <m:StockValue>Any Value

XML , <<<XML <soap:Envelope xmlns:soap="@string@" soap:encodingStyle="@string@">

<soap:Body xmlns:m="@string@"> <m:GetStockPrice> <m:StockName>@string@ <m:StockValue>@string@ <m:StockQty>@integer@.optional()

XML );

Example scenario for api in behat using mongo.

@profile, @user Feature: Listing user toys

As a user I want to list my toys

Background: Given I send and accept JSON

Scenario: Listing toys Given the following users exist: | firstName | lastName | | Chuck | Norris |

And the following toys user "Chuck Norris" exist:
  | name            |
  | Barbie          |
  | GI Joe          |
  | Optimus Prime   |

When I set valid authorization code oauth header for user "Chuck Norris"
And I send a GET request on "/api/toys"
Then the response status code should be 200
And the JSON response should match:
"""
  [
    {
      "id": "@string@",
      "name": "Barbie",
      "_links: "@*@"
    },
    {
      "id": "@string@",
      "name": "GI Joe",
      "_links": "@*@"
    },
    {
      "id": "@string@",
      "name": "Optimus Prime",
      "_links": "@*@"
    }
  ]
"""

PHPUnit integration

The assertMatchesPattern() is a handy assertion that matches values in PHPUnit tests. To use it either include the Coduo\PHPMatcher\PHPUnit\PHPMatcherAssertions trait, or extend the Coduo\PHPMatcher\PHPUnit\PHPMatcherTestCase:

namespace Coduo\PHPMatcher\Tests\PHPUnit;

use Coduo\PHPMatcher\PHPUnit\PHPMatcherAssertions; use PHPUnit\Framework\TestCase;

class PHPMatcherAssertionsTest extends TestCase { use PHPMatcherAssertions;

public function test_it_asserts_if_a_value_matches_the_pattern()
{
    $this->assertMatchesPattern('@string@', 'foo');
}

}

The matchesPattern() method can be used in PHPUnit stubs or mocks: mock=mock = mock=this->createMock(Foo::class); $mock->method('bar') ->with($this->matchesPattern('@string@')) ->willReturn('foo');

License

This library is distributed under the MIT license. Please see the LICENSE file.

Credits

This lib was inspired by JSON Expressions gem &&Behat RestExtension