Skip to content

Commit 24f72c8

Browse files
committed
Added support for strategy-based mappings and accompanying test.
Added support for null literal expressions and accompanying test case. Added MapperTest. BCB: Removed ArrayObject from Mapping inheritance chain.
1 parent a79f5bd commit 24f72c8

9 files changed

Lines changed: 157 additions & 18 deletions

File tree

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ This supposes we already created a mapping, `MyMapping`, to convert the data.
1616

1717
Mappings
1818
--------
19-
Mappings are data transformation descriptions that describe how to convert data from one format to another. Mapping classes are an object wrapper for an array that describes the output format with instructions (hereafter known as *strategies*) that fetch or augment input data. To write a mapping we must know the input data format so we can then write an array that represents the desired output format and decorate it with strategies.
19+
20+
Mappings are data transformation descriptions that describe how to convert data from one format to another. Mappings are an object wrapper for an array that describes the output format with instructions (hereafter known as *strategies*) that fetch or augment input data. To write a mapping we must know the input data format so we can then write an array that represents the desired output format and decorate it with strategies.
2021

2122
### Example
2223

@@ -50,8 +51,9 @@ An expression is a pseudo-type representing the list of valid mapping value type
5051
2. `Mapping`
5152
3. Mapping fragment
5253
4. Scalar
54+
5. `null`
5355

