Skip to content

Commit 1eb8e61

Browse files
authored
Merge pull request #39 from magento-research/feature/conditional-resolver
Conditional Resolver
2 parents df1b1fa + cde0db0 commit 1eb8e61

9 files changed

+452
-11
lines changed

src/AbstractKeyValueStore.php

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
use Zend\Stdlib\ArrayUtils;
1212

13-
abstract class AbstractKeyValueStore implements \JsonSerializable, \Countable
13+
abstract class AbstractKeyValueStore implements \JsonSerializable, \Countable, \Iterator
1414
{
1515
/**
1616
* @var array
@@ -27,6 +27,11 @@ public function count(): int
2727
return \count($this->data);
2828
}
2929

30+
public function current()
31+
{
32+
return $this->get($this->key());
33+
}
34+
3035
/**
3136
* Get value for a key.
3237
*
@@ -38,13 +43,13 @@ public function count(): int
3843
*/
3944
public function get($lookup)
4045
{
41-
if (empty($lookup)) {
46+
if (empty($lookup) && $lookup != 0) {
4247
return;
4348
}
4449

4550
$value = $this->data;
4651

47-
foreach (explode('.', $lookup) as $segment) {
52+
foreach (explode('.', (string) $lookup) as $segment) {
4853
if (\is_array($value) && array_key_exists($segment, $value)) {
4954
$value = $value[$segment];
5055
} else {
@@ -72,7 +77,7 @@ public function has($lookup): bool
7277
{
7378
$subArray = $this->data;
7479

75-
foreach (explode('.', $lookup) as $segment) {
80+
foreach (explode('.', (string) $lookup) as $segment) {
7681
if (\is_array($subArray) && array_key_exists($segment, $subArray)) {
7782
$subArray = $subArray[$segment];
7883
} else {
@@ -101,6 +106,21 @@ public function jsonSerialize()
101106
return $this->toArray();
102107
}
103108

109+
public function key()
110+
{
111+
return key($this->data);
112+
}
113+
114+
public function next(): void
115+
{
116+
next($this->data);
117+
}
118+
119+
public function rewind(): void
120+
{
121+
reset($this->data);
122+
}
123+
104124
/**
105125
* Assign a new key in store.
106126
*
@@ -159,4 +179,9 @@ public function toArray(): array
159179
{
160180
return $this->data;
161181
}
182+
183+
public function valid(): bool
184+
{
185+
return $this->key() !== null && $this->has($this->key());
186+
}
162187
}

src/Context.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ class Context extends AbstractKeyValueStore
3232
'hex',
3333
];
3434

35+
/** @var string[] */
36+
private $unsetOnClone = [];
37+
38+
/**
39+
* Do not propagate non-cloneable keys.
40+
*/
41+
public function __clone()
42+
{
43+
foreach ($this->unsetOnClone as $key) {
44+
unset($this->data[$key]);
45+
}
46+
}
47+
3548
/**
3649
* Instantiate a Context from a Zend Http Request.
3750
*/
@@ -100,12 +113,16 @@ public function isStatusCode($value): bool
100113
*
101114
* {@inheritdoc}
102115
*/
103-
public function set(string $lookup, $value): void
116+
public function set(string $lookup, $value, bool $cloneable = true): void
104117
{
105118
if ($this->isBuiltinValue($lookup)) {
106119
throw new \RuntimeException('Cannot override a builtin value.');
107120
}
108121

122+
if (!$cloneable) {
123+
$this->unsetOnClone[] = $lookup;
124+
}
125+
109126
parent::set($lookup, $value);
110127
}
111128

src/DefinitionIterator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ public function __construct(Definition $rootDefinition, Context $context)
3131
$this->context = $context;
3232
}
3333

34+
/**
35+
* Make a copy of Context in clone.
36+
*/
37+
public function __clone()
38+
{
39+
$this->context = clone $this->context;
40+
}
41+
3442
/**
3543
* Travserse the Definition for a value, using a resolver if necessary.
3644
*
@@ -93,6 +101,11 @@ public function get($lookup, $definition = null)
93101
return $value;
94102
}
95103

104+
public function getContext(): Context
105+
{
106+
return $this->context;
107+
}
108+
96109
public function getRootDefinition(): Definition
97110
{
98111
return $this->rootDefinition;

src/Resolver/Conditional.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Upward\Resolver;
10+
11+
use Magento\Upward\Definition;
12+
13+
class Conditional extends AbstractResolver
14+
{
15+
/**
16+
* {@inheritdoc}
17+
*/
18+
public function getIndicator(): string
19+
{
20+
return 'when';
21+
}
22+
23+
/**
24+
* {@inheritdoc}
25+
*/
26+
public function isValid(Definition $definition): bool
27+
{
28+
if (!$definition->has($this->getIndicator())) {
29+
return false;
30+
}
31+
32+
if (!$definition->has('default')) {
33+
return false;
34+
}
35+
36+
foreach ($definition->get($this->getIndicator()) as $matcher) {
37+
if (!$matcher instanceof Definition) {
38+
return false;
39+
}
40+
41+
if (!$matcher->has('matches') || !\is_string($matcher->get('matches'))) {
42+
return false;
43+
}
44+
45+
if (!$matcher->has('pattern') || !\is_string($matcher->get('pattern'))) {
46+
return false;
47+
}
48+
49+
if (!$matcher->has('use')) {
50+
return false;
51+
}
52+
}
53+
54+
return true;
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function resolve($definition)
61+
{
62+
if (!$definition instanceof Definition) {
63+
throw new \InvalidArgumentException('$definition must be an instance of ' . Definition::class);
64+
}
65+
66+
foreach ($definition->get($this->getIndicator()) as $matcher) {
67+
$value = (string) $this->getIterator()->get($matcher->get('matches'));
68+
$pattern = '/' . addcslashes($matcher->get('pattern'), '/') . '/';
69+
70+
if (preg_match($pattern, $value, $matches)) {
71+
// preface each key with '$'
72+
$matches = array_combine(array_map(function ($key) {
73+
return '$' . $key;
74+
}, array_keys($matches)), $matches);
75+
76+
// $matches is a temporary context value, so need a unique clone of Iterator & Context
77+
$iterator = clone $this->getIterator();
78+
$iterator->getContext()->set('$match', $matches, false);
79+
80+
return $iterator->get('use', $matcher);
81+
}
82+
}
83+
84+
return $this->getIterator()->get('default', $definition);
85+
}
86+
}

src/ResolverFactory.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ class ResolverFactory
2222
* @var array map of resolver key to their class implementation
2323
*/
2424
private static $resolverClasses = [
25-
self::RESOLVER_TYPE_DIRECTORY => Resolver\Directory::class,
26-
self::RESOLVER_TYPE_FILE => Resolver\File::class,
27-
self::RESOLVER_TYPE_INLINE => Resolver\Inline::class,
28-
self::RESOLVER_TYPE_PROXY => Resolver\Proxy::class,
29-
self::RESOLVER_TYPE_SERVICE => Resolver\Service::class,
30-
self::RESOLVER_TYPE_TEMPLATE => Resolver\Template::class,
25+
self::RESOLVER_TYPE_CONDITIONAL => Resolver\Conditional::class,
26+
self::RESOLVER_TYPE_DIRECTORY => Resolver\Directory::class,
27+
self::RESOLVER_TYPE_FILE => Resolver\File::class,
28+
self::RESOLVER_TYPE_INLINE => Resolver\Inline::class,
29+
self::RESOLVER_TYPE_PROXY => Resolver\Proxy::class,
30+
self::RESOLVER_TYPE_SERVICE => Resolver\Service::class,
31+
self::RESOLVER_TYPE_TEMPLATE => Resolver\Template::class,
3132
];
3233

3334
/**

test/AbstractKeyValueStoreTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ public function testGet(): void
6262

6363
verify($value11)->is()->instanceOf(AbstractKeyValueStore::class);
6464
verify($value11->get('key110'))->is()->sameAs('value 110');
65+
66+
$subject = $this->getSubject(['a', 'b', 'c']);
67+
68+
verify($subject->get(0))->is()->sameAs('a');
69+
verify($subject->get('0'))->is()->sameAs('a');
70+
verify($subject->get(''))->is()->null();
6571
}
6672

6773
public function testGetKeys(): void
@@ -109,6 +115,31 @@ public function testHas(): void
109115
verify($subject->has('key1.key11.key112'))->is()->false();
110116
}
111117

118+
public function testIteratorFunction(): void
119+
{
120+
$subject = $this->getSubject(['a', 'b', 'c']);
121+
122+
$subject->rewind();
123+
124+
verify($subject->valid())->is()->true();
125+
verify($subject->current())->is()->sameAs('a');
126+
127+
$subject->next();
128+
verify($subject->valid())->is()->true();
129+
verify($subject->current())->is()->sameAs('b');
130+
131+
$subject->next();
132+
verify($subject->valid())->is()->true();
133+
verify($subject->current())->is()->sameAs('c');
134+
135+
$subject->next();
136+
verify($subject->valid())->is()->false();
137+
138+
$subject->rewind();
139+
verify($subject->valid())->is()->true();
140+
verify($subject->current())->is()->sameAs('a');
141+
}
142+
112143
/**
113144
* @depends testGet
114145
*/

test/ContextTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ public function builtinDataProvider(): array
3232
];
3333
}
3434

35+
public function testClone(): void
36+
{
37+
$original = new Context([]);
38+
39+
$original->set('delete-me1', 'some value', false);
40+
$original->set('delete-me2', 'some other value', false);
41+
$original->set('keep-me', 'some permanent value');
42+
43+
verify($original->has('delete-me1'))->is()->true();
44+
verify($original->has('delete-me2'))->is()->true();
45+
verify($original->has('keep-me'))->is()->true();
46+
47+
$clone = clone $original;
48+
49+
verify($clone->has('delete-me1'))->is()->false();
50+
verify($clone->has('delete-me2'))->is()->false();
51+
verify($clone->has('keep-me'))->is()->true();
52+
}
53+
3554
public function testFromRequest(): void
3655
{
3756
$request = Mockery::mock(Request::class);

test/DefinitionIteratorTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ class DefinitionIteratorTest extends TestCase
2222
{
2323
use MockeryPHPUnitIntegration;
2424

25+
public function testClone(): void
26+
{
27+
$context = new Context([]);
28+
$definition = new Definition([]);
29+
$original = new DefinitionIterator($definition, $context);
30+
31+
verify($original->getContext())->is()->sameAs($context);
32+
verify($original->getRootDefinition())->is()->sameAs($definition);
33+
34+
$clone = clone $original;
35+
36+
verify($clone->getContext())->is()->equalTo($context);
37+
verify($clone->getContext())->isNot()->sameAs($context);
38+
verify($clone->getRootDefinition())->is()->sameAs($definition);
39+
}
40+
2541
public function testDefinitionLoop(): void
2642
{
2743
$context = new Context([]);

0 commit comments

Comments
 (0)