Skip to content

Commit dca7992

Browse files
author
Alistair Kearney
committed
Added Transformer and Transformation feature. Included unit tests for Transformation classes. Updated CHANGELOG and README to reflect this change. Fixes #2
1 parent e6725c2 commit dca7992

7 files changed

+360
-8
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [Unreleased]
6+
#### Added
7+
- Transformer and Transformation classes.
8+
- Added APIFrameworkJSONRendererAppendTransformations delegate
9+
- Added phpunit to composer require-dev
10+
- Added unit tests for Transformation code
11+
12+
#### Removed
13+
- Symphony PDO is not longer a Composer requirement as it is not used
14+
515
## [0.1.1] - 2016-04-25
616
#### Added
717
- Added CONTRIBUTING.md and CHANGELOG.md
@@ -19,4 +29,5 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1929
- Initial release
2030
- Added Symphony PDO as requirement
2131

32+
[Unreleased]: https://github.com/pointybeard/api_framework/compare/v0.1.1...master
2233
[0.1.1]: https://github.com/pointybeard/api_framework/compare/v0.1.0...v0.1.1

README.md

+140-5
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ This is an extension for Symphony CMS. Add it to your `/extensions` folder in yo
88

99
### Requirements
1010

11-
This extension requires the **[Symfony HTTP Foundation](https://github.com/symfony/http-foundation)** (`symfony/http-foundation`) and **[Symphony PDO](https://github.com/pointybeard/symphony-pdo)** (`pointybeard/symphony-pdo`) to be installed via Composer. Either require both of these in your main composer.json file, or run `composer install` on the `extension/api_framework` directory.
11+
This extension requires the **[Symfony HTTP Foundation](https://github.com/symfony/http-foundation)** (`symfony/http-foundation`) to be installed via Composer. Either require both of these in your main composer.json file, or run `composer install` on the `extension/api_framework` directory.
1212

1313
"require": {
1414
"php": ">=5.6.6",
15-
"symfony/http-foundation": "^3.0@dev",
16-
"pointybeard/symphony-pdo": "~0.1"
15+
"symfony/http-foundation": "^3.0@dev"
1716
}
1817

1918
## Usage
@@ -113,8 +112,6 @@ Would result in the following JSON
113112
}
114113
```
115114

116-
**Note that when there is a single, in the case, `<entry>` element, the a JSON array is not produced. This is a known limitation (see [https://github.com/pointybeard/api_framework/issues/2](https://github.com/pointybeard/api_framework/issues/2))**
117-
118115
### Controller Event
119116

120117
Use the `API Framework: Controller` event to listen for PUT, POST, PATCH and DELETE requests. To create your own controller, make a folder called `controllers` in your `/workspace` directory.
@@ -240,3 +237,141 @@ Here is an example of a completed controller:
240237
return $this->render($response, $output);
241238
}
242239
}
240+
241+
### Transformers
242+
243+
Prior to converting the XML into JSON, transformers are run over it. Transformers mutate the result based on a test and action.
244+
245+
`@jsonForceArray`
246+
247+
This transformation will look for the attribute `jsonForceArray` on any XML elements. If it is set to "true", this transformation is applied. It relates to **[#issue-2](https://github.com/pointybeard/api_framework/issues/2)**. When there are multiple elements of the same name, for example 'entry', the JSON encode process will treat these as an array. E.g.
248+
249+
```
250+
<data>
251+
<entries>
252+
<entry>
253+
<id>2</id>
254+
<title>Another Entry</title>
255+
</entry>
256+
<entry>
257+
<id>1</id>
258+
<title>An Entry</title>
259+
</entry>
260+
</entries>
261+
</data>
262+
```
263+
264+
becomes
265+
266+
267+
```
268+
{
269+
"entries": {
270+
"entry": [
271+
{
272+
"id": "2",
273+
"title": "Another Entry",
274+
},
275+
{
276+
"id": "1",
277+
"title": "An Entry",
278+
}
279+
]
280+
}
281+
}
282+
```
283+
284+
However, if there is only a single 'entry' element, it is treated as an object. This is because internally it is just an associtive array, not an indexed array of 'entry' objects. E.g.
285+
286+
```
287+
<data>
288+
<entries>
289+
<entry>
290+
<id>2</id>
291+
<title>Another Entry</title>
292+
</entry>
293+
</entries>
294+
</data>
295+
```
296+
297+
results in
298+
299+
```
300+
{
301+
"entries": {
302+
"entry": {
303+
"id": "1",
304+
"title": "An Entry",
305+
}
306+
}
307+
}
308+
```
309+
310+
Notice that 'entry' is a JSON object. The problem with this is inconsistent data. It changes depending on how many entries are present. The solution is to set `jsonForceArray="true"` on the 'entry' element to trigger the transformation:
311+
312+
```
313+
<data>
314+
<entries>
315+
<entry jsonForceArray="true">
316+
<id>2</id>
317+
<title>Another Entry</title>
318+
</entry>
319+
</entries>
320+
</data>
321+
```
322+
323+
Which results in JSON
324+
325+
```
326+
{
327+
"entries": {
328+
"entry": [
329+
{
330+
"id": "2",
331+
"title": "Another Entry",
332+
}
333+
]
334+
}
335+
}
336+
```
337+
338+
### Creating new Transformers
339+
340+
This extention provides the delegate `APIFrameworkJSONRendererAppendTransformations` on all frontend pages with the `JSON` type. The context includes an instance of `Lib\Transformer`. Use the `append()` method to add your own transformations. E.g.
341+
342+
```
343+
<?php
344+
345+
use Symphony\ApiFramework\Lib;
346+
347+
Class extension_example extends Extension
348+
{
349+
public function getSubscribedDelegates(){
350+
return[[
351+
'page' => '/frontend/',
352+
'delegate' => 'APIFrameworkJSONRendererAppendTransformations',
353+
'callback' => 'appendTransformations'
354+
]];
355+
}
356+
357+
public function appendTransformations($context) {
358+
359+
$context['transformer']->append(
360+
new Lib\Transformation(
361+
362+
// This is the test. If it returns true, the action will be run
363+
function(array $input, array $attributes=[]){
364+
// do some tests in here and return either true or false
365+
return true;
366+
},
367+
368+
// This is the action. If the test passes, this code will be run
369+
function(array $input, array $attributes=[]){
370+
// Operate on $input and return the result.
371+
return $input;
372+
}
373+
)
374+
);
375+
}
376+
}
377+
```

extension.driver.php

+46-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<?php
22

3-
include __DIR__ . '/vendor/autoload.php';
3+
require_once __DIR__ . '/vendor/autoload.php';
44

5-
Class extension_api_framework extends Extension {
5+
use Symphony\ApiFramework\Lib;
6+
7+
Class extension_api_framework extends Extension
8+
{
69
public function getSubscribedDelegates(){
710
return[
811
[
@@ -15,16 +18,56 @@ public function getSubscribedDelegates(){
1518
'delegate' => 'FrontendOutputPreGenerate',
1619
'callback' => 'setBoilerplateXSL'
1720
],
18-
21+
[
22+
'page' => '/frontend/',
23+
'delegate' => 'APIFrameworkJSONRendererAppendTransformations',
24+
'callback' => 'appendTransformations'
25+
],
1926
];
2027
}
2128

29+
public function appendTransformations($context) {
30+
31+
// Add the @jsonForceArray transformation
32+
$context['transformer']->append(
33+
new Lib\Transformation(
34+
function(array $input, array $attributes=[]){
35+
// First make sure there is an attributes array
36+
if(empty($attributes)) {
37+
return false;
38+
}
39+
// Only looking at the jsonForceArray property
40+
elseif(!isset($attributes['jsonForceArray']) || $attributes['jsonForceArray'] !== "true") {
41+
return false;
42+
}
43+
// This is already an indexed array.
44+
elseif(!Lib\array_is_assoc($input)) {
45+
return false;
46+
}
47+
// jsonForceArray is set, and it's true
48+
return true;
49+
},
50+
function(array $input, array $attributes=[]){
51+
$result = [];
52+
// Encapsulate everything in an array
53+
foreach($input as $key => $value) {
54+
$result[$key] = $value;
55+
unset($input[$key]);
56+
}
57+
$input[] = $result;
58+
return $input;
59+
}
60+
)
61+
);
62+
}
63+
2264
public function setJSONLauncher($context)
2365
{
2466
if($_REQUEST['mode'] == 'administration') {
2567
return;
2668
}
2769
define('SYMPHONY_LAUNCHER', 'renderer_json');
70+
2871
include __DIR__ . '/src/Includes/JsonRendererLauncher.php';
2972
}
3073

src/Includes/JsonRendererLauncher.php

+16
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ function renderer_json($mode){
2929
// use JSON_PRETTY_PRINT directly on a SimpleXMLElement object
3030
$outputArray = json_decode(json_encode($xml));
3131

32+
// Get the transforer object ready. Other extensions will
33+
// add their transormations to this.
34+
$transformer = new Lib\Transformer();
35+
36+
/**
37+
* Allow other extensions to add their own transformers
38+
*/
39+
Symphony::ExtensionManager()->notifyMembers(
40+
'APIFrameworkJSONRendererAppendTransformations',
41+
'/frontend/',
42+
['transformer' => &$transformer]
43+
);
44+
45+
// Apply transformations
46+
$outputArray = $transformer->run($outputArray);
47+
3248
// Now put the array through a json_encode
3349
$output = json_encode($outputArray, JSON_PRETTY_PRINT);
3450

src/Lib/Transformation.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
namespace Symphony\ApiFramework\Lib;
3+
4+
class Transformation
5+
{
6+
private $test;
7+
private $action;
8+
9+
public function __construct(callable $test, callable $action)
10+
{
11+
$this->test = $test;
12+
$this->action = $action;
13+
}
14+
15+
public function __call($name, $args) {
16+
if(!isset($this->$name) || !is_callable($this->$name)) {
17+
throw new \Exception(__CLASS__ . " has no callable member '{$name}'.");
18+
}
19+
return call_user_func_array($this->$name, $args);
20+
}
21+
}

src/Lib/Transformer.php

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
namespace Symphony\ApiFramework\Lib;
3+
4+
function array_is_assoc(array $input) {
5+
return array_keys($input) !== range(0, count($input) - 1);
6+
}
7+
8+
function array_remove_empty($haystack)
9+
{
10+
foreach ($haystack as $key => $value) {
11+
if (is_array($value)) {
12+
$haystack[$key] = array_remove_empty($haystack[$key]);
13+
}
14+
if (empty($haystack[$key])) {
15+
unset($haystack[$key]);
16+
}
17+
}
18+
return $haystack;
19+
}
20+
21+
/**
22+
* Transformer
23+
* Modifies an array with various transformations
24+
*/
25+
class Transformer
26+
{
27+
28+
private $transformations = [];
29+
30+
public function append(Transformation $transformation) {
31+
$this->transformations[] = $transformation;
32+
return $this;
33+
}
34+
35+
public function transformations() {
36+
return $this->transformations;
37+
}
38+
39+
public function run(array $input) {
40+
foreach ($this->transformations as $t) {
41+
$input = $this->recursiveApplyTransformationToArray($input, $t);
42+
}
43+
44+
return $input;
45+
}
46+
47+
private function recursiveApplyTransformationToArray(array $input, Transformation $transformation) {
48+
$result = [];
49+
50+
$attributes = isset($input['@attributes']) ? $input['@attributes'] : [];
51+
// Strip out the attributes.
52+
unset($input['@attributes']);
53+
54+
// Run the input against the transformation test. If it passes, run
55+
// the actual transformation
56+
if($transformation->test($input, $attributes) === true) {
57+
$input = $transformation->action($input, $attributes);
58+
}
59+
60+
// Are we dealing with an associative array, or a sequential indexed array
61+
$isAssoc = array_is_assoc($input);
62+
63+
// Iterate over each element in the array and decide if we need to move deeper
64+
foreach($input as $key => $value) {
65+
$next = (is_array($value)
66+
// It's an array, so go deeper.
67+
? $this->recursiveApplyTransformationToArray($value, $transformation)
68+
// Non-array, just append it back in and return
69+
: $value
70+
);
71+
// Preserve array indexes
72+
$isAssoc ? $result[$key] = $next : array_push($result, $value);
73+
}
74+
return $result;
75+
}
76+
}

0 commit comments

Comments
 (0)