54-
[Strategies](#strategies) are invoked and substituted as described in the following section. Mappings may contain any number of additional embedded mappings or mapping fragments—a mapping fragment is just a mapping described by an array instead of a `Mapping` object. Scalar values (*integer*, *float*, *string* and *boolean*) have no special meaning and are presented verbatim in the output.
56+
[Strategies](#strategies) are invoked and substituted as described in the following section. Mappings may contain any number of additional embedded mappings or mapping fragments—a mapping fragment is just a mapping described by an array instead of a `Mapping` object. Scalar values (*integer*, *float*, *string* and *boolean*) and `null` have no special meaning and are presented verbatim in the output.
5557

5658
### Writing a mapping
5759

@@ -68,9 +70,14 @@ Strategies are invokable classes that are invoked by Mapper and substituted for
6870

6971
Strategies are basic building blocks from which complex data manipulation chains can be constructed to meet the bespoke requirements of an application. The composition of strategies forms a powerful object composition DSL that allows us to express how to retrieve and augment data to mould it into the desired format.
7072

71-
### Core strategies
73+
For a complete list of strategies please see the [strategy reference](#strategy-reference).
74+
75+
Strategy reference
76+
------------------
77+
78+
The following strategies ship with Mapper and provide a suite of commonly used features, as listed below.
7279

73-
Core strategies are those that ship with Mapper and provide a suite of commonly used features, as listed below.
80+
### Strategy index
7481

7582
#### Fetchers
7683

@@ -80,7 +87,7 @@ Core strategies are those that ship with Mapper and provide a suite of commonly
8087
#### Augmenters
8188

8289
- [Callback](#callback) – Augments data using the specified callback.
83-
- [Collection](#collection) – Decorates a collection of data by applying a transformation to each datum.
90+
- [Collection](#collection) – Maps a collection of data by applying a transformation to each datum.
8491
- [Context](#context) – Replaces the context for the specified expression.
8592
- [Either](#either) – Either uses the primary strategy, if it returns non-null, otherwise delegates to a fallback expression.
8693
- [Filter](#filter) – Filters null values or values rejected by the specified callback.
@@ -189,7 +196,7 @@ Callback(callable $callback)
189196
190197
### Collection
191198

192-
Decorates a collection of data by applying a transformation to each datum using a callback. The data collection must be an expression that maps to an array otherwise null is returned.
199+
Maps a collection of data by applying a transformation to each datum using a callback. The data collection must be an expression that maps to an array otherwise null is returned.
193200

194201
#### Signature
195202

@@ -523,7 +530,7 @@ Walk(Strategy|Mapping|array|mixed $expression, array|string $path)
523530

524531
Strategies must implement the `Strategy` interface but it is common to extend `Delegate` or `Decorator` because we usually write augmenters which expect another strategy injected into them to provide data. `Delegate` and `Decorator` provide the `delegate()` method, which allows a strategy to evaluate an expression using Mapper, and is usually needed to evaluate the injected strategy. `Delegate` can delegate any expression to Mapper whereas `Decorator` only accepts `Strategy` objects.
525532

526-
It is recommended to name custom strategies with a *Strategy* suffix to help distinguish them from core strategies.
533+
It is recommended to name custom strategies with a *Strategy* suffix to help distinguish them from stock strategies.
527534

528535
Requirements
529536
------------
@@ -537,6 +544,13 @@ Testing
537544
Mapper is fully unit tested. Run the tests with `bin/test` from a shell. All examples
538545
in this document can be found in `DocumentationTest`.
539546

547+
Limitations
548+
-----------
549+
550+
- Strategies do not know the name of the key they are assigned to because `Mapper` does not forward the key name.
551+
- Strategies do not know where they sit in a `Mapping` and therefore cannot traverse a mapping relative to their position.
552+
- The `Collection` strategy overwrites context making any previous context inaccessible to descendants.
553+
540554

541555
[Releases]: https://github.com/ScriptFUSION/Mapper/releases
542556
[Version image]: https://poser.pugx.org/scriptfusion/mapper/version "Latest version"

src/AnonymousMapping.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
<?php
22
namespace ScriptFUSION\Mapper;
33

4+
use ScriptFUSION\Mapper\Strategy\Strategy;
5+
46
final class AnonymousMapping extends Mapping
57
{
68
private $definition;
79

8-
public function __construct(array $definition)
10+
/**
11+
* @param array|Strategy $definition
12+
*/
13+
public function __construct($definition)
914
{
1015
$this->definition = $definition;
1116

src/InvalidMappingException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
namespace ScriptFUSION\Mapper;
3+
4+
/**
5+
* The exception that is thrown when an invalid mapping type is specified.
6+
*/
7+
class InvalidMappingException extends \RuntimeException
8+
{
9+
// Intentionally empty.
10+
}

src/Mapper.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use ScriptFUSION\Mapper\Strategy\Strategy;
55

66
/**
7-
* Maps records according to strategies, mappings and mapping fragments.
7+
* Maps records according to expression value types.
88
*/
99
class Mapper
1010
{
@@ -28,8 +28,8 @@ public function map(array $record, $expression, $context = null)
2828
} /* Mapping fragment. */
2929
elseif (is_array($expression)) {
3030
return $this->mapFragment($record, $expression, $context);
31-
} /* Scalar values. */
32-
elseif (is_scalar($expression)) {
31+
} /* Null or scalar values. */
32+
elseif (null === $expression || is_scalar($expression)) {
3333
return $expression;
3434
}
3535

@@ -47,7 +47,14 @@ public function map(array $record, $expression, $context = null)
4747
*/
4848
protected function mapMapping(array $record, Mapping $mapping, $context = null)
4949
{
50-
return $this->mapFragment($record, $mapping->getArrayCopy(), $context);
50+
$mapped = $this->mapFragment($record, $mapping->toArray(), $context);
51+
52+
if ($mapping->isWrapped()) {
53+
// Unwrap.
54+
return $mapped[0];
55+
}
56+
57+
return $mapped;
5158
}
5259

5360
protected function mapFragment(array $record, array $fragment, $context = null)
@@ -62,7 +69,7 @@ function (&$strategy, $key, array $record) use ($context) {
6269
return $fragment;
6370
}
6471

65-
throw new \Exception; // TODO: Proper exception.
72+
throw new \Exception; // TODO: Determine whether this statement is reachable.
6673
}
6774

6875
/**

src/Mapping.php

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,61 @@
11
<?php
22
namespace ScriptFUSION\Mapper;
33

4+
use ScriptFUSION\Mapper\Strategy\Strategy;
5+
46
/**
57
* Represents a mapping of keys and mappable values.
68
*/
7-
abstract class Mapping extends \ArrayObject
9+
abstract class Mapping
810
{
11+
/**
12+
* @var array
13+
*/
14+
private $mapping;
15+
16+
private $wrapped = false;
17+
918
/**
1019
* Initializes this mapping.
1120
*/
1221
public function __construct()
1322
{
14-
parent::__construct($this->createMapping());
23+
/* Array-based mapping. */
24+
if (is_array($mapping = $this->createMapping())) {
25+
$this->mapping = $mapping;
26+
} /* Strategy-based mapping. */
27+
elseif ($mapping instanceof Strategy) {
28+
$this->mapping = [$mapping];
29+
$this->wrapped = true;
30+
} else {
31+
throw new InvalidMappingException('Invalid mapping: must be array or an instance of Strategy.');
32+
}
1533
}
1634

1735
/**
18-
* Creates a mapping of key names and mappable values.
36+
* Creates a mapping of key names and expressions or a straegy.
1937
*
20-
* @return array Mapping.
38+
* @return array|Strategy Mapping.
2139
*/
2240
abstract protected function createMapping();
41+
42+
/**
43+
* Converts the mapping to an array.
44+
*
45+
* @return array
46+
*/
47+
public function toArray()
48+
{
49+
return $this->mapping;
50+
}
51+
52+
/**
53+
* Gets a value indicating whether the mapping has been wrapped in an array.
54+
*
55+
* @return boolean True if the mapping is wrapped in an outer array, otherwise false.
56+
*/
57+
public function isWrapped()
58+
{
59+
return $this->wrapped;
60+
}
2361
}

src/Strategy/Collection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use ScriptFUSION\Mapper\Mapping;
55

66
/**
7-
* Decorates a collection of data by applying a transformation to each datum using a callback.
7+
* Maps a collection of data by applying a transformation to each datum using a callback.
88
*/
99
class Collection extends Delegate
1010
{
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
namespace ScriptFUSIONTest\Functional;
3+
4+
use ScriptFUSION\Mapper\AnonymousMapping;
5+
use ScriptFUSION\Mapper\Mapper;
6+
use ScriptFUSION\Mapper\Strategy\Merge;
7+
8+
final class StrategyBasedMappingTest extends \PHPUnit_Framework_TestCase
9+
{
10+
public function test()
11+
{
12+
self::assertSame(
13+
['foo' => 'foo', 'bar' => 'bar'],
14+
(new Mapper)->map(
15+
[],
16+
new AnonymousMapping(
17+
new Merge(
18+
['foo' => 'foo'],
19+
['bar' => 'bar']
20+
)
21+
)
22+
)
23+
);
24+
}
25+
}

test/Integration/Mapper/MapperTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ public function provideScalars()
6666
];
6767
}
6868

69+
public function testMapNull()
70+
{
71+
$mapped = $this->mapper->map([], $output = ['foo' => null]);
72+
73+
self::assertSame($output, $mapped);
74+
}
75+
6976
public function testMapInvalidObject()
7077
{
7178
$this->setExpectedException(InvalidExpressionException::class);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
namespace ScriptFUSIONTest\Integration\Mapper;
3+
4+
use ScriptFUSION\Mapper\AnonymousMapping;
5+
use ScriptFUSION\Mapper\InvalidMappingException;
6+
use ScriptFUSION\Mapper\Mapping;
7+
use ScriptFUSION\Mapper\Strategy\Strategy;
8+
9+
final class MappingTest extends \PHPUnit_Framework_TestCase
10+
{
11+
public function testArrayBasedMapping()
12+
{
13+
$mapping = new AnonymousMapping($fragment = ['foo' => 'foo']);
14+
15+
self::assertSame($fragment, $mapping->toArray());
16+
self::assertFalse($mapping->isWrapped());
17+
}
18+
19+
public function testStrategyBasedMapping()
20+
{
21+
$mapping = new AnonymousMapping($strategy = \Mockery::mock(Strategy::class));
22+
23+
self::assertSame([$strategy], $mapping->toArray());
24+
self::assertTrue($mapping->isWrapped());
25+
}
26+
27+
public function testInvalidMapping()
28+
{
29+
$this->setExpectedException(InvalidMappingException::class);
30+
31+
new AnonymousMapping(\Mockery::mock(Mapping::class));
32+
}
33+
}

0 commit comments

Comments
 (0)