├── .nix
├── shell
│ ├── .gitignore
│ └── starship.toml.dist
├── php
│ └── lib
│ │ ├── xdebug.ini.dist
│ │ ├── .gitignore
│ │ ├── pcov.ini.dist
│ │ ├── php.ini.dist
│ │ └── blackfire.ini.dist
└── pkgs
│ └── php
│ └── package.nix
├── src
├── AST
│ ├── Node.php
│ ├── Type.php
│ ├── Expander.php
│ └── Pattern.php
├── Exception
│ ├── Exception.php
│ ├── InvalidArgumentException.php
│ ├── UnknownExpanderException.php
│ ├── InvalidExpanderTypeException.php
│ ├── UnknownExpanderClassException.php
│ ├── PatternException.php
│ └── UnknownTypeException.php
├── Factory.php
├── Matcher
│ ├── ArrayMatcher
│ │ ├── Difference.php
│ │ ├── StringDifference.php
│ │ ├── ValuePatternDifference.php
│ │ └── Diff.php
│ ├── Pattern
│ │ ├── Pattern.php
│ │ ├── Expander
│ │ │ ├── BacktraceBehavior.php
│ │ │ ├── Optional.php
│ │ │ ├── Before.php
│ │ │ ├── After.php
│ │ │ ├── IsEmpty.php
│ │ │ ├── IsNotEmpty.php
│ │ │ ├── IsIp.php
│ │ │ ├── IsUrl.php
│ │ │ ├── ExpanderMatch.php
│ │ │ ├── IsEmail.php
│ │ │ ├── Count.php
│ │ │ ├── InArray.php
│ │ │ ├── IsDateTime.php
│ │ │ ├── MatchRegex.php
│ │ │ ├── NotContains.php
│ │ │ ├── Contains.php
│ │ │ ├── OneOf.php
│ │ │ ├── IsInDateFormat.php
│ │ │ ├── IsTzOffset.php
│ │ │ ├── IsTzIdentifier.php
│ │ │ ├── IsTzAbbreviation.php
│ │ │ ├── LowerThan.php
│ │ │ ├── GreaterThan.php
│ │ │ ├── StartsWith.php
│ │ │ ├── EndsWith.php
│ │ │ ├── DateTimeComparisonTrait.php
│ │ │ ├── HasProperty.php
│ │ │ └── Repeat.php
│ │ ├── Assert
│ │ │ ├── Xml.php
│ │ │ └── Json.php
│ │ ├── PatternExpander.php
│ │ ├── RegexConverter.php
│ │ └── TypePattern.php
│ ├── Matcher.php
│ ├── ValueMatcher.php
│ ├── WildcardMatcher.php
│ ├── CallbackMatcher.php
│ ├── ScalarMatcher.php
│ ├── NullMatcher.php
│ ├── ExpressionMatcher.php
│ ├── BooleanMatcher.php
│ ├── NumberMatcher.php
│ ├── OrMatcher.php
│ ├── DoubleMatcher.php
│ ├── StringMatcher.php
│ ├── IntegerMatcher.php
│ ├── XmlMatcher.php
│ ├── JsonMatcher.php
│ ├── DateMatcher.php
│ ├── TimeMatcher.php
│ ├── UuidMatcher.php
│ ├── DateTimeMatcher.php
│ ├── JsonObjectMatcher.php
│ ├── TimeZoneMatcher.php
│ ├── ChainMatcher.php
│ ├── UlidMatcher.php
│ ├── TextMatcher.php
│ └── ArrayMatcher.php
├── PHPUnit
│ ├── PHPMatcherTestCase.php
│ ├── PHPMatcherAssertions.php
│ └── PHPMatcherConstraint.php
├── Value
│ └── SingleLineString.php
├── Backtrace.php
├── Matcher.php
├── Backtrace
│ ├── VoidBacktrace.php
│ └── InMemoryBacktrace.php
├── PHPMatcher.php
├── Factory
│ └── MatcherFactory.php
├── Lexer.php
├── Parser
│ └── ExpanderInitializer.php
└── Parser.php
├── infection.json
├── phpbench.json
├── phpunit.xml.dist
├── LICENCE
├── shell.nix
├── UPGRADE.md
├── CONTRIBUTING.md
├── composer.json
├── CODE_OF_CONDUCT.md
├── .php-cs-fixer.php
└── README.md
/.nix/shell/.gitignore:
--------------------------------------------------------------------------------
1 | starship.toml
--------------------------------------------------------------------------------
/.nix/php/lib/xdebug.ini.dist:
--------------------------------------------------------------------------------
1 | [xdebug]
2 | xdebug.mode=debug
--------------------------------------------------------------------------------
/.nix/php/lib/.gitignore:
--------------------------------------------------------------------------------
1 | php.ini
2 | blackfire.ini
3 | xdebug.ini
--------------------------------------------------------------------------------
/src/AST/Node.php:
--------------------------------------------------------------------------------
1 | type = $type;
14 | }
15 |
16 | public function __toString() : string
17 | {
18 | return $this->type;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Pattern.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Assert/Xml.php:
--------------------------------------------------------------------------------
1 | description = $description;
14 | }
15 |
16 | public function format() : string
17 | {
18 | return $this->description;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/infection.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeout": 1000,
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | },
8 | "logs": {
9 | "text": "var/logs/infection.log",
10 | "summary": "var/logs/infection_summary.log",
11 | "debug": "var/logs/infection_summary.log"
12 | },
13 | "minMsi": 60,
14 | "minCoveredMsi": 65,
15 | "mutators": {
16 | "@default": true
17 | },
18 | "testFramework": "phpunit",
19 | "phpUnit": {
20 | "customPath": "vendor\/bin\/phpunit"
21 | },
22 | "bootstrap": "./vendor/autoload.php"
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/UnknownTypeException.php:
--------------------------------------------------------------------------------
1 | type = '@' . $type . '@';
14 | parent::__construct(\sprintf('Type pattern "%s" is not supported.', $this->type), 0, null);
15 | }
16 |
17 | public function getType() : string
18 | {
19 | return $this->type;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/phpbench.json:
--------------------------------------------------------------------------------
1 | {
2 | "runner.bootstrap": "./vendor/autoload.php",
3 | "runner.path": "benchmark",
4 | "runner.retry_threshold": 5,
5 | "runner.progress": "dots",
6 | "report.generators": {
7 | "matcher": {
8 | "generator": "expression",
9 | "cols": {
10 | "benchmark": null,
11 | "subject": null,
12 | "revs": null,
13 | "its": null,
14 | "mem_peak": null,
15 | "mode": null,
16 | "rstdev": null
17 | }
18 | }
19 | },
20 | "storage.xml_storage_path": "./var/phpbench"
21 | }
--------------------------------------------------------------------------------
/src/PHPUnit/PHPMatcherTestCase.php:
--------------------------------------------------------------------------------
1 | assertThat($value, self::matchesPattern($pattern), $message);
14 | }
15 |
16 | protected static function matchesPattern($pattern) : PHPMatcherConstraint
17 | {
18 | return new PHPMatcherConstraint($pattern);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | ./src
11 |
12 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Matcher/Matcher.php:
--------------------------------------------------------------------------------
1 | error;
17 | }
18 |
19 | /**
20 | * @inheritdoc
21 | */
22 | public function match($value, $pattern) : bool
23 | {
24 | return $value === $pattern;
25 | }
26 |
27 | public function clearError() : void
28 | {
29 | $this->error = null;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Matcher/ValueMatcher.php:
--------------------------------------------------------------------------------
1 | value = $value;
18 | $this->pattern = $pattern;
19 | $this->path = $path;
20 | }
21 |
22 | public function format() : string
23 | {
24 | return "Value \"{$this->value}\" does not match pattern \"{$this->pattern}\" at path: \"{$this->path}\"";
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Value/SingleLineString.php:
--------------------------------------------------------------------------------
1 | raw = $raw;
16 | }
17 |
18 | public function __toString() : string
19 | {
20 | $normalized = $this->raw;
21 |
22 | if (Json::isValid($this->raw)) {
23 | $normalized = Json::reformat($this->raw);
24 | } elseif (Json::isValid(Json::transformPattern($this->raw))) {
25 | $normalized = Json::reformat(Json::transformPattern($this->raw));
26 | }
27 |
28 | return \str_replace(["\r\n", "\r", "\n"], ' ', $normalized);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Matcher/ArrayMatcher/Diff.php:
--------------------------------------------------------------------------------
1 | differences = $difference;
17 | }
18 |
19 | public function add(Difference $difference) : self
20 | {
21 | return new self(...\array_merge($this->differences, [$difference]));
22 | }
23 |
24 | /**
25 | * @return Difference[]
26 | */
27 | public function all() : array
28 | {
29 | return $this->differences;
30 | }
31 |
32 | public function count() : int
33 | {
34 | return \count($this->differences);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/Optional.php:
--------------------------------------------------------------------------------
1 | backtrace->expanderEntrance(self::NAME, $value);
26 | $this->backtrace->expanderSucceed(self::NAME, $value);
27 |
28 | return true;
29 | }
30 |
31 | public function getError() : ?string
32 | {
33 | return null;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/PHPUnit/PHPMatcherAssertions.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
18 | }
19 |
20 | protected function assertMatchesPattern($pattern, $value, string $message = '') : void
21 | {
22 | TestCase::assertThat($value, self::matchesPattern($pattern, $this->backtrace), $message);
23 | }
24 |
25 | protected static function matchesPattern($pattern, ?Backtrace $backtrace = null) : PHPMatcherConstraint
26 | {
27 | return new PHPMatcherConstraint($pattern, $backtrace);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Backtrace.php:
--------------------------------------------------------------------------------
1 | name = $name;
19 | $this->arguments = [];
20 | }
21 |
22 | public function getName() : string
23 | {
24 | return $this->name;
25 | }
26 |
27 | public function addArgument($argument) : void
28 | {
29 | $this->arguments[] = $argument;
30 | }
31 |
32 | public function hasArguments() : bool
33 | {
34 | return (bool) \count($this->arguments);
35 | }
36 |
37 | /**
38 | * @return mixed[]
39 | */
40 | public function getArguments() : array
41 | {
42 | return $this->arguments;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/AST/Pattern.php:
--------------------------------------------------------------------------------
1 | expanders = [];
19 | $this->type = $type;
20 | }
21 |
22 | public function getType() : Type
23 | {
24 | return $this->type;
25 | }
26 |
27 | public function hasExpanders() : bool
28 | {
29 | return (bool) \count($this->expanders);
30 | }
31 |
32 | /**
33 | * @return Expander[]
34 | */
35 | public function getExpanders() : array
36 | {
37 | return $this->expanders;
38 | }
39 |
40 | public function addExpander(Expander $expander) : void
41 | {
42 | $this->expanders[] = $expander;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Matcher/WildcardMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
21 | }
22 |
23 | public function match($value, $pattern) : bool
24 | {
25 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
26 |
27 | return true;
28 | }
29 |
30 | public function canMatch($pattern) : bool
31 | {
32 | $result = \is_string($pattern) && 0 !== \preg_match(self::MATCH_PATTERN, $pattern);
33 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
34 |
35 | return $result;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/Before.php:
--------------------------------------------------------------------------------
1 | isAfterOrEqualTo($this->boundary)) {
23 | $this->error = \sprintf('Value "%s" is after or equal to "%s".', $value, new StringConverter($this->boundary));
24 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
25 |
26 | return false;
27 | }
28 |
29 | $this->backtrace->expanderSucceed(self::NAME, $value);
30 |
31 | return true;
32 | }
33 |
34 | protected static function getName() : string
35 | {
36 | return self::NAME;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/After.php:
--------------------------------------------------------------------------------
1 | isBeforeOrEqualTo($this->boundary)) {
23 | $this->error = \sprintf('Value "%s" is before or equal to "%s".', new StringConverter($value), new StringConverter($this->boundary));
24 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
25 |
26 | return false;
27 | }
28 |
29 | $this->backtrace->expanderSucceed(self::NAME, $value);
30 |
31 | return true;
32 | }
33 |
34 | protected static function getName() : string
35 | {
36 | return self::NAME;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/RegexConverter.php:
--------------------------------------------------------------------------------
1 | getType()) {
16 | case 'string':
17 | case 'wildcard':
18 | case '*':
19 | return '(.+)';
20 | case 'number':
21 | return '(\\-?[0-9]*[\\.|\\,]?[0-9]*)';
22 | case 'integer':
23 | return '(\\-?[0-9]*)';
24 | case 'double':
25 | return '(\\-?[0-9]*[\\.|\\,][0-9]*)';
26 | case 'uuid':
27 | return '(' . UuidMatcher::UUID_PATTERN . ')';
28 | case 'ulid':
29 | return '(' . UlidMatcher::ULID_PATTERN . ')';
30 |
31 | default:
32 | throw new UnknownTypeException($typePattern->getType());
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014-2021 Michal Dabrowski, Norbert Orzechowicz
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsEmpty.php:
--------------------------------------------------------------------------------
1 | backtrace->expanderEntrance(self::NAME, $value);
29 |
30 | if (!empty($value)) {
31 | $this->error = \sprintf('Value %s is not empty.', new StringConverter($value));
32 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
33 |
34 | return false;
35 | }
36 |
37 | $this->backtrace->expanderSucceed(self::NAME, $value);
38 |
39 | return true;
40 | }
41 |
42 | public function getError() : ?string
43 | {
44 | return $this->error;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsNotEmpty.php:
--------------------------------------------------------------------------------
1 | backtrace->expanderEntrance(self::NAME, $value);
29 |
30 | if (false === $value || (empty($value) && '0' != $value)) {
31 | $this->error = \sprintf('Value %s is not blank.', new StringConverter($value));
32 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
33 |
34 | return false;
35 | }
36 |
37 | $this->backtrace->expanderSucceed(self::NAME, $value);
38 |
39 | return true;
40 | }
41 |
42 | public function getError() : ?string
43 | {
44 | return $this->error;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Matcher/CallbackMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
17 | }
18 |
19 | public function match($value, $pattern) : bool
20 | {
21 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
22 | $result = (bool) $pattern->__invoke($value);
23 |
24 | if ($result) {
25 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
26 | } else {
27 | $this->error = \sprintf('Callback matcher failed for value %s', new StringConverter($value));
28 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
29 | }
30 |
31 | return $result;
32 | }
33 |
34 | public function canMatch($pattern) : bool
35 | {
36 | $result = \is_object($pattern) && \is_callable($pattern);
37 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
38 |
39 | return $result;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Matcher.php:
--------------------------------------------------------------------------------
1 | valueMatcher = $valueMatcher;
18 | $this->backtrace = $backtrace;
19 | }
20 |
21 | public function match($value, $pattern) : bool
22 | {
23 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
24 |
25 | $result = $this->valueMatcher->match($value, $pattern);
26 |
27 | if ($result) {
28 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
29 | $this->valueMatcher->clearError();
30 | } else {
31 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->valueMatcher->getError());
32 | }
33 |
34 | return $result;
35 | }
36 |
37 | /**
38 | * @return null|string
39 | */
40 | public function getError() : ?string
41 | {
42 | return $this->valueMatcher->getError();
43 | }
44 |
45 | public function backtrace() : Backtrace
46 | {
47 | return $this->backtrace;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Backtrace/VoidBacktrace.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
18 | }
19 |
20 | public function match($value, $pattern) : bool
21 | {
22 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
23 |
24 | if ($value !== $pattern) {
25 | $this->error = \sprintf(
26 | '"%s" does not match "%s".',
27 | new SingleLineString((string) new StringConverter($value)),
28 | new SingleLineString((string) new StringConverter($pattern))
29 | );
30 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
31 |
32 | return false;
33 | }
34 |
35 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
36 |
37 | return true;
38 | }
39 |
40 | public function canMatch($pattern) : bool
41 | {
42 | $result = \is_scalar($pattern);
43 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
44 |
45 | return $result;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.nix/php/lib/blackfire.ini.dist:
--------------------------------------------------------------------------------
1 | [blackfire]
2 | ; On Windows use the following configuration:
3 | ; extension=php_blackfire.dll
4 |
5 | ; Sets fine-grained configuration for Probe.
6 | ; This should be left blank in most cases. For most installs,
7 | ; the server credentials should only be set in the agent.
8 | ;blackfire.server_id =
9 |
10 | ; Sets fine-grained configuration for Probe.
11 | ; This should be left blank in most cases. For most installs,
12 | ; the server credentials should only be set in the agent.
13 | ;blackfire.server_token =
14 |
15 | ; Log verbosity level:
16 | ; 4: debug
17 | ; 3: info
18 | ; 2: warning;
19 | ; 1: error
20 | ;blackfire.log_level = 1
21 |
22 | ; Log file (STDERR by default)
23 | ;blackfire.log_file = /tmp/blackfire.log
24 |
25 | ; Add the stacktrace to the probe logs when a segmentation fault occurs.
26 | ; Debug option inactive on Windows and Alpine.
27 | ;blackfire.debug.sigsegv_handler = 0
28 |
29 | ; Sets the socket where the agent is listening.
30 | ; Possible value can be a unix socket or a TCP address.
31 | ; Defaults values are:
32 | ; - Linux: unix:///var/run/blackfire/agent.sock
33 | ; - macOS amd64: unix:///usr/local/var/run/blackfire-agent.sock
34 | ; - macOS arm64 (M1): unix:///opt/homebrew/var/run/blackfire-agent.sock
35 | ; - Windows: tcp://127.0.0.1:8307
36 | ;blackfire.agent_socket = unix:///var/run/blackfire/agent.sock
37 |
38 | ; Enables Blackfire Monitoring
39 | ; Enabled by default since version 1.61.0
40 | ;blackfire.apm_enabled = 1
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | let
2 | nixpkgs = fetchTarball {
3 | # Oct 31, 2025
4 | url = "https://github.com/NixOS/nixpkgs/archive/66a437ebcf6160152336e801a7ec289ba2aba3c5.tar.gz";
5 | };
6 |
7 | lockedPkgs = import nixpkgs {
8 | config = {
9 | allowUnfree = true;
10 | };
11 | };
12 | in
13 | {
14 | pkgs ? lockedPkgs,
15 | php-version ? 8.3,
16 | with-blackfire ? false,
17 | with-xdebug ? false,
18 | with-pcov ? !with-blackfire
19 | }:
20 |
21 | let
22 | base-php = if php-version == 8.3 then
23 | pkgs.php83
24 | else if php-version == 8.4 then
25 | pkgs.php84
26 | else
27 | throw "Unknown php version ${php-version}";
28 |
29 | php = pkgs.callPackage ./.nix/pkgs/php/package.nix {
30 | php = base-php;
31 | inherit with-pcov with-xdebug with-blackfire;
32 | };
33 | in
34 | pkgs.mkShell {
35 | buildInputs = [
36 | php
37 | php.packages.composer
38 | pkgs.starship
39 | pkgs.figlet
40 | pkgs.act
41 | ]
42 | ++ pkgs.lib.optional with-blackfire pkgs.blackfire
43 | ;
44 |
45 | shellHook = ''
46 | if [ -f "$PWD/.nix/shell/starship.toml" ]; then
47 | export STARSHIP_CONFIG="$PWD/.nix/shell/starship.toml"
48 | else
49 | export STARSHIP_CONFIG="$PWD/.nix/shell/starship.toml.dist"
50 | fi
51 |
52 | eval "$(${pkgs.starship}/bin/starship init bash)"
53 |
54 | clear
55 | figlet "PHP Matcher"
56 | '';
57 | }
--------------------------------------------------------------------------------
/src/Matcher/NullMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
22 | }
23 |
24 | /**
25 | * {@inheritDoc}
26 | */
27 | public function match($value, $pattern) : bool
28 | {
29 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
30 |
31 | if (null !== $value) {
32 | $this->error = \sprintf('%s "%s" does not match null.', \gettype($value), new StringConverter($value));
33 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
34 |
35 | return false;
36 | }
37 |
38 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
39 |
40 | return true;
41 | }
42 |
43 | /**
44 | * {@inheritDoc}
45 | */
46 | public function canMatch($pattern) : bool
47 | {
48 | $result = null === $pattern || (\is_string($pattern) && 0 !== \preg_match(self::MATCH_PATTERN, $pattern));
49 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
50 |
51 | return $result;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | # 4.0 -> 5.0
2 |
3 | **Backtrace**
4 |
5 | In order to improve performance of matcher `Backtrace` class was replaced `InMemoryBacktrace`
6 | that implements `Backtrace` interface.
7 |
8 | In order to use backtrace provide it directly to `MatcherFactory` or `PHPMatcher` class.
9 |
10 | PHPUnit tests require `setBacktrace` method to be used before test:
11 |
12 | ```php
13 | $this->setBacktrace($backtrace = new InMemoryBacktrace());
14 | $this->assertMatchesPattern('{"foo": "@integer@"}', json_encode(['foo' => 'bar']));
15 | ```
16 |
17 | **Optional Matchers**
18 |
19 | XML and Expression matchers are now optional, in order to use them add following
20 | dependencies to your composer.json file:
21 |
22 | XMLMatcher
23 |
24 | ```
25 | "openlss/lib-array2xml": "^1.0"
26 | ```
27 |
28 | ExpressionMatcher
29 |
30 | ```
31 | symfony/expression-language
32 | ```
33 |
34 | # 3.x -> 4.0
35 |
36 | Below you can find list of changes between `3.x` and `4.0` versions of PHPMatcher.
37 |
38 | **Creating Matcher:**
39 | ```diff
40 | -$factory = new MatcherFactory();
41 | -$matcher = $factory->createMatcher();
42 | +$matcher = new PHPMatcher();
43 | ```
44 |
45 | **Using Matcher**
46 | ```diff
47 | -PHPMatcher::match($value, $pattern, $error)
48 | +$matcher = (new PHPMatcher())->match($value, $pattern);;
49 | ```
50 |
51 | **Accessing last error/backtrace**
52 | ```diff
53 | +$matcher = new PHPMatcher();
54 | +$matcher->match($value, $pattern);
55 | +echo $matcher->error();
56 | +echo $matcher->backtrace();
57 | ```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # A quick guide to contribute to the project:
2 |
3 | ## Installing the dev environment
4 |
5 | 1. Fork the repo
6 | 2. Clone the repo to local
7 | 3. Install dependencies: `composer update` (this assumes you have 'composer' aliased to wherever your composer.phar lives)
8 | 4. Run the tests. We only take pull requests with passing tests, and it's great to know that you have a clean slate:
9 | `composer test`
10 |
11 | ## Adding new features
12 |
13 | Pull requests with new features needs to be created against master branch.
14 |
15 | If new feature require BC Break please note that in your PR comment, it will added in next major version.
16 | New features that does not have any BC Breaks are going to be added in next minor version.
17 |
18 | ## Codding standards
19 |
20 | In order to fix codding standards please exeecute:
21 |
22 | ```
23 | composer cs:php:fix
24 | ```
25 |
26 | ## Patches and bugfixes
27 |
28 | 1. Check the oldest version that patch/bug fix can be applied.
29 | 2. Create PR against that version
30 |
31 | For example if you are fixing pattern expander that was introduced in version 1.1 make sure that PR with fix
32 | is created against version 1.1, not master or 2.0
33 |
34 | ## The actual contribution
35 |
36 | 1. Make the changes/additions to the code, committing often and making clear what you've done
37 | 2. Make sure you write tests for your code, located in the folder structure `tests/Coduo/PHPMatcher/...`
38 | 3. Run your tests (often and while coding): `./bin/phpunit`
39 | 4. Create Pull Request on github to against proper branch
40 |
--------------------------------------------------------------------------------
/src/PHPMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace !== null
19 | ? $backtrace
20 | : new VoidBacktrace();
21 | }
22 |
23 | public function match($value, $pattern) : bool
24 | {
25 | $this->matcher = null;
26 |
27 | return $this->getMatcher()->match($value, $pattern);
28 | }
29 |
30 | /**
31 | * Returns backtrace from last matching.
32 | * When called before PHPMatcher::match() function it will return instance where Backtrace::isEmpty() will return true.
33 | *
34 | * @return Backtrace
35 | */
36 | public function backtrace() : Backtrace
37 | {
38 | return $this->backtrace;
39 | }
40 |
41 | /**
42 | * Returns error from last matching.
43 | * If last matching was successful this function will return null.
44 | *
45 | * @return null|string
46 | */
47 | public function error() : ?string
48 | {
49 | return $this->getMatcher()->getError();
50 | }
51 |
52 | private function getMatcher() : Matcher
53 | {
54 | if (null === $this->matcher) {
55 | $this->matcher = (new MatcherFactory())->createMatcher($this->backtrace());
56 | }
57 |
58 | return $this->matcher;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/TypePattern.php:
--------------------------------------------------------------------------------
1 | type = $type;
21 | $this->expanders = [];
22 | }
23 |
24 | public function is(string $type) : bool
25 | {
26 | return \strtolower($this->type) === \strtolower($type);
27 | }
28 |
29 | public function getType() : string
30 | {
31 | return \strtolower($this->type);
32 | }
33 |
34 | public function addExpander(PatternExpander $expander) : void
35 | {
36 | $this->expanders[] = $expander;
37 | }
38 |
39 | public function matchExpanders($value) : bool
40 | {
41 | foreach ($this->expanders as $expander) {
42 | if (!$expander->match($value)) {
43 | $this->error = $expander->getError();
44 |
45 | return false;
46 | }
47 | }
48 |
49 | return true;
50 | }
51 |
52 | public function getError() : ?string
53 | {
54 | return $this->error;
55 | }
56 |
57 | public function hasExpander(string $expanderName) : bool
58 | {
59 | foreach ($this->expanders as $expander) {
60 | if ($expander::is($expanderName)) {
61 | return true;
62 | }
63 | }
64 |
65 | return false;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsIp.php:
--------------------------------------------------------------------------------
1 | backtrace->expanderEntrance(self::NAME, $value);
29 |
30 | if (!\is_string($value)) {
31 | $this->error = \sprintf('IsIp expander require "string", got "%s".', new StringConverter($value));
32 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
33 |
34 | return false;
35 | }
36 |
37 | if (!$this->matchValue($value)) {
38 | $this->error = \sprintf('string "%s" is not a valid IP address.', $value);
39 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
40 |
41 | return false;
42 | }
43 |
44 | $this->backtrace->expanderSucceed(self::NAME, $value);
45 |
46 | return true;
47 | }
48 |
49 | public function getError() : ?string
50 | {
51 | return $this->error;
52 | }
53 |
54 | private function matchValue(string $value) : bool
55 | {
56 | return false !== \filter_var($value, FILTER_VALIDATE_IP);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsUrl.php:
--------------------------------------------------------------------------------
1 | backtrace->expanderEntrance(self::NAME, $value);
29 |
30 | if (!\is_string($value)) {
31 | $this->error = \sprintf('IsUrl expander require "string", got "%s".', new StringConverter($value));
32 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
33 |
34 | return false;
35 | }
36 |
37 | if (!$this->matchValue($value)) {
38 | $this->error = \sprintf('string "%s" is not a valid URL.', $value);
39 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
40 |
41 | return false;
42 | }
43 |
44 | $this->backtrace->expanderSucceed(self::NAME, $value);
45 |
46 | return true;
47 | }
48 |
49 | public function getError() : ?string
50 | {
51 | return $this->error;
52 | }
53 |
54 | private function matchValue(string $value) : bool
55 | {
56 | return false !== \filter_var($value, FILTER_VALIDATE_URL);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Matcher/ExpressionMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
23 | }
24 |
25 | public function match($value, $pattern) : bool
26 | {
27 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
28 |
29 | $language = new ExpressionLanguage();
30 | \preg_match(self::MATCH_PATTERN, $pattern, $matches);
31 | $expressionResult = $language->evaluate($matches[1], ['value' => $value]);
32 |
33 | if (!$expressionResult) {
34 | $this->error = \sprintf('"%s" expression fails for value "%s".', $pattern, new StringConverter($value));
35 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
36 |
37 | return false;
38 | }
39 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
40 |
41 | return true;
42 | }
43 |
44 | public function canMatch($pattern) : bool
45 | {
46 | $result = \is_string($pattern) && 0 !== \preg_match(self::MATCH_PATTERN, $pattern);
47 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
48 |
49 | return $result;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/ExpanderMatch.php:
--------------------------------------------------------------------------------
1 | pattern = $pattern;
32 | }
33 |
34 | public static function is(string $name) : bool
35 | {
36 | return self::NAME === $name;
37 | }
38 |
39 | /**
40 | * @param mixed $value
41 | */
42 | public function match($value) : bool
43 | {
44 | $this->backtrace->expanderEntrance(self::NAME, $value);
45 |
46 | if ($this->matcher === null) {
47 | $this->matcher = (new MatcherFactory())->createMatcher($this->backtrace);
48 | }
49 |
50 | $result = $this->matcher->match($value, $this->pattern);
51 |
52 | if ($result) {
53 | $this->backtrace->expanderSucceed(self::NAME, $value);
54 | } else {
55 | $this->backtrace->expanderFailed(self::NAME, $value, '');
56 | }
57 |
58 | return $result;
59 | }
60 |
61 | public function getError() : ?string
62 | {
63 | return $this->matcher->getError();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsEmail.php:
--------------------------------------------------------------------------------
1 | backtrace->expanderEntrance(self::NAME, $value);
29 |
30 | if (!\is_string($value)) {
31 | $this->error = \sprintf('IsEmail expander require "string", got "%s".', new StringConverter($value));
32 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
33 |
34 | return false;
35 | }
36 |
37 | if (!$this->matchValue($value)) {
38 | $this->error = \sprintf('string "%s" is not a valid e-mail address.', $value);
39 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
40 |
41 | return false;
42 | }
43 |
44 | $this->backtrace->expanderSucceed(self::NAME, $value);
45 |
46 | return true;
47 | }
48 |
49 | public function getError() : ?string
50 | {
51 | return $this->error;
52 | }
53 |
54 | private function matchValue(string $value) : bool
55 | {
56 | return false !== \filter_var($value, FILTER_VALIDATE_EMAIL);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.nix/pkgs/php/package.nix:
--------------------------------------------------------------------------------
1 | {
2 | php,
3 | with-pcov ? true,
4 | with-xdebug ? false,
5 | with-blackfire ? false
6 | }:
7 |
8 | let
9 | flowPHP = php.withExtensions (
10 | { enabled, all }:
11 | with all;
12 | enabled
13 | ++ [
14 | simplexml
15 | ]
16 | ++ (if with-xdebug then [xdebug] else [])
17 | ++ (if with-pcov then [pcov] else [])
18 | ++ (if with-blackfire then [blackfire] else [])
19 | );
20 | in
21 | flowPHP.buildEnv {
22 | extraConfig = ""
23 | + (
24 | if builtins.pathExists ./../../php/lib/php.ini
25 | then builtins.readFile ./../../php/lib/php.ini
26 | else builtins.readFile ./../../php/lib/php.ini.dist
27 | )
28 | + "\n"
29 | + (
30 | if with-xdebug
31 | then
32 | if builtins.pathExists ./../../php/lib/xdebug.ini
33 | then builtins.readFile ./../../php/lib/xdebug.ini
34 | else builtins.readFile ./../../php/lib/xdebug.ini.dist
35 | else ""
36 | )
37 | + "\n"
38 | + (
39 | if with-blackfire
40 | then
41 | if builtins.pathExists ./../../php/lib/blackfire.ini
42 | then builtins.readFile ./../../php/lib/blackfire.ini
43 | else builtins.readFile ./../../php/lib/blackfire.ini.dist
44 | else ""
45 | )
46 | + "\n"
47 | + (
48 | if with-pcov
49 | then
50 | if builtins.pathExists ./../../php/lib/pcov.ini
51 | then builtins.readFile ./../../php/lib/pcov.ini
52 | else builtins.readFile ./../../php/lib/pcov.ini.dist
53 | else ""
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/Matcher/BooleanMatcher.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
25 | $this->backtrace = $backtrace;
26 | }
27 |
28 | public function match($value, $pattern) : bool
29 | {
30 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
31 |
32 | if (!\is_bool($value)) {
33 | $this->error = \sprintf('%s "%s" is not a valid boolean.', \gettype($value), new StringConverter($value));
34 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
35 |
36 | return false;
37 | }
38 |
39 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
40 |
41 | return true;
42 | }
43 |
44 | public function canMatch($pattern) : bool
45 | {
46 | if (!\is_string($pattern)) {
47 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
48 |
49 | return false;
50 | }
51 |
52 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
53 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
54 |
55 | return $result;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Matcher/NumberMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
25 | $this->parser = $parser;
26 | }
27 |
28 | public function match($value, $pattern) : bool
29 | {
30 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
31 |
32 | if (!\is_numeric($value)) {
33 | $this->error = \sprintf('%s "%s" is not a valid number.', \gettype($value), new StringConverter($value));
34 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
35 |
36 | return false;
37 | }
38 |
39 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
40 |
41 | return true;
42 | }
43 |
44 | public function canMatch($pattern) : bool
45 | {
46 | if (!\is_string($pattern)) {
47 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
48 |
49 | return false;
50 | }
51 |
52 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
53 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
54 |
55 | return $result;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/Count.php:
--------------------------------------------------------------------------------
1 | value = $value;
26 | }
27 |
28 | public static function is(string $name) : bool
29 | {
30 | return self::NAME === $name;
31 | }
32 |
33 | public function match($value) : bool
34 | {
35 | $this->backtrace->expanderEntrance(self::NAME, $value);
36 |
37 | if (!\is_array($value)) {
38 | $this->error = \sprintf('Count expander require "array", got "%s".', new StringConverter($value));
39 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
40 |
41 | return false;
42 | }
43 |
44 | if (\count($value) !== $this->value) {
45 | $this->error = \sprintf('Expected count of %s is %s.', new StringConverter($value), new StringConverter($this->value));
46 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
47 |
48 | return false;
49 | }
50 |
51 | $this->backtrace->expanderSucceed(self::NAME, $value);
52 |
53 | return true;
54 | }
55 |
56 | public function getError() : ?string
57 | {
58 | return $this->error;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/InArray.php:
--------------------------------------------------------------------------------
1 | value = $value;
26 | }
27 |
28 | public static function is(string $name) : bool
29 | {
30 | return self::NAME === $name;
31 | }
32 |
33 | public function match($value) : bool
34 | {
35 | $this->backtrace->expanderEntrance(self::NAME, $value);
36 |
37 | if (!\is_array($value)) {
38 | $this->error = \sprintf('InArray expander require "array", got "%s".', new StringConverter($value));
39 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
40 |
41 | return false;
42 | }
43 |
44 | if (!\in_array($this->value, $value, true)) {
45 | $this->error = \sprintf("%s doesn't have \"%s\" element.", new StringConverter($value), new StringConverter($this->value));
46 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
47 |
48 | return false;
49 | }
50 |
51 | $this->backtrace->expanderSucceed(self::NAME, $value);
52 |
53 | return true;
54 | }
55 |
56 | public function getError() : ?string
57 | {
58 | return $this->error;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsDateTime.php:
--------------------------------------------------------------------------------
1 | backtrace->expanderEntrance(self::NAME, $value);
29 |
30 | if (!\is_string($value)) {
31 | $this->error = \sprintf('IsDateTime expander require "string", got "%s".', new StringConverter($value));
32 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
33 |
34 | return false;
35 | }
36 |
37 | if (!$this->matchValue($value)) {
38 | $this->error = \sprintf('string "%s" is not a valid date.', $value);
39 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
40 |
41 | return false;
42 | }
43 |
44 | $this->backtrace->expanderSucceed(self::NAME, $value);
45 |
46 | return true;
47 | }
48 |
49 | public function getError() : ?string
50 | {
51 | return $this->error;
52 | }
53 |
54 | private function matchValue(string $value) : bool
55 | {
56 | try {
57 | new \DateTime($value);
58 |
59 | return true;
60 | } catch (\Exception $exception) {
61 | return false;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Matcher/OrMatcher.php:
--------------------------------------------------------------------------------
1 | chainMatcher = $chainMatcher;
23 | $this->backtrace = $backtrace;
24 | }
25 |
26 | public function match($value, $pattern) : bool
27 | {
28 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
29 |
30 | $patterns = \explode('||', $pattern);
31 | $patterns = \array_map('trim', $patterns);
32 |
33 | foreach ($patterns as $childPattern) {
34 | if ($this->matchChild($value, $childPattern)) {
35 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
36 |
37 | return true;
38 | }
39 | }
40 |
41 | $this->backtrace->matcherFailed(self::class, $value, $pattern, (string) $this->error);
42 |
43 | return false;
44 | }
45 |
46 | public function canMatch($pattern) : bool
47 | {
48 | $result = \is_string($pattern) && 0 !== \preg_match_all(self::MATCH_PATTERN, $pattern, $matches);
49 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
50 |
51 | return $result;
52 | }
53 |
54 | private function matchChild($value, $pattern) : bool
55 | {
56 | if (!$this->chainMatcher->canMatch($pattern)) {
57 | return false;
58 | }
59 |
60 | return $this->chainMatcher->match($value, $pattern);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/MatchRegex.php:
--------------------------------------------------------------------------------
1 | pattern = $pattern;
30 | }
31 |
32 | public static function is(string $name) : bool
33 | {
34 | return self::NAME === $name;
35 | }
36 |
37 | public function match($value) : bool
38 | {
39 | $this->backtrace->expanderEntrance(self::NAME, $value);
40 |
41 | if (!\is_string($value)) {
42 | $this->error = \sprintf('Match expander require "string", got "%s".', new StringConverter($value));
43 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
44 |
45 | return false;
46 | }
47 |
48 | if (1 !== \preg_match($this->pattern, $value)) {
49 | $this->error = \sprintf("string \"%s\" don't match pattern %s.", $value, $this->pattern);
50 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
51 |
52 | return false;
53 | }
54 |
55 | $this->backtrace->expanderSucceed(self::NAME, $value);
56 |
57 | return true;
58 | }
59 |
60 | public function getError() : ?string
61 | {
62 | return $this->error;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/NotContains.php:
--------------------------------------------------------------------------------
1 | string = $string;
28 | $this->ignoreCase = $ignoreCase;
29 | }
30 |
31 | public static function is(string $name) : bool
32 | {
33 | return self::NAME === $name;
34 | }
35 |
36 | public function match($value) : bool
37 | {
38 | $this->backtrace->expanderEntrance(self::NAME, $value);
39 |
40 | if (!\is_string($value)) {
41 | $this->error = \sprintf('Not contains expander require "string", got "%s".', new StringConverter($value));
42 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
43 |
44 | return false;
45 | }
46 |
47 | $contains = $this->ignoreCase
48 | ? \mb_stripos($value, $this->string)
49 | : \mb_strpos($value, $this->string);
50 |
51 | if ($contains !== false) {
52 | $this->error = \sprintf('String "%s" contains "%s".', $value, $this->string);
53 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
54 |
55 | return false;
56 | }
57 |
58 | $this->backtrace->expanderSucceed(self::NAME, $value);
59 |
60 | return true;
61 | }
62 |
63 | public function getError() : ?string
64 | {
65 | return $this->error;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/Contains.php:
--------------------------------------------------------------------------------
1 | string = $string;
28 | $this->ignoreCase = $ignoreCase;
29 | }
30 |
31 | public static function is(string $name) : bool
32 | {
33 | return self::NAME === $name;
34 | }
35 |
36 | public function match($value) : bool
37 | {
38 | $this->backtrace->expanderEntrance(self::NAME, $value);
39 |
40 | if (!\is_string($value)) {
41 | $this->error = \sprintf('Contains expander require "string", got "%s".', new StringConverter($value));
42 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
43 |
44 | return false;
45 | }
46 |
47 | $contains = $this->ignoreCase
48 | ? \mb_stripos($value, $this->string)
49 | : \mb_strpos($value, $this->string);
50 |
51 | if ($contains === false) {
52 | $this->error = \sprintf("String \"%s\" doesn't contains \"%s\".", $value, $this->string);
53 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
54 |
55 | return false;
56 | }
57 |
58 | $this->backtrace->expanderSucceed(self::NAME, $value);
59 |
60 | return true;
61 | }
62 |
63 | public function getError() : ?string
64 | {
65 | return $this->error;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/OneOf.php:
--------------------------------------------------------------------------------
1 | expanders[] = $argument;
38 | }
39 | }
40 |
41 | public static function is(string $name) : bool
42 | {
43 | return self::NAME === $name;
44 | }
45 |
46 | public function match($value) : bool
47 | {
48 | $this->backtrace->expanderEntrance(self::NAME, $value);
49 |
50 | foreach ($this->expanders as $expander) {
51 | if ($expander->match($value)) {
52 | $this->backtrace->expanderSucceed(self::NAME, $value);
53 |
54 | return true;
55 | }
56 | }
57 |
58 | $this->error = \sprintf('Any expander available in OneOf expander does not match "%s".', new StringConverter($value));
59 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
60 |
61 | return false;
62 | }
63 |
64 | public function getError() : ?string
65 | {
66 | return $this->error;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Matcher/DoubleMatcher.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
25 | $this->backtrace = $backtrace;
26 | }
27 |
28 | public function match($value, $pattern) : bool
29 | {
30 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
31 |
32 | if (!\is_float($value)) {
33 | $this->error = \sprintf('%s "%s" is not a valid double.', \gettype($value), new StringConverter($value));
34 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
35 |
36 | return false;
37 | }
38 |
39 | $typePattern = $this->parser->parse($pattern);
40 |
41 | if (!$typePattern->matchExpanders($value)) {
42 | $this->error = $typePattern->getError();
43 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
44 |
45 | return false;
46 | }
47 |
48 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
49 |
50 | return true;
51 | }
52 |
53 | public function canMatch($pattern) : bool
54 | {
55 | if (!\is_string($pattern)) {
56 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
57 |
58 | return false;
59 | }
60 |
61 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
62 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
63 |
64 | return $result;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Matcher/StringMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
25 | $this->parser = $parser;
26 | }
27 |
28 | public function match($value, $pattern) : bool
29 | {
30 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
31 |
32 | if (!\is_string($value)) {
33 | $this->error = \sprintf('%s "%s" is not a valid string.', \gettype($value), new StringConverter($value));
34 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
35 |
36 | return false;
37 | }
38 |
39 | $typePattern = $this->parser->parse($pattern);
40 |
41 | if (!$typePattern->matchExpanders($value)) {
42 | $this->error = $typePattern->getError();
43 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
44 |
45 | return false;
46 | }
47 |
48 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
49 |
50 | return true;
51 | }
52 |
53 | public function canMatch($pattern) : bool
54 | {
55 | if (!\is_string($pattern)) {
56 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
57 |
58 | return false;
59 | }
60 |
61 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
62 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
63 |
64 | return $result;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Matcher/IntegerMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
25 | $this->parser = $parser;
26 | }
27 |
28 | public function match($value, $pattern) : bool
29 | {
30 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
31 |
32 | if (!\is_int($value)) {
33 | $this->error = \sprintf('%s "%s" is not a valid integer.', \gettype($value), new StringConverter($value));
34 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
35 |
36 | return false;
37 | }
38 |
39 | $typePattern = $this->parser->parse($pattern);
40 |
41 | if (!$typePattern->matchExpanders($value)) {
42 | $this->error = $typePattern->getError();
43 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
44 |
45 | return false;
46 | }
47 |
48 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
49 |
50 | return true;
51 | }
52 |
53 | public function canMatch($pattern) : bool
54 | {
55 | if (!\is_string($pattern)) {
56 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
57 |
58 | return false;
59 | }
60 |
61 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
62 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
63 |
64 | return $result;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsInDateFormat.php:
--------------------------------------------------------------------------------
1 | format = $format;
26 | }
27 |
28 | public static function is(string $name) : bool
29 | {
30 | return self::NAME === $name;
31 | }
32 |
33 | public function match($value) : bool
34 | {
35 | $this->backtrace->expanderEntrance(self::NAME, $value);
36 |
37 | if (!\is_string($value)) {
38 | $this->error = \sprintf('IsDateTime expander require "string", got "%s".', new StringConverter($value));
39 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
40 |
41 | return false;
42 | }
43 |
44 | if (!$this->matchValue($value)) {
45 | $this->error = \sprintf('string "%s" is not a valid date.', $value);
46 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
47 |
48 | return false;
49 | }
50 |
51 | $this->backtrace->expanderSucceed(self::NAME, $value);
52 |
53 | return true;
54 | }
55 |
56 | public function getError() : ?string
57 | {
58 | return $this->error;
59 | }
60 |
61 | private function matchValue(string $value) : bool
62 | {
63 | try {
64 | $date = \DateTime::createFromFormat($this->format, $value);
65 |
66 | return $date && ($date->format($this->format) === $value);
67 | } catch (\Exception $exception) {
68 | return false;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsTzOffset.php:
--------------------------------------------------------------------------------
1 | error = null;
25 | }
26 |
27 | public static function is(string $name) : bool
28 | {
29 | return self::NAME === $name;
30 | }
31 |
32 | public function match($value) : bool
33 | {
34 | $this->backtrace->expanderEntrance(self::NAME, $value);
35 |
36 | if (!\is_string($value)) {
37 | $this->error = \sprintf('Match expander require "string", got "%s".', new StringConverter($value));
38 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
39 |
40 | return false;
41 | }
42 |
43 | try {
44 | $timezone = TimeZone::fromString($value);
45 |
46 | if ($result = $timezone->isOffset()) {
47 | $this->backtrace->expanderSucceed(self::NAME, $value);
48 | } else {
49 | $this->error = \sprintf('Timezone "%s" is not an offset type.', $value);
50 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
51 | }
52 |
53 | return $result;
54 | } catch (\Exception $exception) {
55 | $this->error = \sprintf('Timezone expander require valid timezone, got "%s".', new StringConverter($value));
56 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
57 |
58 | return false;
59 | }
60 | }
61 |
62 | public function getError() : ?string
63 | {
64 | return $this->error;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsTzIdentifier.php:
--------------------------------------------------------------------------------
1 | error = null;
25 | }
26 |
27 | public static function is(string $name) : bool
28 | {
29 | return self::NAME === $name;
30 | }
31 |
32 | public function match($value) : bool
33 | {
34 | $this->backtrace->expanderEntrance(self::NAME, $value);
35 |
36 | if (!\is_string($value)) {
37 | $this->error = \sprintf('Match expander require "string", got "%s".', new StringConverter($value));
38 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
39 |
40 | return false;
41 | }
42 |
43 | try {
44 | $timezone = TimeZone::fromString($value);
45 |
46 | if ($result = $timezone->isIdentifier()) {
47 | $this->backtrace->expanderSucceed(self::NAME, $value);
48 | } else {
49 | $this->error = \sprintf('Timezone "%s" is not an identifier type.', $value);
50 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
51 | }
52 |
53 | return $result;
54 | } catch (\Exception $exception) {
55 | $this->error = \sprintf('Timezone expander require valid timezone, got "%s".', new StringConverter($value));
56 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
57 |
58 | return false;
59 | }
60 | }
61 |
62 | public function getError() : ?string
63 | {
64 | return $this->error;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/IsTzAbbreviation.php:
--------------------------------------------------------------------------------
1 | error = null;
25 | }
26 |
27 | public static function is(string $name) : bool
28 | {
29 | return self::NAME === $name;
30 | }
31 |
32 | public function match($value) : bool
33 | {
34 | $this->backtrace->expanderEntrance(self::NAME, $value);
35 |
36 | if (!\is_string($value)) {
37 | $this->error = \sprintf('Match expander require "string", got "%s".', new StringConverter($value));
38 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
39 |
40 | return false;
41 | }
42 |
43 | try {
44 | $timezone = TimeZone::fromString($value);
45 |
46 | if ($result = $timezone->isAbbreviation()) {
47 | $this->backtrace->expanderSucceed(self::NAME, $value);
48 | } else {
49 | $this->error = \sprintf('Timezone "%s" is not an abbreviation type.', $value);
50 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
51 | }
52 |
53 | return $result;
54 | } catch (\Exception $exception) {
55 | $this->error = \sprintf('Timezone expander require valid timezone, got "%s".', new StringConverter($value));
56 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
57 |
58 | return false;
59 | }
60 | }
61 |
62 | public function getError() : ?string
63 | {
64 | return $this->error;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/LowerThan.php:
--------------------------------------------------------------------------------
1 | boundary = $boundary;
33 | }
34 |
35 | public static function is(string $name) : bool
36 | {
37 | return self::NAME === $name;
38 | }
39 |
40 | public function match($value) : bool
41 | {
42 | $this->backtrace->expanderEntrance(self::NAME, $value);
43 |
44 | if (!\is_float($value) && !\is_int($value)) {
45 | $this->error = \sprintf('Value "%s" is not a valid number.', new StringConverter($value));
46 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
47 |
48 | return false;
49 | }
50 |
51 | if ($value >= $this->boundary) {
52 | $this->error = \sprintf('Value "%s" is not lower than "%s".', new StringConverter($value), new StringConverter($this->boundary));
53 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
54 |
55 | return false;
56 | }
57 |
58 | $result = $value < $this->boundary;
59 |
60 | if ($result) {
61 | $this->backtrace->expanderSucceed(self::NAME, $value);
62 | } else {
63 | $this->backtrace->expanderFailed(self::NAME, $value, '');
64 | }
65 |
66 | return $result;
67 | }
68 |
69 | public function getError() : ?string
70 | {
71 | return $this->error;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Matcher/XmlMatcher.php:
--------------------------------------------------------------------------------
1 | arrayMatcher = $arrayMatcher;
21 | $this->backtrace = $backtrace;
22 | }
23 |
24 | public function match($value, $pattern) : bool
25 | {
26 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
27 |
28 | if (parent::match($value, $pattern)) {
29 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
30 |
31 | return true;
32 | }
33 |
34 | if (!Xml::isValid($value) || !Xml::isValid($pattern)) {
35 | $this->error = \sprintf("Value or pattern are not valid XML's");
36 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
37 |
38 | return false;
39 | }
40 |
41 | $arrayValue = XML2Array::createArray($value);
42 | $arrayPattern = XML2Array::createArray($pattern);
43 |
44 | $match = $this->arrayMatcher->match($arrayValue, $arrayPattern);
45 |
46 | if (!$match) {
47 | $this->error = \sprintf(
48 | 'Value %s does not match pattern %s',
49 | new StringConverter($value),
50 | new StringConverter($pattern)
51 | );
52 |
53 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
54 |
55 | return false;
56 | }
57 |
58 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
59 |
60 | return true;
61 | }
62 |
63 | public function canMatch($pattern) : bool
64 | {
65 | $result = Xml::isValid($pattern);
66 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
67 |
68 | return $result;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/GreaterThan.php:
--------------------------------------------------------------------------------
1 | boundary = $boundary;
33 | }
34 |
35 | public static function is(string $name) : bool
36 | {
37 | return self::NAME === $name;
38 | }
39 |
40 | public function match($value) : bool
41 | {
42 | $this->backtrace->expanderEntrance(self::NAME, $value);
43 |
44 | if (!\is_float($value) && !\is_int($value) && !\is_numeric($value)) {
45 | $this->error = \sprintf('Value "%s" is not a valid number.', new StringConverter($value));
46 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
47 |
48 | return false;
49 | }
50 |
51 | if ($value <= $this->boundary) {
52 | $this->error = \sprintf('Value "%s" is not greater than "%s".', new StringConverter($value), new StringConverter($this->boundary));
53 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
54 |
55 | return false;
56 | }
57 |
58 | $result = $value > $this->boundary;
59 |
60 | if ($result) {
61 | $this->backtrace->expanderSucceed(self::NAME, $value);
62 | } else {
63 | $this->backtrace->expanderFailed(self::NAME, $value, '');
64 | }
65 |
66 | return $result;
67 | }
68 |
69 | public function getError() : ?string
70 | {
71 | return $this->error;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/StartsWith.php:
--------------------------------------------------------------------------------
1 | stringBeginning = $stringBeginning;
28 | $this->ignoreCase = $ignoreCase;
29 | }
30 |
31 | public static function is(string $name) : bool
32 | {
33 | return self::NAME === $name;
34 | }
35 |
36 | public function match($value) : bool
37 | {
38 | $this->backtrace->expanderEntrance(self::NAME, $value);
39 |
40 | if (!\is_string($value)) {
41 | $this->error = \sprintf('StartsWith expander require "string", got "%s".', new StringConverter($value));
42 | $this->backtrace->expanderSucceed(self::NAME, $value);
43 |
44 | return false;
45 | }
46 |
47 | if (empty($this->stringBeginning)) {
48 | $this->backtrace->expanderSucceed(self::NAME, $value);
49 |
50 | return true;
51 | }
52 |
53 | if ($this->matchValue($value)) {
54 | $this->error = \sprintf("string \"%s\" doesn't starts with string \"%s\".", $value, $this->stringBeginning);
55 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
56 |
57 | return false;
58 | }
59 |
60 | $this->backtrace->expanderSucceed(self::NAME, $value);
61 |
62 | return true;
63 | }
64 |
65 | public function getError() : ?string
66 | {
67 | return $this->error;
68 | }
69 |
70 | private function matchValue(string $value) : bool
71 | {
72 | return $this->ignoreCase
73 | ? \mb_stripos($value, $this->stringBeginning) !== 0
74 | : \mb_strpos($value, $this->stringBeginning) !== 0;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Matcher/JsonMatcher.php:
--------------------------------------------------------------------------------
1 | arrayMatcher = $arrayMatcher;
19 | $this->backtrace = $backtrace;
20 | }
21 |
22 | public function match($value, $pattern) : bool
23 | {
24 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
25 |
26 | if (parent::match($value, $pattern)) {
27 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
28 |
29 | return true;
30 | }
31 |
32 | if (!Json::isValid($value)) {
33 | $this->error = \sprintf('Invalid given JSON of value. %s', Json::getErrorMessage());
34 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
35 |
36 | return false;
37 | }
38 |
39 | if (!Json::isValidPattern($pattern)) {
40 | $this->error = \sprintf('Invalid given JSON of pattern. %s', Json::getErrorMessage());
41 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
42 |
43 | return false;
44 | }
45 |
46 | $transformedPattern = Json::isValid($pattern) ? $pattern : Json::transformPattern($pattern);
47 |
48 | $match = $this->arrayMatcher->match(\json_decode($value, true), \json_decode($transformedPattern, true));
49 |
50 | if (!$match) {
51 | $this->error = $this->arrayMatcher->getError();
52 |
53 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
54 |
55 | return false;
56 | }
57 |
58 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
59 |
60 | return true;
61 | }
62 |
63 | public function canMatch($pattern) : bool
64 | {
65 | $result = Json::isValidPattern($pattern);
66 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
67 |
68 | return $result;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/EndsWith.php:
--------------------------------------------------------------------------------
1 | stringEnding = $stringEnding;
28 | $this->ignoreCase = $ignoreCase;
29 | }
30 |
31 | public static function is(string $name) : bool
32 | {
33 | return self::NAME === $name;
34 | }
35 |
36 | public function match($value) : bool
37 | {
38 | $this->backtrace->expanderEntrance(self::NAME, $value);
39 |
40 | if (!\is_string($value)) {
41 | $this->error = \sprintf('EndsWith expander require "string", got "%s".', new StringConverter($value));
42 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
43 |
44 | return false;
45 | }
46 |
47 | if (empty($this->stringEnding)) {
48 | $this->backtrace->expanderSucceed(self::NAME, $value);
49 |
50 | return true;
51 | }
52 |
53 | if (!$this->matchValue($value)) {
54 | $this->error = \sprintf("string \"%s\" doesn't ends with string \"%s\".", $value, $this->stringEnding);
55 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
56 |
57 | return false;
58 | }
59 |
60 | $this->backtrace->expanderSucceed(self::NAME, $value);
61 |
62 | return true;
63 | }
64 |
65 | public function getError() : ?string
66 | {
67 | return $this->error;
68 | }
69 |
70 | private function matchValue(string $value) : bool
71 | {
72 | return $this->ignoreCase
73 | ? \mb_substr(\mb_strtolower($value), -\mb_strlen(\mb_strtolower($this->stringEnding))) === \mb_strtolower($this->stringEnding)
74 | : \mb_substr($value, -\mb_strlen($this->stringEnding)) === $this->stringEnding;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/DateTimeComparisonTrait.php:
--------------------------------------------------------------------------------
1 | boundary = DateTime::fromString($boundary);
22 | } catch (\Exception $exception) {
23 | throw new \InvalidArgumentException(\sprintf('Boundary value "%s" is not a valid date, date time or time.', new StringConverter($boundary)));
24 | }
25 | }
26 |
27 | public static function is(string $name) : bool
28 | {
29 | return static::getName() === $name;
30 | }
31 |
32 | public function match($value) : bool
33 | {
34 | $this->backtrace->expanderEntrance(static::getName(), $value);
35 |
36 | if (!\is_string($value)) {
37 | $this->error = \sprintf('%s expander require "string", got "%s".', static::getName(), new StringConverter($value));
38 | $this->backtrace->expanderFailed(static::getName(), $value, $this->error);
39 |
40 | return false;
41 | }
42 |
43 | return $this->compare($value);
44 | }
45 |
46 | public function getError() : ?string
47 | {
48 | return $this->error;
49 | }
50 |
51 | abstract protected static function getName() : string;
52 |
53 | /**
54 | * @param string $value raw value
55 | * @param DateTime $datetime value converted in DateTime object
56 | */
57 | abstract protected function handleComparison(string $value, DateTime $datetime) : bool;
58 |
59 | private function compare(string $value) : bool
60 | {
61 | try {
62 | $datetime = DateTime::fromString($value);
63 | } catch (\Exception $e) {
64 | $this->error = \sprintf('Value "%s" is not a valid date, date time or time.', new StringConverter($value));
65 | $this->backtrace->expanderFailed(static::getName(), $value, $this->error);
66 |
67 | return false;
68 | }
69 |
70 | return $this->handleComparison($value, $datetime);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Matcher/DateMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
26 | $this->parser = $parser;
27 | }
28 |
29 | public function match($value, $pattern) : bool
30 | {
31 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
32 |
33 | if (!\is_string($value)) {
34 | $this->error = \sprintf('%s "%s" is not a valid string.', \gettype($value), new StringConverter($value));
35 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
36 |
37 | return false;
38 | }
39 |
40 | try {
41 | /** @phpstan-ignore-next-line */
42 | Day::fromString($value);
43 | } catch (\Exception $exception) {
44 | $this->error = \sprintf('%s "%s" is not a valid date.', $value, new StringConverter($value));
45 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
46 |
47 | return false;
48 | }
49 |
50 | $typePattern = $this->parser->parse($pattern);
51 |
52 | if (!$typePattern->matchExpanders($value)) {
53 | $this->error = $typePattern->getError();
54 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
55 |
56 | return false;
57 | }
58 |
59 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
60 |
61 | return true;
62 | }
63 |
64 | public function canMatch($pattern) : bool
65 | {
66 | if (!\is_string($pattern)) {
67 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
68 |
69 | return false;
70 | }
71 |
72 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
73 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
74 |
75 | return $result;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Matcher/TimeMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
26 | $this->parser = $parser;
27 | }
28 |
29 | public function match($value, $pattern) : bool
30 | {
31 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
32 |
33 | if (!\is_string($value)) {
34 | $this->error = \sprintf('%s "%s" is not a valid string.', \gettype($value), new StringConverter($value));
35 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
36 |
37 | return false;
38 | }
39 |
40 | try {
41 | /** @phpstan-ignore-next-line */
42 | Time::fromString($value);
43 | } catch (\Exception $exception) {
44 | $this->error = \sprintf('%s "%s" is not a valid time.', $value, new StringConverter($value));
45 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
46 |
47 | return false;
48 | }
49 |
50 | $typePattern = $this->parser->parse($pattern);
51 |
52 | if (!$typePattern->matchExpanders($value)) {
53 | $this->error = $typePattern->getError();
54 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
55 |
56 | return false;
57 | }
58 |
59 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
60 |
61 | return true;
62 | }
63 |
64 | public function canMatch($pattern) : bool
65 | {
66 | if (!\is_string($pattern)) {
67 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
68 |
69 | return false;
70 | }
71 |
72 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
73 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
74 |
75 | return $result;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Matcher/UuidMatcher.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
35 | $this->backtrace = $backtrace;
36 | }
37 |
38 | public function match($value, $pattern) : bool
39 | {
40 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
41 |
42 | if (!\is_string($value)) {
43 | $this->error = \sprintf(
44 | '%s "%s" is not a valid UUID: not a string.',
45 | \gettype($value),
46 | new StringConverter($value)
47 | );
48 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
49 |
50 | return false;
51 | }
52 |
53 | if (1 !== \preg_match(self::UUID_FORMAT_PATTERN, $value)) {
54 | $this->error = \sprintf(
55 | '%s "%s" is not a valid UUID: invalid format.',
56 | \gettype($value),
57 | $value
58 | );
59 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
60 |
61 | return false;
62 | }
63 |
64 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
65 |
66 | return true;
67 | }
68 |
69 | public function canMatch($pattern) : bool
70 | {
71 | if (!\is_string($pattern)) {
72 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
73 |
74 | return false;
75 | }
76 |
77 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
78 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
79 |
80 | return $result;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Matcher/DateTimeMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
26 | $this->parser = $parser;
27 | }
28 |
29 | public function match($value, $pattern) : bool
30 | {
31 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
32 |
33 | if (!\is_string($value)) {
34 | $this->error = \sprintf('%s "%s" is not a valid string.', \gettype($value), new StringConverter($value));
35 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
36 |
37 | return false;
38 | }
39 |
40 | try {
41 | /** @phpstan-ignore-next-line */
42 | DateTime::fromString($value);
43 | } catch (\Exception $exception) {
44 | $this->error = \sprintf('%s "%s" is not a valid date time.', $value, new StringConverter($value));
45 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
46 |
47 | return false;
48 | }
49 |
50 | $typePattern = $this->parser->parse($pattern);
51 |
52 | if (!$typePattern->matchExpanders($value)) {
53 | $this->error = $typePattern->getError();
54 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
55 |
56 | return false;
57 | }
58 |
59 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
60 |
61 | return true;
62 | }
63 |
64 | public function canMatch($pattern) : bool
65 | {
66 | if (!\is_string($pattern)) {
67 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
68 |
69 | return false;
70 | }
71 |
72 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
73 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
74 |
75 | return $result;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Matcher/JsonObjectMatcher.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
26 | $this->parser = $parser;
27 | }
28 |
29 | public function match($value, $pattern) : bool
30 | {
31 | if (!$this->isJsonPattern($pattern)) {
32 | $this->error = \sprintf('%s "%s" is not a valid json.', \gettype($value), new StringConverter($value));
33 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
34 |
35 | return false;
36 | }
37 |
38 | if (!Json::isValid($value) && null !== $value && !\is_array($value)) {
39 | $this->error = \sprintf('Invalid given JSON of value. %s', Json::getErrorMessage());
40 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
41 |
42 | return false;
43 | }
44 |
45 | return $this->allExpandersMatch($value, $pattern);
46 | }
47 |
48 | public function canMatch($pattern) : bool
49 | {
50 | $result = \is_string($pattern) && $this->isJsonPattern($pattern);
51 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
52 |
53 | return $result;
54 | }
55 |
56 | private function isJsonPattern($pattern) : bool
57 | {
58 | if (!\is_string($pattern)) {
59 | return false;
60 | }
61 |
62 | return $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::JSON_PATTERN);
63 | }
64 |
65 | private function allExpandersMatch($value, $pattern) : bool
66 | {
67 | $typePattern = $this->parser->parse($pattern);
68 |
69 | if (!$typePattern->matchExpanders($value)) {
70 | $this->error = $typePattern->getError();
71 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
72 |
73 | return false;
74 | }
75 |
76 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
77 |
78 | return true;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/HasProperty.php:
--------------------------------------------------------------------------------
1 | propertyName = $propertyName;
27 | }
28 |
29 | public static function is(string $name) : bool
30 | {
31 | return self::NAME === $name;
32 | }
33 |
34 | public function match($value) : bool
35 | {
36 | $this->backtrace->expanderEntrance(self::NAME, $value);
37 |
38 | if (\is_array($value)) {
39 | $hasProperty = \array_key_exists($this->propertyName, $value);
40 |
41 | if (!$hasProperty) {
42 | $this->error = \sprintf('"json" object "%s" does not have "%s" propety.', new StringConverter($value), new StringConverter($this->propertyName));
43 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
44 |
45 | return false;
46 | }
47 |
48 | $this->backtrace->expanderSucceed(self::NAME, $value);
49 |
50 | return true;
51 | }
52 |
53 | if (!Json::isValid($value)) {
54 | $this->error = \sprintf('HasProperty expander require valid "json" string, got "%s".', new StringConverter($value));
55 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
56 |
57 | return false;
58 | }
59 |
60 | $jsonArray = \json_decode(Json::reformat($value), true);
61 |
62 | $hasProperty = \array_key_exists($this->propertyName, $jsonArray);
63 |
64 | if (!$hasProperty) {
65 | $this->error = \sprintf('"json" object "%s" does not have "%s" propety.', new StringConverter($value), new StringConverter($this->propertyName));
66 | $this->backtrace->expanderFailed(self::NAME, $value, $this->error);
67 |
68 | return false;
69 | }
70 |
71 | $this->backtrace->expanderSucceed(self::NAME, $value);
72 |
73 | return true;
74 | }
75 |
76 | public function getError() : ?string
77 | {
78 | return $this->error;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Assert/Json.php:
--------------------------------------------------------------------------------
1 | backtrace = $backtrace;
31 | $this->parser = $parser;
32 | }
33 |
34 | public function match($value, $pattern) : bool
35 | {
36 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
37 |
38 | if (!\is_string($value)) {
39 | $this->error = \sprintf('%s "%s" is not a valid string.', \gettype($value), new StringConverter($value));
40 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
41 |
42 | return false;
43 | }
44 |
45 | try {
46 | /** @phpstan-ignore-next-line */
47 | TimeZone::fromString($value);
48 | } catch (\Exception $exception) {
49 | $this->error = \sprintf('%s "%s" is not a valid timezone.', $value, new StringConverter($value));
50 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
51 |
52 | return false;
53 | }
54 |
55 | $typePattern = $this->parser->parse($pattern);
56 |
57 | if (!$typePattern->matchExpanders($value)) {
58 | $this->error = $typePattern->getError();
59 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
60 |
61 | return false;
62 | }
63 |
64 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
65 |
66 | return true;
67 | }
68 |
69 | public function canMatch($pattern) : bool
70 | {
71 | if (!\is_string($pattern)) {
72 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
73 |
74 | return false;
75 | }
76 |
77 | $result = $this->parser->hasValidSyntax($pattern) && ($this->parser->parse($pattern)->is(self::PATTERN) || $this->parser->parse($pattern)->is(self::PATTERN_SHORT));
78 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
79 |
80 | return $result;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/PHPUnit/PHPMatcherConstraint.php:
--------------------------------------------------------------------------------
1 | pattern = $pattern;
31 | $this->matcher = new PHPMatcher($backtrace);
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function toString() : string
38 | {
39 | return 'matches given pattern.';
40 | }
41 |
42 | protected function failureDescription($other) : string
43 | {
44 | $errorDescription = $this->matcher->error() ?: 'Value does not match given pattern';
45 | $backtrace = $this->matcher->backtrace();
46 |
47 | return $backtrace instanceof VoidBacktrace
48 | ? $errorDescription
49 | : $errorDescription
50 | . "\nBacktrace:\n" . $this->matcher->backtrace();
51 | }
52 |
53 | protected function matches($other) : bool
54 | {
55 | return $this->matcher->match($other, $this->pattern);
56 | }
57 |
58 | /**
59 | * {@inheritdoc}
60 | */
61 | protected function fail($other, $description, ?ComparisonFailure $comparisonFailure = null) : never
62 | {
63 | parent::fail($other, $description, $comparisonFailure ?? $this->createComparisonFailure($other));
64 | }
65 |
66 | private function createComparisonFailure($other) : ?ComparisonFailure
67 | {
68 | if (!\is_string($other) || !\is_string($this->pattern) || !\class_exists(Json::class)) {
69 | return null;
70 | }
71 |
72 | [$error, $otherJson] = Json::canonicalize($other);
73 |
74 | if ($error) {
75 | return null;
76 | }
77 |
78 | [$error, $patternJson] = Json::canonicalize($this->pattern);
79 |
80 | if ($error) {
81 | return null;
82 | }
83 |
84 | return new ComparisonFailure(
85 | \json_decode($this->pattern),
86 | \json_decode($other),
87 | Json::prettify($patternJson),
88 | Json::prettify($otherJson),
89 | 'Failed asserting that the pattern matches the given value.'
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Matcher/ChainMatcher.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | private array $matcherErrors;
27 |
28 | /**
29 | * @param Backtrace $backtrace
30 | * @param ValueMatcher[] $matchers
31 | */
32 | public function __construct(string $name, Backtrace $backtrace, array $matchers = [])
33 | {
34 | $this->backtrace = $backtrace;
35 | $this->matchers = $matchers;
36 | $this->name = $name;
37 | }
38 |
39 | public function registerMatcher(ValueMatcher $matcher) : void
40 | {
41 | $this->matchers[] = $matcher;
42 | }
43 |
44 | public function match($value, $pattern) : bool
45 | {
46 | $this->backtrace->matcherEntrance($this->matcherName(), $value, $pattern);
47 |
48 | foreach ($this->matchers as $propertyMatcher) {
49 | if ($propertyMatcher->canMatch($pattern)) {
50 | if ($propertyMatcher->match($value, $pattern)) {
51 | $this->backtrace->matcherSucceed($this->matcherName(), $value, $pattern);
52 |
53 | return true;
54 | }
55 |
56 | $this->matcherErrors[\get_class($propertyMatcher)] = (string) $propertyMatcher->getError();
57 | $this->error = $propertyMatcher->getError();
58 | }
59 | }
60 |
61 | if (!isset($this->error)) {
62 | if (\is_array($value) && isset($this->matcherErrors[ArrayMatcher::class])) {
63 | $this->error = $this->matcherErrors[ArrayMatcher::class];
64 | } elseif (Json::isValidPattern($pattern) && isset($this->matcherErrors[JsonMatcher::class])) {
65 | $this->error = $this->matcherErrors[JsonMatcher::class];
66 | } else {
67 | $this->error = \sprintf(
68 | 'Any matcher from chain can\'t match value "%s" to pattern "%s"',
69 | new SingleLineString((string) new StringConverter($value)),
70 | new SingleLineString((string) new StringConverter($pattern))
71 | );
72 | }
73 | }
74 |
75 | $this->backtrace->matcherFailed($this->matcherName(), $value, $pattern, $this->error);
76 |
77 | return false;
78 | }
79 |
80 | public function canMatch($pattern) : bool
81 | {
82 | $this->backtrace->matcherCanMatch($this->matcherName(), $pattern, true);
83 |
84 | return true;
85 | }
86 |
87 | /**
88 | * @return string
89 | */
90 | private function matcherName() : string
91 | {
92 | return \sprintf('%s (%s)', self::class, $this->name);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "coduo/php-matcher",
3 | "type": "library",
4 | "description": "PHP Matcher enables you to match values with patterns",
5 | "keywords": ["json", "matcher", "tests", "match"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Michał Dąbrowski",
10 | "email": "dabrowski@brillante.pl"
11 | },
12 | {
13 | "name": "Norbert Orzechowicz",
14 | "email": "norbert@orzechowicz.pl"
15 | }
16 | ],
17 | "require": {
18 | "php": "~8.3 || ~8.4 || ~8.5",
19 | "ext-filter": "*",
20 | "ext-json": "*",
21 | "ext-simplexml": "*",
22 | "aeon-php/calendar": "^1.0.6",
23 | "coduo/php-to-string": "^3",
24 | "doctrine/lexer": "^3.0"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^10.5",
28 | "openlss/lib-array2xml": "^1.0",
29 | "symfony/expression-language": "^5.4 || ^6.4 || ^7.3 || ^8.0"
30 | },
31 | "suggest": {
32 | "openlss/lib-array2xml": "In order ot use Coduo\\PHPMatcher\\Matcher\\XmlMatcher",
33 | "symfony/expression-language" : "In order to use Coduo\\PHPMatcher\\Matcher\\ExpressionMatcher"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "Coduo\\PHPMatcher\\": "src/"
38 | }
39 | },
40 | "autoload-dev": {
41 | "psr-4": {
42 | "Coduo\\PHPMatcher\\Tests\\": "tests/",
43 | "Coduo\\PHPMatcher\\Benchmark\\": "benchmark/Coduo/PHPMatcher/Benchmark/"
44 | }
45 | },
46 | "config": {
47 | "sort-packages": true
48 | },
49 | "scripts": {
50 | "benchmark": [
51 | "Composer\\Config::disableProcessTimeout",
52 | "tools\/phpbench\/vendor\/bin\/phpbench run --report=matcher"
53 | ],
54 | "build": [
55 | "@static:analyze",
56 | "@test",
57 | "@test:mutation"
58 | ],
59 | "cs:php:fix": [
60 | "tools\/cs-fixer\/vendor\/bin\/php-cs-fixer fix --using-cache=no"
61 | ],
62 | "test" : [
63 | "vendor\/bin\/phpunit --coverage-html var/phpunit/coverage/html --coverage-filter src"
64 | ],
65 | "test:mutation": [
66 | "Composer\\Config::disableProcessTimeout",
67 | "tools\/infection\/vendor\/bin\/infection --threads=4"
68 | ],
69 | "static:analyze": [
70 | "tools\/cs-fixer\/vendor\/bin\/php-cs-fixer fix --dry-run",
71 | "tools\/phpstan\/vendor\/bin\/phpstan analyze -c phpstan.neon"
72 | ],
73 | "tools:install": [
74 | "composer install --working-dir=./tools/cs-fixer",
75 | "composer install --working-dir=./tools/infection",
76 | "composer install --working-dir=./tools/phpbench",
77 | "composer install --working-dir=./tools/phpstan"
78 | ],
79 | "tools:update": [
80 | "composer update --working-dir=./tools/cs-fixer",
81 | "composer update --working-dir=./tools/infection",
82 | "composer update --working-dir=./tools/phpbench",
83 | "composer update --working-dir=./tools/phpstan"
84 | ],
85 | "post-install-cmd": [
86 | "@tools:install"
87 | ],
88 | "post-update-cmd": [
89 | "@tools:update"
90 | ]
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at norbert+coduo@orzechowicz.pl. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/Matcher/UlidMatcher.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
34 | $this->backtrace = $backtrace;
35 | }
36 |
37 | public function match($value, $pattern) : bool
38 | {
39 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
40 |
41 | if (!\is_string($value)) {
42 | $this->error = \sprintf(
43 | '%s "%s" is not a valid ULID: not a string.',
44 | \gettype($value),
45 | new StringConverter($value)
46 | );
47 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
48 |
49 | return false;
50 | }
51 |
52 | if (\strlen($value) !== \strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) {
53 | $this->error = \sprintf(
54 | '%s "%s" is not a valid ULID: invalid characters.',
55 | \gettype($value),
56 | new StringConverter($value)
57 | );
58 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
59 |
60 | return false;
61 | }
62 |
63 | if (26 < \strlen($value)) {
64 | $this->error = \sprintf(
65 | '%s "%s" is not a valid ULID: too long.',
66 | \gettype($value),
67 | new StringConverter($value)
68 | );
69 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
70 |
71 | return false;
72 | }
73 |
74 | if (26 > \strlen($value)) {
75 | $this->error = \sprintf(
76 | '%s "%s" is not a valid ULID: too short.',
77 | \gettype($value),
78 | new StringConverter($value)
79 | );
80 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
81 |
82 | return false;
83 | }
84 |
85 | // Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'
86 | // Cf https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings
87 | if ($value[0] > '7') {
88 | $this->error = \sprintf(
89 | '%s "%s" is not a valid ULID: overflow.',
90 | \gettype($value),
91 | new StringConverter($value)
92 | );
93 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->error);
94 |
95 | return false;
96 | }
97 |
98 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
99 |
100 | return true;
101 | }
102 |
103 | public function canMatch($pattern) : bool
104 | {
105 | if (!\is_string($pattern)) {
106 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
107 |
108 | return false;
109 | }
110 |
111 | $result = $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
112 | $this->backtrace->matcherCanMatch(self::class, $pattern, $result);
113 |
114 | return $result;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/Factory/MatcherFactory.php:
--------------------------------------------------------------------------------
1 | buildMatchers($this->buildParser($backtrace), $backtrace), $backtrace);
18 | }
19 |
20 | private function buildMatchers(Parser $parser, Backtrace $backtrace) : Matcher\ChainMatcher
21 | {
22 | $scalarMatchers = $this->buildScalarMatchers($parser, $backtrace);
23 | $arrayMatcher = $this->buildArrayMatcher($scalarMatchers, $parser, $backtrace);
24 |
25 | // Matchers are registered in order of matching
26 | // 1) all scalars
27 | // 2) json/xml
28 | // 3) array
29 | // 4) or "||"
30 | // 5) full text
31 |
32 | $matchers = [$scalarMatchers];
33 | $matchers[] = new Matcher\JsonMatcher($arrayMatcher, $backtrace);
34 |
35 | if (\class_exists(\LSS\XML2Array::class)) {
36 | $matchers[] = new Matcher\XmlMatcher($arrayMatcher, $backtrace);
37 | }
38 |
39 | $matchers[] = $arrayMatcher;
40 | $matchers[] = $this->buildOrMatcher($backtrace, $matchers);
41 | $matchers[] = new Matcher\TextMatcher($backtrace, $parser);
42 |
43 | return new Matcher\ChainMatcher(
44 | 'all',
45 | $backtrace,
46 | $matchers
47 | );
48 | }
49 |
50 | private function buildArrayMatcher(Matcher\ChainMatcher $scalarMatchers, Parser $parser, Backtrace $backtrace) : Matcher\ArrayMatcher
51 | {
52 | $arrayMatcher = new Matcher\ArrayMatcher(
53 | new Matcher\ChainMatcher(
54 | 'array',
55 | $backtrace,
56 | [
57 | new Matcher\OrMatcher($backtrace, $orMatchers = clone $scalarMatchers),
58 | $scalarMatchers,
59 | new Matcher\TextMatcher($backtrace, $parser),
60 | ]
61 | ),
62 | $backtrace,
63 | $parser
64 | );
65 | $orMatchers->registerMatcher($arrayMatcher);
66 |
67 | return $arrayMatcher;
68 | }
69 |
70 | private function buildScalarMatchers(Parser $parser, Backtrace $backtrace) : Matcher\ChainMatcher
71 | {
72 | return new Matcher\ChainMatcher(
73 | 'scalars',
74 | $backtrace,
75 | [
76 | new Matcher\CallbackMatcher($backtrace),
77 | new Matcher\ExpressionMatcher($backtrace),
78 | new Matcher\NullMatcher($backtrace),
79 | new Matcher\StringMatcher($backtrace, $parser),
80 | new Matcher\IntegerMatcher($backtrace, $parser),
81 | new Matcher\BooleanMatcher($backtrace, $parser),
82 | new Matcher\DoubleMatcher($backtrace, $parser),
83 | new Matcher\NumberMatcher($backtrace, $parser),
84 | new Matcher\TimeMatcher($backtrace, $parser),
85 | new Matcher\DateMatcher($backtrace, $parser),
86 | new Matcher\DateTimeMatcher($backtrace, $parser),
87 | new Matcher\TimeZoneMatcher($backtrace, $parser),
88 | new Matcher\ScalarMatcher($backtrace),
89 | new Matcher\WildcardMatcher($backtrace),
90 | new Matcher\UuidMatcher($backtrace, $parser),
91 | new Matcher\UlidMatcher($backtrace, $parser),
92 | new Matcher\JsonObjectMatcher($backtrace, $parser),
93 | ]
94 | );
95 | }
96 |
97 | private function buildOrMatcher(Backtrace $backtrace, array $orMatchers) : Matcher\OrMatcher
98 | {
99 | return new Matcher\OrMatcher(
100 | $backtrace,
101 | new Matcher\ChainMatcher(
102 | 'or',
103 | $backtrace,
104 | $orMatchers
105 | )
106 | );
107 | }
108 |
109 | private function buildParser(Backtrace $backtrace) : Parser
110 | {
111 | return new Parser(new Lexer(), new Parser\ExpanderInitializer($backtrace));
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Backtrace/InMemoryBacktrace.php:
--------------------------------------------------------------------------------
1 | trace = [];
21 | }
22 |
23 | public function __toString() : string
24 | {
25 | return \implode("\n", $this->trace);
26 | }
27 |
28 | public function matcherCanMatch(string $name, $value, bool $result) : void
29 | {
30 | $this->trace[] = \sprintf(
31 | '#%d Matcher %s %s match pattern "%s"',
32 | $this->entriesCount(),
33 | $name,
34 | $result ? 'can' : "can't",
35 | new SingleLineString((string) new StringConverter($value))
36 | );
37 | }
38 |
39 | public function matcherEntrance(string $name, $value, $pattern) : void
40 | {
41 | $this->trace[] = \sprintf(
42 | '#%d Matcher %s matching value "%s" with "%s" pattern',
43 | $this->entriesCount(),
44 | $name,
45 | new SingleLineString((string) new StringConverter($value)),
46 | new SingleLineString((string) new StringConverter($pattern))
47 | );
48 | }
49 |
50 | public function matcherSucceed(string $name, $value, $pattern) : void
51 | {
52 | $this->trace[] = \sprintf(
53 | '#%d Matcher %s successfully matched value "%s" with "%s" pattern',
54 | $this->entriesCount(),
55 | $name,
56 | new SingleLineString((string) new StringConverter($value)),
57 | new SingleLineString((string) new StringConverter($pattern))
58 | );
59 | }
60 |
61 | public function matcherFailed(string $name, $value, $pattern, string $error) : void
62 | {
63 | $this->trace[] = \sprintf(
64 | '#%d Matcher %s failed to match value "%s" with "%s" pattern',
65 | $this->entriesCount(),
66 | $name,
67 | new SingleLineString((string) new StringConverter($value)),
68 | new SingleLineString((string) new StringConverter($pattern))
69 | );
70 |
71 | $this->trace[] = \sprintf(
72 | '#%d Matcher %s error: %s',
73 | $this->entriesCount(),
74 | $name,
75 | new SingleLineString($error)
76 | );
77 | }
78 |
79 | public function expanderEntrance(string $name, $value) : void
80 | {
81 | $this->trace[] = \sprintf(
82 | '#%d Expander %s matching value "%s"',
83 | $this->entriesCount(),
84 | $name,
85 | new SingleLineString((string) new StringConverter($value))
86 | );
87 | }
88 |
89 | public function expanderSucceed(string $name, $value) : void
90 | {
91 | $this->trace[] = \sprintf(
92 | '#%d Expander %s successfully matched value "%s"',
93 | $this->entriesCount(),
94 | $name,
95 | new SingleLineString((string) new StringConverter($value))
96 | );
97 | }
98 |
99 | public function expanderFailed(string $name, $value, string $error) : void
100 | {
101 | $this->trace[] = \sprintf(
102 | '#%d Expander %s failed to match value "%s"',
103 | $this->entriesCount(),
104 | $name,
105 | new SingleLineString((string) new StringConverter($value))
106 | );
107 |
108 | $this->trace[] = \sprintf(
109 | '#%d Expander %s error: %s',
110 | $this->entriesCount(),
111 | $name,
112 | new SingleLineString($error)
113 | );
114 | }
115 |
116 | public function isEmpty() : bool
117 | {
118 | return \count($this->trace) === 0;
119 | }
120 |
121 | /**
122 | * @return mixed[]
123 | */
124 | public function raw() : array
125 | {
126 | return $this->trace;
127 | }
128 |
129 | public function last() : ?string
130 | {
131 | if ($this->entriesCount()) {
132 | return \end($this->trace);
133 | }
134 |
135 | return null;
136 | }
137 |
138 | private function entriesCount() : int
139 | {
140 | return \count($this->trace) + 1;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/Lexer.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | final class Lexer extends AbstractLexer
13 | {
14 | /**
15 | * @var int
16 | */
17 | public const T_NONE = 1;
18 |
19 | /**
20 | * @var int
21 | */
22 | public const T_EXPANDER_NAME = 2;
23 |
24 | /**
25 | * @var int
26 | */
27 | public const T_CLOSE_PARENTHESIS = 3;
28 |
29 | /**
30 | * @var int
31 | */
32 | public const T_OPEN_CURLY_BRACE = 4;
33 |
34 | /**
35 | * @var int
36 | */
37 | public const T_CLOSE_CURLY_BRACE = 5;
38 |
39 | /**
40 | * @var int
41 | */
42 | public const T_STRING = 6;
43 |
44 | /**
45 | * @var int
46 | */
47 | public const T_NUMBER = 7;
48 |
49 | /**
50 | * @var int
51 | */
52 | public const T_BOOLEAN = 8;
53 |
54 | /**
55 | * @var int
56 | */
57 | public const T_NULL = 9;
58 |
59 | /**
60 | * @var int
61 | */
62 | public const T_COMMA = 10;
63 |
64 | /**
65 | * @var int
66 | */
67 | public const T_COLON = 11;
68 |
69 | /**
70 | * @var int
71 | */
72 | public const T_TYPE_PATTERN = 12;
73 |
74 | /**
75 | * Lexical catchable patterns.
76 | */
77 | protected function getCatchablePatterns() : array
78 | {
79 | return [
80 | '\\.?[a-zA-Z0-9_]+\\(', // expander name
81 | '[a-zA-Z0-9.]*', // words
82 | '\\-?[0-9]*\\.?[0-9]*', // numbers
83 | "'(?:[^']|'')*'", // string between ' character
84 | '"(?:[^"]|"")*"', // string between " character,
85 | '@[a-zA-Z0-9\\*.]+@', // type pattern
86 | ];
87 | }
88 |
89 | /**
90 | * Lexical non-catchable patterns.
91 | *
92 | * @return string[]
93 | */
94 | protected function getNonCatchablePatterns() : array
95 | {
96 | return [
97 | '\\s+',
98 | ];
99 | }
100 |
101 | /**
102 | * Retrieve token type. Also processes the token value if necessary.
103 | */
104 | protected function getType(&$value) : int
105 | {
106 | $type = self::T_NONE;
107 |
108 | if (')' === $value) {
109 | return self::T_CLOSE_PARENTHESIS;
110 | }
111 |
112 | if ('{' === $value) {
113 | return self::T_OPEN_CURLY_BRACE;
114 | }
115 |
116 | if ('}' === $value) {
117 | return self::T_CLOSE_CURLY_BRACE;
118 | }
119 |
120 | if (':' === $value) {
121 | return self::T_COLON;
122 | }
123 |
124 | if (',' === $value) {
125 | return self::T_COMMA;
126 | }
127 |
128 | if ($this->isTypePatternToken($value)) {
129 | $value = \trim($value, '@');
130 |
131 | return self::T_TYPE_PATTERN;
132 | }
133 |
134 | if ($this->isStringToken($value)) {
135 | $value = $this->extractStringValue($value);
136 |
137 | return self::T_STRING;
138 | }
139 |
140 | if ($this->isBooleanToken($value)) {
141 | $value = \strtolower($value);
142 |
143 | return self::T_BOOLEAN;
144 | }
145 |
146 | if ($this->isNullToken($value)) {
147 | $value = \strtolower($value);
148 |
149 | return self::T_NULL;
150 | }
151 |
152 | if (\is_numeric($value)) {
153 | return self::T_NUMBER;
154 | }
155 |
156 | if ($this->isExpanderNameToken($value)) {
157 | $value = \rtrim(\ltrim($value, '.'), '(');
158 |
159 | return self::T_EXPANDER_NAME;
160 | }
161 |
162 | return $type;
163 | }
164 |
165 | protected function isStringToken(string $value) : bool
166 | {
167 | return \in_array(\substr($value, 0, 1), ['"', "'"], true);
168 | }
169 |
170 | protected function isBooleanToken(string $value) : bool
171 | {
172 | return \in_array(\strtolower($value), ['true', 'false'], true);
173 | }
174 |
175 | protected function isNullToken(string $value) : bool
176 | {
177 | return \strtolower($value) === 'null';
178 | }
179 |
180 | protected function extractStringValue(string $value) : string
181 | {
182 | return \trim(\trim($value, "'"), '"');
183 | }
184 |
185 | protected function isExpanderNameToken(string $value) : bool
186 | {
187 | return \substr($value, -1) === '(' && \strlen($value) > 1;
188 | }
189 |
190 | protected function isTypePatternToken(string $value) : bool
191 | {
192 | return \substr($value, 0, 1) === '@' && \substr($value, \strlen($value) - 1, 1) === '@' && \strlen($value) > 1;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/Matcher/Pattern/Expander/Repeat.php:
--------------------------------------------------------------------------------
1 | pattern = $pattern;
38 | $this->isStrict = $isStrict;
39 | $this->isScalar = true;
40 |
41 | if (\is_string($pattern)) {
42 | $json = \json_decode($pattern, true);
43 |
44 | if ($json !== null && \json_last_error() === JSON_ERROR_NONE) {
45 | $this->pattern = $json;
46 | $this->isScalar = false;
47 | }
48 | } elseif (\is_array($pattern)) {
49 | $this->isScalar = false;
50 | } else {
51 | throw new \InvalidArgumentException('Repeat pattern must be a string or an array.');
52 | }
53 | }
54 |
55 | public static function is(string $name) : bool
56 | {
57 | return self::NAME === $name;
58 | }
59 |
60 | public function match($value) : bool
61 | {
62 | $this->backtrace->expanderEntrance(self::NAME, $value);
63 |
64 | if (!\is_array($value)) {
65 | $this->error = \sprintf('Repeat expander require "array", got "%s".', new StringConverter($value));
66 | $this->backtrace->expanderSucceed(self::NAME, $value);
67 |
68 | return false;
69 | }
70 |
71 | $factory = new MatcherFactory();
72 | $matcher = $factory->createMatcher($this->backtrace);
73 |
74 | if ($this->isScalar) {
75 | $result = $this->matchScalar($value, $matcher);
76 |
77 | if ($result) {
78 | $this->backtrace->expanderSucceed(self::NAME, $value);
79 | } else {
80 | $this->backtrace->expanderFailed(self::NAME, $value, '');
81 | }
82 |
83 | return $result;
84 | }
85 |
86 | $result = $this->matchJson($value, $matcher);
87 |
88 | if ($result) {
89 | $this->backtrace->expanderSucceed(self::NAME, $value);
90 | } else {
91 | $this->backtrace->expanderFailed(self::NAME, $value, '');
92 | }
93 |
94 | return $result;
95 | }
96 |
97 | public function getError() : ?string
98 | {
99 | return $this->error;
100 | }
101 |
102 | private function matchScalar(array $values, Matcher $matcher) : bool
103 | {
104 | foreach ($values as $index => $value) {
105 | $match = $matcher->match($value, $this->pattern);
106 |
107 | if (!$match) {
108 | $this->error = \sprintf('Repeat expander, entry n°%d, find error : %s', $index, $matcher->getError());
109 |
110 | return false;
111 | }
112 | }
113 |
114 | return true;
115 | }
116 |
117 | private function matchJson(array $values, Matcher $matcher) : bool
118 | {
119 | $patternKeys = \array_keys($this->pattern);
120 | $patternKeysLength = \count($patternKeys);
121 |
122 | foreach ($values as $index => $value) {
123 | $valueKeys = \array_keys($value);
124 | $valueKeysLength = \count($valueKeys);
125 |
126 | if ($this->isStrict && $patternKeysLength !== $valueKeysLength) {
127 | $this->error = \sprintf('Repeat expander expect to have %d keys in array but get : %d', $patternKeysLength, $valueKeysLength);
128 |
129 | return false;
130 | }
131 |
132 | foreach ($patternKeys as $key) {
133 | if (!\array_key_exists($key, $value)) {
134 | $this->error = \sprintf('Repeat expander, entry n°%d, require "array" to have key "%s".', $index, $key);
135 |
136 | return false;
137 | }
138 |
139 | $match = $matcher->match($value[$key], $this->pattern[$key]);
140 |
141 | if (!$match) {
142 | $this->error = \sprintf('Repeat expander, entry n°%d, key "%s", find error : %s', $index, $key, $matcher->getError());
143 |
144 | return false;
145 | }
146 | }
147 | }
148 |
149 | return true;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/Parser/ExpanderInitializer.php:
--------------------------------------------------------------------------------
1 | Pattern\Expander\After::class,
23 | Pattern\Expander\Before::NAME => Pattern\Expander\Before::class,
24 | Pattern\Expander\Contains::NAME => Pattern\Expander\Contains::class,
25 | Pattern\Expander\NotContains::NAME => Pattern\Expander\NotContains::class,
26 | Pattern\Expander\Count::NAME => Pattern\Expander\Count::class,
27 | Pattern\Expander\EndsWith::NAME => Pattern\Expander\EndsWith::class,
28 | Pattern\Expander\GreaterThan::NAME => Pattern\Expander\GreaterThan::class,
29 | Pattern\Expander\InArray::NAME => Pattern\Expander\InArray::class,
30 | Pattern\Expander\IsDateTime::NAME => Pattern\Expander\IsDateTime::class,
31 | Pattern\Expander\IsInDateFormat::NAME => Pattern\Expander\IsInDateFormat::class,
32 | Pattern\Expander\IsEmail::NAME => Pattern\Expander\IsEmail::class,
33 | Pattern\Expander\IsEmpty::NAME => Pattern\Expander\IsEmpty::class,
34 | Pattern\Expander\IsNotEmpty::NAME => Pattern\Expander\IsNotEmpty::class,
35 | Pattern\Expander\IsUrl::NAME => Pattern\Expander\IsUrl::class,
36 | Pattern\Expander\IsIp::NAME => Pattern\Expander\IsIp::class,
37 | Pattern\Expander\IsTzOffset::NAME => Pattern\Expander\IsTzOffset::class,
38 | Pattern\Expander\IsTzAbbreviation::NAME => Pattern\Expander\IsTzAbbreviation::class,
39 | Pattern\Expander\IsTzIdentifier::NAME => Pattern\Expander\IsTzIdentifier::class,
40 | Pattern\Expander\LowerThan::NAME => Pattern\Expander\LowerThan::class,
41 | Pattern\Expander\MatchRegex::NAME => Pattern\Expander\MatchRegex::class,
42 | Pattern\Expander\OneOf::NAME => Pattern\Expander\OneOf::class,
43 | Pattern\Expander\Optional::NAME => Pattern\Expander\Optional::class,
44 | Pattern\Expander\StartsWith::NAME => Pattern\Expander\StartsWith::class,
45 | Pattern\Expander\Repeat::NAME => Pattern\Expander\Repeat::class,
46 | Pattern\Expander\ExpanderMatch::NAME => Pattern\Expander\ExpanderMatch::class,
47 | Pattern\Expander\HasProperty::NAME => Pattern\Expander\HasProperty::class,
48 | ];
49 |
50 | private Backtrace $backtrace;
51 |
52 | public function __construct(Backtrace $backtrace)
53 | {
54 | $this->backtrace = $backtrace;
55 | }
56 |
57 | public function setExpanderDefinition(string $expanderName, string $expanderFQCN) : void
58 | {
59 | if (!\class_exists($expanderFQCN)) {
60 | throw new UnknownExpanderClassException(\sprintf('Class "%s" does not exists.', $expanderFQCN));
61 | }
62 |
63 | $this->expanderDefinitions[$expanderName] = $expanderFQCN;
64 | }
65 |
66 | public function hasExpanderDefinition(string $expanderName) : bool
67 | {
68 | return \array_key_exists($expanderName, $this->expanderDefinitions);
69 | }
70 |
71 | public function getExpanderDefinition(string $expanderName) : string
72 | {
73 | if (!$this->hasExpanderDefinition($expanderName)) {
74 | throw new InvalidArgumentException(\sprintf('Definition for "%s" expander does not exists.', $expanderName));
75 | }
76 |
77 | return $this->expanderDefinitions[$expanderName];
78 | }
79 |
80 | public function initialize(Expander $expanderNode) : PatternExpander
81 | {
82 | if (!\array_key_exists($expanderNode->getName(), $this->expanderDefinitions)) {
83 | throw new UnknownExpanderException(\sprintf('Unknown expander "%s"', $expanderNode->getName()));
84 | }
85 |
86 | $reflection = new \ReflectionClass($this->expanderDefinitions[$expanderNode->getName()]);
87 |
88 | if ($expanderNode->hasArguments()) {
89 | $arguments = [];
90 |
91 | foreach ($expanderNode->getArguments() as $argument) {
92 | $arguments[] = ($argument instanceof Expander)
93 | ? $this->initialize($argument)
94 | : $argument;
95 | }
96 |
97 | $expander = $reflection->newInstanceArgs($arguments);
98 | } else {
99 | $expander = $reflection->newInstance();
100 | }
101 |
102 | if (!$expander instanceof PatternExpander) {
103 | throw new InvalidExpanderTypeException();
104 | }
105 |
106 | $expander->setBacktrace($this->backtrace);
107 |
108 | return $expander;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Matcher/TextMatcher.php:
--------------------------------------------------------------------------------
1 | parser = $parser;
36 | $this->backtrace = $backtrace;
37 | }
38 |
39 | /**
40 | * {@inheritDoc}
41 | */
42 | public function match($value, $pattern) : bool
43 | {
44 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
45 |
46 | if (!\is_string($value)) {
47 | $this->error = \sprintf('%s "%s" is not a valid string.', \gettype($value), new StringConverter($value));
48 | $this->backtrace->matcherFailed(self::class, $value, $pattern, (string) $this->error);
49 |
50 | return false;
51 | }
52 |
53 | $patternRegex = $pattern;
54 | $patternsReplacedWithRegex = $this->replaceTypePatternsWithPlaceholders($patternRegex);
55 | $patternRegex = $this->prepareRegex($patternRegex);
56 |
57 | try {
58 | $patternRegex = $this->replacePlaceholderWithPatternRegexes($patternRegex, $patternsReplacedWithRegex);
59 | } catch (UnknownTypeException $unknownTypeException) {
60 | $this->error = \sprintf('Type pattern "%s" is not supported by TextMatcher.', $unknownTypeException->getType());
61 | $this->backtrace->matcherFailed(self::class, $value, $pattern, (string) $this->error);
62 |
63 | return false;
64 | }
65 |
66 | if (!\preg_match($patternRegex, $value, $matchedValues)) {
67 | $this->error = \sprintf('"%s" does not match "%s" pattern', $value, $pattern);
68 | $this->backtrace->matcherFailed(self::class, $value, $pattern, (string) $this->error);
69 |
70 | return false;
71 | }
72 |
73 | \array_shift($matchedValues); // remove matched string
74 |
75 | if (\count($patternsReplacedWithRegex) !== \count($matchedValues)) {
76 | $this->error = 'Unexpected TextMatcher error.';
77 | $this->backtrace->matcherFailed(self::class, $value, $pattern, (string) $this->error);
78 |
79 | return false;
80 | }
81 |
82 | foreach ($patternsReplacedWithRegex as $index => $typePattern) {
83 | if (!$typePattern->matchExpanders($matchedValues[$index])) {
84 | $this->error = $typePattern->getError();
85 | $this->backtrace->matcherFailed(self::class, $value, $pattern, (string) $this->error);
86 |
87 | return false;
88 | }
89 | }
90 |
91 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
92 |
93 | return true;
94 | }
95 |
96 | /**
97 | * {@inheritDoc}
98 | */
99 | public function canMatch($pattern) : bool
100 | {
101 | if (!\is_string($pattern)) {
102 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
103 |
104 | return false;
105 | }
106 |
107 | if (Json::isValidPattern($pattern)) {
108 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
109 |
110 | return false;
111 | }
112 |
113 | if (Xml::isValid($pattern)) {
114 | $this->backtrace->matcherCanMatch(self::class, $pattern, false);
115 |
116 | return false;
117 | }
118 |
119 | $this->backtrace->matcherCanMatch(self::class, $pattern, true);
120 |
121 | return true;
122 | }
123 |
124 | /**
125 | * Replace each type pattern (@string@.startsWith("lorem")) with placeholder, in order
126 | * to use preg_quote without destroying pattern & expanders.
127 | *
128 | * before replacement: "/users/@integer@.greaterThan(200)/active"
129 | * after replacement: "/users/__PLACEHOLDER0__/active"
130 | *
131 | * @param string $patternRegex
132 | *
133 | * @return array|TypePattern[]
134 | */
135 | private function replaceTypePatternsWithPlaceholders(string &$patternRegex) : array
136 | {
137 | $patternsReplacedWithRegex = [];
138 | \preg_match_all(self::PATTERN_REGEXP, $patternRegex, $matches);
139 |
140 | foreach ($matches[0] as $index => $typePatternString) {
141 | $typePattern = $this->parser->parse($typePatternString);
142 | $patternsReplacedWithRegex[] = $typePattern;
143 | $patternRegex = \str_replace(
144 | $typePatternString,
145 | \sprintf(self::PATTERN_REGEXP_PLACEHOLDER_TEMPLATE, $index),
146 | $patternRegex
147 | );
148 | }
149 |
150 | return $patternsReplacedWithRegex;
151 | }
152 |
153 | /**
154 | * Replace placeholders with type pattern regular expressions
155 | * before replacement: "/users/__PLACEHOLDER0__/active"
156 | * after replacement: "/^\/users\/(\-?[0-9]*)\/active$/".
157 | *
158 | * @param string $patternRegex
159 | *
160 | * @throws UnknownTypeException
161 | *
162 | * @return string
163 | */
164 | private function replacePlaceholderWithPatternRegexes(string $patternRegex, array $patternsReplacedWithRegex) : string
165 | {
166 | $regexConverter = new RegexConverter();
167 |
168 | foreach ($patternsReplacedWithRegex as $index => $typePattern) {
169 | $patternRegex = \str_replace(
170 | \sprintf(self::PATTERN_REGEXP_PLACEHOLDER_TEMPLATE, $index),
171 | $regexConverter->toRegex($typePattern),
172 | $patternRegex
173 | );
174 | }
175 |
176 | return $patternRegex;
177 | }
178 |
179 | /**
180 | * Prepare regular expression.
181 | *
182 | * @param string $patternRegex
183 | *
184 | * @return string
185 | */
186 | private function prepareRegex(string $patternRegex) : string
187 | {
188 | return '/^' . \preg_quote($patternRegex, '/') . '$/';
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | files()
5 | ->in([
6 | __DIR__ . '/src',
7 | __DIR__ . '/tests',
8 | ]);
9 |
10 | if (!\file_exists(__DIR__ . '/var')) {
11 | \mkdir(__DIR__ . '/var');
12 | }
13 |
14 | if (!\file_exists(__DIR__ . '/var')) {
15 | \mkdir(__DIR__ . '/var');
16 | }
17 |
18 | /**
19 | * This configuration was taken from https://github.com/sebastianbergmann/phpunit/blob/master/.php_cs.dist
20 | * and slightly adjusted.
21 | */
22 | $config = new PhpCsFixer\Config();
23 |
24 | return $config
25 | ->setRiskyAllowed(true)
26 | ->setCacheFile(__DIR__.'/var/.php_cs.cache')
27 | ->setRules([
28 | 'align_multiline_comment' => true,
29 | 'array_indentation' => true,
30 | 'array_syntax' => ['syntax' => 'short'],
31 | 'blank_line_after_namespace' => true,
32 | 'blank_line_before_statement' => [
33 | 'statements' => [
34 | 'break',
35 | 'continue',
36 | 'declare',
37 | 'default',
38 | 'do',
39 | 'exit',
40 | 'for',
41 | 'foreach',
42 | 'goto',
43 | 'if',
44 | 'include',
45 | 'include_once',
46 | 'require',
47 | 'require_once',
48 | 'return',
49 | 'switch',
50 | 'throw',
51 | 'try',
52 | 'while',
53 | ],
54 | ],
55 | 'braces' => true,
56 | 'cast_spaces' => true,
57 | 'class_attributes_separation' => ['elements' => ['const'=>'one', 'method'=>'one', 'property'=>'one']],
58 | 'combine_consecutive_issets' => true,
59 | 'combine_consecutive_unsets' => true,
60 | 'compact_nullable_typehint' => true,
61 | 'concat_space' => ['spacing' => 'one'],
62 | 'constant_case' => true,
63 | 'declare_equal_normalize' => ['space' => 'none'],
64 | 'declare_strict_types' => true,
65 | 'dir_constant' => true,
66 | 'elseif' => true,
67 | 'encoding' => true,
68 | 'echo_tag_syntax' => true,
69 | 'explicit_indirect_variable' => true,
70 | 'explicit_string_variable' => true,
71 | 'full_opening_tag' => true,
72 | 'fully_qualified_strict_types' => true,
73 | 'function_typehint_space' => true,
74 | 'function_declaration' => true,
75 | 'global_namespace_import' => [
76 | 'import_classes' => false,
77 | 'import_constants' => false,
78 | 'import_functions' => false,
79 | ],
80 | 'heredoc_to_nowdoc' => true,
81 | 'increment_style' => [
82 | 'style' => PhpCsFixer\Fixer\Operator\IncrementStyleFixer::STYLE_POST,
83 | ],
84 | 'indentation_type' => true,
85 | 'is_null' => true,
86 | 'line_ending' => true,
87 | 'list_syntax' => ['syntax' => 'short'],
88 | 'logical_operators' => true,
89 | 'lowercase_keywords' => true,
90 | 'lowercase_static_reference' => true,
91 | 'magic_constant_casing' => true,
92 | 'magic_method_casing' => true,
93 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'],
94 | 'modernize_types_casting' => false,
95 | 'multiline_comment_opening_closing' => true,
96 | 'multiline_whitespace_before_semicolons' => true,
97 | 'native_constant_invocation' => false,
98 | 'native_function_casing' => false,
99 | 'native_function_invocation' => ['include'=>['@all']],
100 | 'native_function_type_declaration_casing' => true,
101 | 'new_with_braces' => false,
102 | 'no_alias_functions' => true,
103 | 'no_alternative_syntax' => true,
104 | 'no_blank_lines_after_class_opening' => true,
105 | 'no_blank_lines_after_phpdoc' => true,
106 | 'no_blank_lines_before_namespace' => false,
107 | 'no_closing_tag' => true,
108 | 'no_empty_comment' => true,
109 | 'no_empty_phpdoc' => true,
110 | 'no_empty_statement' => true,
111 | 'no_extra_blank_lines' => true,
112 | 'no_homoglyph_names' => true,
113 | 'no_leading_import_slash' => true,
114 | 'no_leading_namespace_whitespace' => true,
115 | 'no_mixed_echo_print' => ['use' => 'print'],
116 | 'no_multiline_whitespace_around_double_arrow' => true,
117 | 'no_null_property_initialization' => true,
118 | 'no_php4_constructor' => true,
119 | 'no_short_bool_cast' => true,
120 | 'no_singleline_whitespace_before_semicolons' => true,
121 | 'no_spaces_after_function_name' => true,
122 | 'no_spaces_around_offset' => true,
123 | 'no_spaces_inside_parenthesis' => true,
124 | 'no_superfluous_elseif' => true,
125 | 'no_superfluous_phpdoc_tags' => false,
126 | 'no_trailing_comma_in_list_call' => true,
127 | 'no_trailing_comma_in_singleline_array' => true,
128 | 'no_trailing_whitespace' => true,
129 | 'no_trailing_whitespace_in_comment' => true,
130 | 'no_unneeded_control_parentheses' => true,
131 | 'no_unneeded_curly_braces' => true,
132 | 'no_unneeded_final_method' => true,
133 | 'no_unreachable_default_argument_value' => true,
134 | 'no_unset_on_property' => true,
135 | 'no_unused_imports' => true,
136 | 'no_useless_else' => true,
137 | 'no_useless_return' => true,
138 | 'no_whitespace_before_comma_in_array' => true,
139 | 'no_whitespace_in_blank_line' => true,
140 | 'non_printable_character' => true,
141 | 'normalize_index_brace' => true,
142 | 'object_operator_without_whitespace' => true,
143 | 'ordered_class_elements' => [
144 | 'order' => [
145 | 'use_trait',
146 | 'constant_public',
147 | 'constant_protected',
148 | 'constant_private',
149 | 'property_public_static',
150 | 'property_protected_static',
151 | 'property_private_static',
152 | 'property_public',
153 | 'property_protected',
154 | 'property_private',
155 | 'construct',
156 | 'method_public_static',
157 | 'destruct',
158 | 'magic',
159 | 'phpunit',
160 | 'method_public',
161 | 'method_protected',
162 | 'method_private',
163 | 'method_protected_static',
164 | 'method_private_static',
165 | ],
166 | ],
167 | 'ordered_imports' => [
168 | 'imports_order' => [
169 | PhpCsFixer\Fixer\Import\OrderedImportsFixer::IMPORT_TYPE_CONST,
170 | PhpCsFixer\Fixer\Import\OrderedImportsFixer::IMPORT_TYPE_FUNCTION,
171 | PhpCsFixer\Fixer\Import\OrderedImportsFixer::IMPORT_TYPE_CLASS,
172 | ]
173 | ],
174 | 'ordered_interfaces' => [
175 | 'direction' => 'ascend',
176 | 'order' => 'alpha',
177 | ],
178 | 'phpdoc_add_missing_param_annotation' => false,
179 | 'phpdoc_align' => ['align' => 'left'],
180 | 'phpdoc_annotation_without_dot' => true,
181 | 'phpdoc_indent' => true,
182 | 'phpdoc_no_access' => true,
183 | 'phpdoc_no_empty_return' => true,
184 | 'phpdoc_no_package' => true,
185 | 'phpdoc_order' => true,
186 | 'phpdoc_return_self_reference' => true,
187 | 'phpdoc_scalar' => true,
188 | 'phpdoc_separation' => true,
189 | 'phpdoc_single_line_var_spacing' => true,
190 | 'phpdoc_summary' => true,
191 | 'phpdoc_to_comment' => false,
192 | 'phpdoc_trim' => true,
193 | 'phpdoc_trim_consecutive_blank_line_separation' => true,
194 | 'phpdoc_types' => ['groups' => ['simple', 'meta']],
195 | 'phpdoc_types_order' => true,
196 | 'phpdoc_var_without_name' => true,
197 | 'pow_to_exponentiation' => true,
198 | 'protected_to_private' => true,
199 | 'return_assignment' => true,
200 | 'return_type_declaration' => ['space_before' => 'one'],
201 | 'self_accessor' => true,
202 | 'self_static_accessor' => true,
203 | 'semicolon_after_instruction' => true,
204 | 'set_type_to_cast' => true,
205 | 'short_scalar_cast' => true,
206 | 'simple_to_complex_string_variable' => true,
207 | 'simplified_null_return' => false,
208 | 'single_blank_line_at_eof' => true,
209 | 'single_import_per_statement' => true,
210 | 'single_line_after_imports' => true,
211 | 'single_quote' => true,
212 | 'standardize_not_equals' => true,
213 | 'strict_param' => true,
214 | 'ternary_to_null_coalescing' => true,
215 | 'trailing_comma_in_multiline' => true,
216 | 'trim_array_spaces' => true,
217 | 'unary_operator_spaces' => true,
218 | 'visibility_required' => [
219 | 'elements' => [
220 | 'const',
221 | 'method',
222 | 'property',
223 | ],
224 | ],
225 | 'void_return' => true,
226 | 'whitespace_after_comma_in_array' => true,
227 | ])
228 | ->setFinder($finder);
--------------------------------------------------------------------------------
/src/Parser.php:
--------------------------------------------------------------------------------
1 | lexer = $lexer;
27 | $this->expanderInitializer = $expanderInitializer;
28 | }
29 |
30 | public function hasValidSyntax(string $pattern) : bool
31 | {
32 | try {
33 | $this->getAST($pattern);
34 |
35 | return true;
36 | } catch (Exception $exception) {
37 | return false;
38 | }
39 | }
40 |
41 | public function parse(string $pattern) : Pattern\TypePattern
42 | {
43 | $ast = $this->getAST($pattern);
44 | $typePattern = new Pattern\TypePattern((string) $ast->getType());
45 |
46 | foreach ($ast->getExpanders() as $expander) {
47 | $typePattern->addExpander($this->expanderInitializer->initialize($expander));
48 | }
49 |
50 | return $typePattern;
51 | }
52 |
53 | public function getAST(string $pattern) : AST\Pattern
54 | {
55 | if ($pattern === '') {
56 | return new AST\Pattern(new AST\Type(''));
57 | }
58 |
59 | $this->lexer->setInput($pattern);
60 |
61 | return $this->getPattern();
62 | }
63 |
64 | private function getPattern() : AST\Pattern
65 | {
66 | $this->lexer->moveNext();
67 |
68 | if ($this->lexer->lookahead->type !== Lexer::T_TYPE_PATTERN) {
69 | throw PatternException::syntaxError(
70 | $this->unexpectedSyntaxError($this->lexer->lookahead, '@type@ pattern')
71 | );
72 | }
73 |
74 | $pattern = new AST\Pattern(new AST\Type($this->lexer->lookahead->value));
75 |
76 | $this->lexer->moveNext();
77 |
78 | if (!$this->endOfPattern()) {
79 | $this->addExpanderNodes($pattern);
80 | }
81 |
82 | return $pattern;
83 | }
84 |
85 | private function addExpanderNodes(AST\Pattern $pattern) : void
86 | {
87 | while (($expander = $this->getNextExpanderNode()) !== null) {
88 | $pattern->addExpander($expander);
89 | }
90 | }
91 |
92 | /**
93 | * Try to get next expander, return null if there is no expander left.
94 | */
95 | private function getNextExpanderNode() : ?AST\Expander
96 | {
97 | if ($this->endOfPattern()) {
98 | return null;
99 | }
100 |
101 | $expander = new AST\Expander($this->getExpanderName());
102 |
103 | /* @phpstan-ignore-next-line */
104 | if ($this->endOfPattern()) {
105 | $this->unexpectedEndOfString(')');
106 | }
107 |
108 | $this->addArgumentValues($expander);
109 |
110 | if ($this->endOfPattern()) {
111 | $this->unexpectedEndOfString(')');
112 | }
113 |
114 | if (!$this->isNextCloseParenthesis()) {
115 | throw PatternException::syntaxError($this->unexpectedSyntaxError($this->lexer->lookahead, ')'));
116 | }
117 |
118 | return $expander;
119 | }
120 |
121 | private function getExpanderName() : string
122 | {
123 | $lookahead = $this->lexer->lookahead;
124 |
125 | if ($lookahead === null) {
126 | throw PatternException::syntaxError($this->unexpectedSyntaxError($lookahead, '.expanderName(args) definition'));
127 | }
128 |
129 | if ($lookahead->type !== Lexer::T_EXPANDER_NAME) {
130 | throw PatternException::syntaxError($this->unexpectedSyntaxError($lookahead, '.expanderName(args) definition'));
131 | }
132 |
133 | $this->lexer->moveNext();
134 |
135 | return $lookahead->value;
136 | }
137 |
138 | /**
139 | * Add arguments to expander.
140 | */
141 | private function addArgumentValues(AST\Expander $expander) : void
142 | {
143 | while (($argument = $this->getNextArgumentValue()) !== null) {
144 | $coercedArgument = $this->coerceArgumentValue($argument);
145 |
146 | $expander->addArgument($coercedArgument);
147 |
148 | if (!$this->lexer->isNextToken(Lexer::T_COMMA)) {
149 | break;
150 | }
151 |
152 | $this->lexer->moveNext();
153 |
154 | if ($this->lexer->isNextToken(Lexer::T_CLOSE_PARENTHESIS)) {
155 | throw PatternException::syntaxError($this->unexpectedSyntaxError($this->lexer->lookahead, 'string, number, boolean or null argument'));
156 | }
157 | }
158 | }
159 |
160 | private function coerceArgumentValue(mixed $argument) : mixed
161 | {
162 | $coercedArgument = $argument;
163 |
164 | if (\is_string($argument)) {
165 | $lowercaseArgument = \strtolower($argument);
166 |
167 | if ($lowercaseArgument === self::NULL_VALUE) {
168 | $coercedArgument = null;
169 | } elseif ($lowercaseArgument === 'true') {
170 | $coercedArgument = true;
171 | } elseif ($lowercaseArgument === 'false') {
172 | $coercedArgument = false;
173 | } elseif (\is_numeric($argument)) {
174 | $coercedArgument = (\strpos($argument, '.') === false) ? (int) $argument : (float) $argument;
175 | }
176 | } elseif (\is_array($argument)) {
177 | $coercedArgument = [];
178 |
179 | foreach ($argument as $key => $arg) {
180 | $coercedArgument[$key] = $this->coerceArgumentValue($arg);
181 | }
182 | }
183 |
184 | return $coercedArgument;
185 | }
186 |
187 | /**
188 | * Try to get next argument. Return false if there are no arguments left before ")".
189 | */
190 | private function getNextArgumentValue()
191 | {
192 | $validArgumentTypes = [
193 | Lexer::T_STRING,
194 | Lexer::T_NUMBER,
195 | Lexer::T_BOOLEAN,
196 | Lexer::T_NULL,
197 | ];
198 |
199 | if ($this->lexer->isNextToken(Lexer::T_CLOSE_PARENTHESIS)) {
200 | return;
201 | }
202 |
203 | if ($this->lexer->isNextToken(Lexer::T_OPEN_CURLY_BRACE)) {
204 | return $this->getArrayArgument();
205 | }
206 |
207 | if ($this->lexer->isNextToken(Lexer::T_EXPANDER_NAME)) {
208 | return $this->getNextExpanderNode();
209 | }
210 |
211 | if (!$this->lexer->isNextTokenAny($validArgumentTypes)) {
212 | throw PatternException::syntaxError($this->unexpectedSyntaxError($this->lexer->lookahead, 'string, number, boolean or null argument'));
213 | }
214 |
215 | $tokenType = $this->lexer->lookahead->type;
216 | $argument = $this->lexer->lookahead->value;
217 | $this->lexer->moveNext();
218 |
219 | if ($tokenType === Lexer::T_NULL) {
220 | $argument = self::NULL_VALUE;
221 | }
222 |
223 | return $argument;
224 | }
225 |
226 | private function getArrayArgument() : array
227 | {
228 | $arrayArgument = [];
229 | $this->lexer->moveNext();
230 |
231 | while ($this->getNextArrayElement($arrayArgument) !== null) {
232 | $this->lexer->moveNext();
233 | }
234 |
235 | if (!$this->lexer->isNextToken(Lexer::T_CLOSE_CURLY_BRACE)) {
236 | throw PatternException::syntaxError($this->unexpectedSyntaxError($this->lexer->lookahead, '}'));
237 | }
238 |
239 | $this->lexer->moveNext();
240 |
241 | return $arrayArgument;
242 | }
243 |
244 | private function getNextArrayElement(array &$array) : ?bool
245 | {
246 | if ($this->lexer->isNextToken(Lexer::T_CLOSE_CURLY_BRACE)) {
247 | return null;
248 | }
249 |
250 | $key = $this->getNextArgumentValue();
251 |
252 | if ($key === self::NULL_VALUE) {
253 | $key = '';
254 | }
255 |
256 | if (!$this->lexer->isNextToken(Lexer::T_COLON)) {
257 | throw PatternException::syntaxError($this->unexpectedSyntaxError($this->lexer->lookahead, ':'));
258 | }
259 |
260 | $this->lexer->moveNext();
261 |
262 | $value = $this->getNextArgumentValue();
263 |
264 | if ($value === self::NULL_VALUE) {
265 | $value = null;
266 | }
267 |
268 | $array[$key] = $value;
269 |
270 | if (!$this->lexer->isNextToken(Lexer::T_COMMA)) {
271 | return null;
272 | }
273 |
274 | return true;
275 | }
276 |
277 | private function isNextCloseParenthesis() : bool
278 | {
279 | $isCloseParenthesis = $this->lexer->isNextToken(Lexer::T_CLOSE_PARENTHESIS);
280 | $this->lexer->moveNext();
281 |
282 | return $isCloseParenthesis;
283 | }
284 |
285 | /**
286 | * @param ?Token $unexpectedToken
287 | * @param string $expected
288 | */
289 | private function unexpectedSyntaxError($unexpectedToken, ?string $expected = null) : string
290 | {
291 | $tokenPos = $unexpectedToken !== null ? $unexpectedToken->position : '-1';
292 | $message = \sprintf('line 0, col %d: Error: ', $tokenPos);
293 | $message .= (isset($expected)) ? \sprintf('Expected "%s", got ', $expected) : 'Unexpected';
294 | $message .= \sprintf('"%s"', $unexpectedToken->value);
295 |
296 | return $message;
297 | }
298 |
299 | /**
300 | * @param string $expected
301 | *
302 | * @throws PatternException
303 | */
304 | private function unexpectedEndOfString(?string $expected = null) : void
305 | {
306 | $tokenPos = (isset($this->lexer->token->position))
307 | ? $this->lexer->token->position + \strlen((string) $this->lexer->token->value) : '-1';
308 | $message = \sprintf('line 0, col %d: Error: ', $tokenPos);
309 | $message .= (isset($expected)) ? \sprintf('Expected "%s", got end of string.', $expected) : 'Unexpected';
310 | $message .= 'end of string';
311 |
312 | throw PatternException::syntaxError($message);
313 | }
314 |
315 | private function endOfPattern() : bool
316 | {
317 | return null === $this->lexer->lookahead;
318 | }
319 | }
320 |
--------------------------------------------------------------------------------
/src/Matcher/ArrayMatcher.php:
--------------------------------------------------------------------------------
1 | diff = new Diff();
47 | $this->propertyMatcher = $propertyMatcher;
48 | $this->parser = $parser;
49 | $this->backtrace = $backtrace;
50 | }
51 |
52 | public function match($value, $pattern) : bool
53 | {
54 | $this->backtrace->matcherEntrance(self::class, $value, $pattern);
55 |
56 | if (parent::match($value, $pattern)) {
57 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
58 |
59 | return true;
60 | }
61 |
62 | if (!\is_array($value)) {
63 | $this->addValuePatternDifference($value, $pattern);
64 |
65 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());
66 |
67 | return false;
68 | }
69 |
70 | if ($this->isArrayPattern($pattern)) {
71 | return $this->allExpandersMatch($value, $pattern);
72 | }
73 |
74 | if (!$this->iterateMatch($value, $pattern)) {
75 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());
76 |
77 | return false;
78 | }
79 |
80 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
81 |
82 | return true;
83 | }
84 |
85 | public function canMatch($pattern) : bool
86 | {
87 | return \is_array($pattern) || $this->isArrayPattern($pattern);
88 | }
89 |
90 | public function getError() : ?string
91 | {
92 | if (!$this->diff->count()) {
93 | return null;
94 | }
95 |
96 | return \implode("\n", \array_map(fn (Difference $difference) : string => $difference->format(), $this->diff->all()));
97 | }
98 |
99 | public function clearError() : void
100 | {
101 | $this->diff = new Diff();
102 | }
103 |
104 | private function isArrayPattern($pattern) : bool
105 | {
106 | if (!\is_string($pattern)) {
107 | return false;
108 | }
109 |
110 | return $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is(self::PATTERN);
111 | }
112 |
113 | private function iterateMatch(array $values, array $patterns, string $parentPath = '') : bool
114 | {
115 | $pattern = null;
116 | $previousPattern = null;
117 |
118 | if (\in_array(self::ARRAY_PREVIOUS_PATTERN_REPEAT, $patterns, true)) {
119 | $patterns = \array_merge(
120 | \array_replace($patterns, [\array_search(self::ARRAY_PREVIOUS_PATTERN_REPEAT, $patterns, true) => self::ARRAY_PREVIOUS_PATTERN]),
121 | \array_fill(0, \count($values) - \count($patterns), self::ARRAY_PREVIOUS_PATTERN)
122 | );
123 | }
124 |
125 | foreach ($values as $key => $value) {
126 | $path = $this->formatAccessPath($key);
127 |
128 | if ($this->shouldSkipValueMatchingFor($pattern)) {
129 | continue;
130 | }
131 |
132 | if ($this->valueExist($path, $patterns)) {
133 | $pattern = $this->getValueByPath($patterns, $path);
134 | } elseif (isset($patterns[self::UNIVERSAL_KEY])) {
135 | $pattern = $patterns[self::UNIVERSAL_KEY];
136 | } else {
137 | $this->setMissingElementInError('pattern', $this->formatFullPath($parentPath, $path));
138 |
139 | return false;
140 | }
141 |
142 | if ($pattern === self::ARRAY_PREVIOUS_PATTERN) {
143 | $pattern = $previousPattern;
144 | }
145 |
146 | if ($this->shouldSkipValueMatchingFor($pattern)) {
147 | continue;
148 | }
149 |
150 | if ($this->valueMatchPattern($value, $pattern, $this->formatFullPath($parentPath, $path))) {
151 | continue;
152 | }
153 |
154 | if (!\is_array($value)) {
155 | return false;
156 | }
157 |
158 | if (!$this->canMatch($pattern)) {
159 | $this->addValuePatternDifference($value, $parentPath, $this->formatFullPath($parentPath, $path));
160 |
161 | return false;
162 | }
163 |
164 | if ($this->isArrayPattern($pattern)) {
165 | if (!$this->allExpandersMatch($value, $pattern, $parentPath)) {
166 | $this->addValuePatternDifference($value, $parentPath, $this->formatFullPath($parentPath, $path));
167 |
168 | return false;
169 | }
170 |
171 | continue;
172 | }
173 |
174 | if (!$this->iterateMatch($value, $pattern, $this->formatFullPath($parentPath, $path))) {
175 | return false;
176 | }
177 |
178 | $previousPattern = $pattern;
179 | }
180 |
181 | return $this->isPatternValid($patterns, $values, $parentPath);
182 | }
183 |
184 | private function isPatternValid(array $pattern, array $values, string $parentPath) : bool
185 | {
186 | $skipPattern = self::UNBOUNDED_PATTERN;
187 |
188 | $pattern = \array_filter(
189 | $pattern,
190 | fn ($item) => $item !== $skipPattern
191 | );
192 |
193 | $notExistingKeys = $this->findNotExistingKeys($pattern, $values);
194 |
195 | if (\count($notExistingKeys) > 0) {
196 | $keyNames = \array_keys($notExistingKeys);
197 | $path = $this->formatFullPath($parentPath, $this->formatAccessPath($keyNames[0]));
198 | $this->setMissingElementInError('value', $path);
199 |
200 | return false;
201 | }
202 |
203 | return true;
204 | }
205 |
206 | /**
207 | * @return mixed[]
208 | */
209 | private function findNotExistingKeys(array $patterns, array $values) : array
210 | {
211 | $notExistingKeys = \array_diff_key($patterns, $values);
212 |
213 | return \array_filter($notExistingKeys, function ($pattern, $key) use ($values) : bool {
214 | if ($key === self::UNIVERSAL_KEY) {
215 | return false;
216 | }
217 |
218 | if (\is_array($pattern)) {
219 | return empty($pattern) || !$this->match($values, $pattern);
220 | }
221 |
222 | try {
223 | $typePattern = $this->parser->parse($pattern);
224 | } catch (\Throwable) {
225 | return true;
226 | }
227 |
228 | return !$typePattern->hasExpander('optional');
229 | }, ARRAY_FILTER_USE_BOTH);
230 | }
231 |
232 | private function valueMatchPattern($value, $pattern, $parentPath) : bool
233 | {
234 | $match = $this->propertyMatcher->canMatch($pattern) &&
235 | $this->propertyMatcher->match($value, $pattern);
236 |
237 | if (!$match) {
238 | if (!\is_array($value)) {
239 | $this->addValuePatternDifference($value, $pattern, $parentPath);
240 | }
241 | }
242 |
243 | return $match;
244 | }
245 |
246 | private function valueExist(string $path, array $haystack) : bool
247 | {
248 | return $this->arrayPropertyExists($this->getKeyFromAccessPath($path), $haystack);
249 | }
250 |
251 | private function arrayPropertyExists(string $property, array $objectOrArray) : bool
252 | {
253 | return isset($objectOrArray[$property]) ||
254 | \array_key_exists($property, $objectOrArray);
255 | }
256 |
257 | private function getValueByPath(array $array, string $path)
258 | {
259 | return $array[$this->getKeyFromAccessPath($path)];
260 | }
261 |
262 | private function setMissingElementInError(string $place, string $path) : void
263 | {
264 | $this->diff = $this->diff->add(new StringDifference(\sprintf('There is no element under path %s in %s.', $path, $place)));
265 | }
266 |
267 | private function formatAccessPath($key) : string
268 | {
269 | return \sprintf('[%s]', $key);
270 | }
271 |
272 | private function getKeyFromAccessPath(string $path) : string
273 | {
274 | return \substr($path, 1, -1);
275 | }
276 |
277 | private function formatFullPath(string $parentPath, string $path) : string
278 | {
279 | return \sprintf('%s%s', $parentPath, $path);
280 | }
281 |
282 | private function shouldSkipValueMatchingFor($lastPattern) : bool
283 | {
284 | return $lastPattern === self::UNBOUNDED_PATTERN;
285 | }
286 |
287 | private function allExpandersMatch($value, $pattern, $parentPath = '') : bool
288 | {
289 | $typePattern = $this->parser->parse($pattern);
290 |
291 | if (!$typePattern->matchExpanders($value)) {
292 | $this->addValuePatternDifference($value, $pattern, $parentPath);
293 |
294 | $this->backtrace->matcherFailed(self::class, $value, $pattern, $this->getError());
295 |
296 | return false;
297 | }
298 |
299 | $this->backtrace->matcherSucceed(self::class, $value, $pattern);
300 |
301 | return true;
302 | }
303 |
304 | private function addValuePatternDifference($value, $pattern, string $path = '') : void
305 | {
306 | $this->diff = $this->diff->add(new ValuePatternDifference(
307 | (string) new StringConverter($value),
308 | (string) new StringConverter($pattern),
309 | $path ? $path : 'root'
310 | ));
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Matcher
2 |
3 | [](https://shepherd.dev/coduo/php-matcher)
4 |
5 | Library created for testing all kinds of JSON/XML/TXT/Scalar values against patterns.
6 |
7 | API:
8 |
9 | ```php
10 | PHPMatcher::match($value = '{"foo": "bar"}', $pattern = '{"foo": "@string@"}') : bool;
11 | PHPMatcher::backtrace() : Backtrace;
12 | PHPMatcher::error() : ?string;
13 | ```
14 |
15 | It was built to simplify API's functional testing.
16 |
17 | * [](https://github.com/coduo/php-matcher/actions/workflows/test-suite.yml) - [6.x README](https://github.com/coduo/php-matcher/tree/6.x/README.md) PHP >= 8.3 <= 8.5
18 | * [5.x README](https://github.com/coduo/php-matcher/tree/5.x/README.md) PHP >= 7.2 < 8.0
19 | * [5.0 README](https://github.com/coduo/php-matcher/tree/5.0/README.md) PHP >= 7.2 < 8.0
20 | * [4.0.* README](https://github.com/coduo/php-matcher/tree/4.0/README.md) PHP >= 7.2 < 8.0
21 | * [3.2.* README](https://github.com/coduo/php-matcher/tree/3.2/README.md) PHP >= 7.0 < 8.0
22 | * [3.1.* README](https://github.com/coduo/php-matcher/tree/3.1/README.md) PHP >= 7.0 < 8.0
23 |
24 | [](https://packagist.org/packages/coduo/php-matcher)
25 | [](https://packagist.org/packages/coduo/php-matcher)
26 | [](https://packagist.org/packages/coduo/php-matcher)
27 | [](https://packagist.org/packages/coduo/php-matcher)
28 |
29 | ## Sandbox
30 |
31 | Feel free to play first with [Sandbox](https://php-matcher.norbert.tech/)
32 |
33 | ## Installation
34 |
35 | Require new dev dependency using composer:
36 |
37 | ```
38 | composer require --dev "coduo/php-matcher"
39 | ```
40 |
41 | ## Basic usage
42 |
43 | ### Direct PHPMatcher usage
44 |
45 | ```php
46 | match("lorem ipsum dolor", "@string@");
52 |
53 | if (!$match) {
54 | echo "Error: " . $matcher->error();
55 | echo "Backtrace: \n";
56 | echo (string) $matcher->backtrace();
57 | }
58 | ```
59 |
60 | ### PHPUnit extending PHPMatcherTestCase
61 |
62 | ```php
63 | assertMatchesPattern('{"name": "@string@"}', '{"name": "Norbert"}');
72 | }
73 | }
74 | ```
75 |
76 | ### PHPUnit using PHPMatcherAssertions trait
77 |
78 | ```php
79 | assertMatchesPattern('{"name": "@string@"}', '{"name": "Norbert"}');
91 | }
92 | }
93 | ```
94 |
95 | ### Available patterns
96 |
97 | * ``@string@``
98 | * ``@integer@``
99 | * ``@number@``
100 | * ``@double@``
101 | * ``@boolean@``
102 | * ``@time@``
103 | * ``@date@``
104 | * ``@datetime@``
105 | * ``@timezone@`` || ``@tz``
106 | * ``@array@``
107 | * ``@array_previous@`` - match next array element using pattern from previous element
108 | * ``@array_previous_repeat@`` - match all remaining array elements using pattern from previous element
109 | * ``@...@`` - *unbounded array*, once used matcher will skip any further array elements
110 | * ``@null@``
111 | * ``@*@`` || ``@wildcard@``
112 | * ``expr(expression)`` - **optional**, requires `symfony/expression-language: ^2.3|^3.0|^4.0|^5.0` to be present
113 | * ``@uuid@``
114 | * ``@ulid@``
115 | * ``@json@``
116 | * ``@string@||@integer@`` - string OR integer
117 |
118 | ### Available pattern expanders
119 |
120 | * ``startsWith($stringBeginning, $ignoreCase = false)``
121 | * ``endsWith($stringEnding, $ignoreCase = false)``
122 | * ``contains($string, $ignoreCase = false)``
123 | * ``notContains($string, $ignoreCase = false)``
124 | * ``isDateTime()``
125 | * ``isInDateFormat($format)`` - example `"@datetime@.isInDateFormat('Y-m-d H:i:s')`
126 | * ``before(string $date)`` - example ``"@string@.isDateTime().before(\"2020-01-01 00:00:00\")"``
127 | * ``after(string $date)`` - example ``"@string@.isDateTime().after(\"2020-01-01 00:00:00\")"``
128 | * ``isTzOffset()``
129 | * ``isTzIdentifier()``
130 | * ``isTzAbbreviation()``
131 | * ``isEmail()``
132 | * ``isUrl()``
133 | * ``isIp()``
134 | * ``isEmpty()``
135 | * ``isNotEmpty()``
136 | * ``lowerThan($boundry)``
137 | * ``greaterThan($boundry)``
138 | * ``inArray($value)`` - example ``"@array@.inArray(\"ROLE_USER\")"``
139 | * ``hasProperty($propertyName)`` - example ``"@json@.hasProperty(\"property_name\")"``
140 | * ``oneOf(...$expanders)`` - example ``"@string@.oneOf(contains('foo'), contains('bar'), contains('baz'))"``
141 | * ``matchRegex($regex)`` - example ``"@string@.matchRegex('/^lorem.+/')"``
142 | * ``optional()`` - work's only with ``ArrayMatcher``, ``JsonMatcher`` and ``XmlMatcher``
143 | * ``count()`` - work's only with ``ArrayMatcher`` - example ``"@array@.count(5)"``
144 | * ``repeat($pattern, $isStrict = true)`` - example ``'@array@.repeat({"name": "foe"})'`` or ``"@array@.repeat('@string@')"``
145 | * ``match($pattern)`` - example ``{"image":"@json@.match({\"url\":\"@string@.isUrl()\"})"}``
146 |
147 | ## Example usage
148 |
149 | ### Scalar matching
150 |
151 | ```php
152 | match(1, 1);
159 | $matcher->match('string', 'string');
160 | ```
161 |
162 | ### String matching
163 |
164 | ```php
165 | match('Norbert', '@string@');
172 | $matcher->match("lorem ipsum dolor", "@string@.startsWith('lorem').contains('ipsum').endsWith('dolor')");
173 |
174 | ```
175 |
176 | ### Time matching
177 |
178 | ```php
179 | match('00:00:00', '@time@');
186 | $matcher->match('00:01:00.000000', '@time@');
187 | $matcher->match('00:01:00', '@time@.after("00:00:00")');
188 | $matcher->match('00:00:00', '@time@.before("01:00:00")');
189 |
190 | ```
191 |
192 | ### Date matching
193 |
194 | ```php
195 | match('2014-08-19', '@date@');
202 | $matcher->match('2020-01-11', '@date@');
203 | $matcher->match('2014-08-19', '@date@.before("2016-08-19")');
204 | $matcher->match('2014-08-19', '@date@.before("today").after("+ 100year")');
205 |
206 | ```
207 |
208 | ### DateTime matching
209 |
210 | ```php
211 | match('2014-08-19', '@datetime@');
218 | $matcher->match('2020-01-11 00:00:00', '@datetime@');
219 | $matcher->match('2014-08-19', '@datetime@.before("2016-08-19")');
220 | $matcher->match('2014-08-19', '@datetime@.before("today").after("+ 100year")');
221 |
222 | ```
223 |
224 | ### TimeZone matching
225 |
226 | ```php
227 | match('Europe/Warsaw', '@timezone@');
234 | $matcher->match('Europe/Warsaw', '@tz@');
235 | $matcher->match('GMT', '@tz@');
236 | $matcher->match('01:00', '@tz@');
237 | $matcher->match('01:00', '@tz@.isTzOffset()');
238 | $matcher->match('GMT', '@tz@.isTzAbbreviation()');
239 | $matcher->match('Europe/Warsaw', '@tz@.isTzIdentifier()');
240 | ```
241 |
242 | ### Integer matching
243 |
244 | ```php
245 | match(100, '@integer@');
252 | $matcher->match(100, '@integer@.lowerThan(200).greaterThan(10)');
253 |
254 | ```
255 |
256 | ### Number matching
257 |
258 | ```php
259 | match(100, '@number@');
266 | $matcher->match('200', '@number@');
267 | $matcher->match(1.25, '@number@');
268 | $matcher->match('1.25', '@number@');
269 | $matcher->match(0b10100111001, '@number@');
270 | ```
271 |
272 | ### Double matching
273 |
274 | ```php
275 | match(10.1, "@double@");
282 | $matcher->match(10.1, "@double@.lowerThan(50.12).greaterThan(10)");
283 | ```
284 |
285 | ### Boolean matching
286 |
287 | ```php
288 | match(true, "@boolean@");
295 | $matcher->match(false, "@boolean@");
296 | ```
297 |
298 | ### Wildcard matching
299 |
300 | ```php
301 | match("@integer@", "@*@");
308 | $matcher->match("foobar", "@*@");
309 | $matcher->match(true, "@*@");
310 | $matcher->match(6.66, "@*@");
311 | $matcher->match(array("bar"), "@wildcard@");
312 | $matcher->match(new \stdClass, "@wildcard@");
313 | ```
314 |
315 | ### Expression matching
316 |
317 | ```php
318 | match(new \DateTime('2014-04-01'), "expr(value.format('Y-m-d') == '2014-04-01'");
325 | $matcher->match("Norbert", "expr(value === 'Norbert')");
326 | ```
327 |
328 | ### UUID matching
329 |
330 | ```php
331 | match('9f4db639-0e87-4367-9beb-d64e3f42ae18', '@uuid@');
338 | ```
339 |
340 | ### ULID matching
341 |
342 | ```php
343 | match('01BX5ZZKBKACTAV9WEVGEMMVS0', '@ulid@');
350 | ```
351 |
352 | ### Array matching
353 |
354 | ```php
355 | match(
362 | array(
363 | 'users' => array(
364 | array(
365 | 'id' => 1,
366 | 'firstName' => 'Norbert',
367 | 'lastName' => 'Orzechowicz',
368 | 'roles' => array('ROLE_USER'),
369 | 'position' => 'Developer',
370 | ),
371 | array(
372 | 'id' => 2,
373 | 'firstName' => 'Michał',
374 | 'lastName' => 'Dąbrowski',
375 | 'roles' => array('ROLE_USER')
376 | ),
377 | array(
378 | 'id' => 3,
379 | 'firstName' => 'Johnny',
380 | 'lastName' => 'DąbrowsBravoki',
381 | 'roles' => array('ROLE_HANDSOME_GUY')
382 | )
383 | ),
384 | true,
385 | 6.66
386 | ),
387 | array(
388 | 'users' => array(
389 | array(
390 | 'id' => '@integer@.greaterThan(0)',
391 | 'firstName' => '@string@',
392 | 'lastName' => 'Orzechowicz',
393 | 'roles' => '@array@',
394 | 'position' => '@string@.optional()'
395 | ),
396 | array(
397 | 'id' => '@integer@',
398 | 'firstName' => '@string@',
399 | 'lastName' => 'Dąbrowski',
400 | 'roles' => '@array@'
401 | ),
402 | '@...@'
403 | ),
404 | '@boolean@',
405 | '@double@'
406 | )
407 | );
408 | ```
409 |
410 | ### Array Previous
411 |
412 | > @array_previous@ can also be used when matching JSON's and XML's
413 |
414 | ```php
415 | match(
422 | array(
423 | 'users' => array(
424 | array(
425 | 'id' => 1,
426 | 'firstName' => 'Norbert',
427 | 'lastName' => 'Orzechowicz',
428 | 'roles' => array('ROLE_USER'),
429 | 'position' => 'Developer',
430 | ),
431 | array(
432 | 'id' => 2,
433 | 'firstName' => 'Michał',
434 | 'lastName' => 'Dąbrowski',
435 | 'roles' => array('ROLE_USER')
436 | ),
437 | array(
438 | 'id' => 3,
439 | 'firstName' => 'Johnny',
440 | 'lastName' => 'DąbrowsBravoki',
441 | 'roles' => array('ROLE_HANDSOME_GUY')
442 | )
443 | ),
444 | true,
445 | 6.66
446 | ),
447 | array(
448 | 'users' => array(
449 | array(
450 | 'id' => '@integer@.greaterThan(0)',
451 | 'firstName' => '@string@',
452 | 'lastName' => 'Orzechowicz',
453 | 'roles' => '@array@',
454 | 'position' => '@string@.optional()'
455 | ),
456 | '@array_previous@',
457 | '@array_previous@'
458 | ),
459 | '@boolean@',
460 | '@double@'
461 | )
462 | );
463 | ```
464 |
465 | ### Array Previous Repeat
466 |
467 | > @array_previous_repeat@ can also be used when matching JSON's and XML's
468 |
469 | ```php
470 | match(
477 | array(
478 | 'users' => array(
479 | array(
480 | 'id' => 1,
481 | 'firstName' => 'Norbert',
482 | 'lastName' => 'Orzechowicz',
483 | 'roles' => array('ROLE_USER'),
484 | 'position' => 'Developer',
485 | ),
486 | array(
487 | 'id' => 2,
488 | 'firstName' => 'Michał',
489 | 'lastName' => 'Dąbrowski',
490 | 'roles' => array('ROLE_USER')
491 | ),
492 | array(
493 | 'id' => 3,
494 | 'firstName' => 'Johnny',
495 | 'lastName' => 'DąbrowsBravoki',
496 | 'roles' => array('ROLE_HANDSOME_GUY')
497 | )
498 | ),
499 | true,
500 | 6.66
501 | ),
502 | array(
503 | 'users' => array(
504 | array(
505 | 'id' => '@integer@.greaterThan(0)',
506 | 'firstName' => '@string@',
507 | 'lastName' => 'Orzechowicz',
508 | 'roles' => '@array@',
509 | 'position' => '@string@.optional()'
510 | ),
511 | '@array_previous_repeat@'
512 | ),
513 | '@boolean@',
514 | '@double@'
515 | )
516 | );
517 | ```
518 |
519 | ### Json matching
520 |
521 | ```php
522 | match(
529 | '{
530 | "users":[
531 | {
532 | "firstName": "Norbert",
533 | "lastName": "Orzechowicz",
534 | "created": "2014-01-01",
535 | "roles":["ROLE_USER", "ROLE_DEVELOPER"]
536 | }
537 | ]
538 | }',
539 | '{
540 | "users":[
541 | {
542 | "firstName": "@string@",
543 | "lastName": "@string@",
544 | "created": "@string@.isDateTime()",
545 | "roles": "@array@",
546 | "position": "@string@.optional()"
547 | }
548 | ]
549 | }'
550 | );
551 | ```
552 |
553 | ### Json matching with unbounded arrays and objects
554 |
555 | ```php
556 | match(
563 | '{
564 | "users":[
565 | {
566 | "firstName": "Norbert",
567 | "lastName": "Orzechowicz",
568 | "created": "2014-01-01",
569 | "roles":["ROLE_USER", "ROLE_DEVELOPER"],
570 | "attributes": {
571 | "isAdmin": false,
572 | "dateOfBirth": null,
573 | "hasEmailVerified": true
574 | },
575 | "avatar": {
576 | "url": "http://avatar-image.com/avatar.png"
577 | }
578 | },
579 | {
580 | "firstName": "Michał",
581 | "lastName": "Dąbrowski",
582 | "created": "2014-01-01",
583 | "roles":["ROLE_USER", "ROLE_DEVELOPER", "ROLE_ADMIN"],
584 | "attributes": {
585 | "isAdmin": true,
586 | "dateOfBirth": null,
587 | "hasEmailVerified": true
588 | },
589 | "avatar": null
590 | }
591 | ]
592 | }',
593 | '{
594 | "users":[
595 | {
596 | "firstName": "@string@",
597 | "lastName": "@string@",
598 | "created": "@string@.isDateTime()",
599 | "roles": [
600 | "ROLE_USER",
601 | "@...@"
602 | ],
603 | "attributes": {
604 | "isAdmin": @boolean@,
605 | "@*@": "@*@"
606 | },
607 | "avatar": "@json@.match({\"url\":\"@string@.isUrl()\"})"
608 | }
609 | ,
610 | @...@
611 | ]
612 | }'
613 | );
614 | ```
615 |
616 | ### Xml matching
617 |
618 | **Optional** - requires `openlss/lib-array2xml: ^1.0` to be present.
619 |
620 | ```php
621 | match(<<
629 |
632 |
633 |
634 |
635 | IBM
636 | Any Value
637 |
638 |
639 |
640 |
641 | XML
642 | ,
643 | <<
645 |
648 |
649 |
650 |
651 | @string@
652 | @string@
653 | @integer@.optional()
654 |
655 |
656 |
657 |
658 | XML
659 | );
660 | ```
661 |
662 | Example scenario for api in behat using mongo.
663 | ---
664 | ``` cucumber
665 | @profile, @user
666 | Feature: Listing user toys
667 |
668 | As a user
669 | I want to list my toys
670 |
671 | Background:
672 | Given I send and accept JSON
673 |
674 | Scenario: Listing toys
675 | Given the following users exist:
676 | | firstName | lastName |
677 | | Chuck | Norris |
678 |
679 | And the following toys user "Chuck Norris" exist:
680 | | name |
681 | | Barbie |
682 | | GI Joe |
683 | | Optimus Prime |
684 |
685 | When I set valid authorization code oauth header for user "Chuck Norris"
686 | And I send a GET request on "/api/toys"
687 | Then the response status code should be 200
688 | And the JSON response should match:
689 | """
690 | [
691 | {
692 | "id": "@string@",
693 | "name": "Barbie",
694 | "_links: "@*@"
695 | },
696 | {
697 | "id": "@string@",
698 | "name": "GI Joe",
699 | "_links": "@*@"
700 | },
701 | {
702 | "id": "@string@",
703 | "name": "Optimus Prime",
704 | "_links": "@*@"
705 | }
706 | ]
707 | """
708 | ```
709 |
710 | ## PHPUnit integration
711 |
712 | The `assertMatchesPattern()` is a handy assertion that matches values in PHPUnit tests.
713 | To use it either include the `Coduo\PHPMatcher\PHPUnit\PHPMatcherAssertions` trait,
714 | or extend the `Coduo\PHPMatcher\PHPUnit\PHPMatcherTestCase`:
715 |
716 | ```php
717 | namespace Coduo\PHPMatcher\Tests\PHPUnit;
718 |
719 | use Coduo\PHPMatcher\PHPUnit\PHPMatcherAssertions;
720 | use PHPUnit\Framework\TestCase;
721 |
722 | class PHPMatcherAssertionsTest extends TestCase
723 | {
724 | use PHPMatcherAssertions;
725 |
726 | public function test_it_asserts_if_a_value_matches_the_pattern()
727 | {
728 | $this->assertMatchesPattern('@string@', 'foo');
729 | }
730 | }
731 | ```
732 |
733 | The `matchesPattern()` method can be used in PHPUnit stubs or mocks:
734 |
735 | ```php
736 | $mock = $this->createMock(Foo::class);
737 | $mock->method('bar')
738 | ->with($this->matchesPattern('@string@'))
739 | ->willReturn('foo');
740 | ```
741 |
742 | ## License
743 |
744 | This library is distributed under the MIT license. Please see the LICENSE file.
745 |
746 | ## Credits
747 |
748 | This lib was inspired by [JSON Expressions gem](https://github.com/chancancode/json_expressions) &&
749 | [Behat RestExtension ](https://github.com/jakzal/RestExtension)
750 |
--------------------------------------------------------------------------------