├── tests ├── .gitkeep ├── NamedArrayRefinery.php ├── FooBarObjectRefinery.php ├── FooBarArrayRefinery.php ├── ObjectRefinery.php ├── ArrayRefinery.php └── RefineryTest.php ├── .gitignore ├── src ├── Exceptions │ ├── RefineryMethodNotFound.php │ └── AttachmentClassNotFound.php ├── Contracts │ └── Refinery.php └── Refinery.php ├── .travis.yml ├── composer.json ├── phpunit.xml ├── LICENSE └── README.md /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea 3 | composer.lock -------------------------------------------------------------------------------- /src/Exceptions/RefineryMethodNotFound.php: -------------------------------------------------------------------------------- 1 | $item->foobar 19 | ]; 20 | } 21 | } -------------------------------------------------------------------------------- /tests/FooBarArrayRefinery.php: -------------------------------------------------------------------------------- 1 | $item['foobar'], 20 | ]; 21 | } 22 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "michaeljennings/refinery", 3 | "description": "A php class to refine data into a set format.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Michael Jennings", 8 | "email": "michael.jennings91@gmail.com" 9 | }, 10 | { 11 | "name": "George Hanson", 12 | "email": "george.w.hanson96@gmail.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.4.0" 17 | }, 18 | "require-dev": { 19 | "satooshi/php-coveralls": "1.0.*", 20 | "phpunit/phpunit": "^5.7" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Michaeljennings\\Refinery\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Michaeljennings\\Refinery\\Tests\\": "tests/" 30 | } 31 | }, 32 | "minimum-stability": "stable" 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 michaeljennings 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tests/ObjectRefinery.php: -------------------------------------------------------------------------------- 1 | $item->foo, 19 | 'bar' => $item->bar, 20 | 'baz' => 'qux', 21 | 'quux' => $this->quux 22 | ]; 23 | } 24 | 25 | public function fooBarAttach() 26 | { 27 | return $this->attach(FooBarObjectRefinery::class); 28 | } 29 | 30 | public function fooBarEmbed() 31 | { 32 | return $this->embed(FooBarObjectRefinery::class); 33 | } 34 | 35 | public function fooBarNest() 36 | { 37 | return $this->nest(FooBarObjectRefinery::class, function($raw) { 38 | return $raw; 39 | }); 40 | } 41 | 42 | public function fooBarRaw() 43 | { 44 | return $this->attach(function($item) { 45 | return $item->foo . $item->bar; 46 | }); 47 | } 48 | } -------------------------------------------------------------------------------- /tests/ArrayRefinery.php: -------------------------------------------------------------------------------- 1 | $item['foo'], 19 | 'bar' => $item['bar'], 20 | 'baz' => 'qux', 21 | 'quux' => $this->quux 22 | ]; 23 | } 24 | 25 | public function fooBarAttach() 26 | { 27 | return $this->attach(FooBarArrayRefinery::class); 28 | } 29 | 30 | public function fooBarEmbed() 31 | { 32 | return $this->embed(FooBarArrayRefinery::class); 33 | } 34 | 35 | public function fooBarNest() 36 | { 37 | return $this->nest(FooBarArrayRefinery::class, function($raw) { 38 | return $raw; 39 | }); 40 | } 41 | 42 | public function fooBarRaw() 43 | { 44 | return $this->attach(function($item) { 45 | return $item['foo'] . $item['bar']; 46 | }); 47 | } 48 | 49 | public function classDoesNotExist() 50 | { 51 | return $this->attach("NonExistentClass"); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Contracts/Refinery.php: -------------------------------------------------------------------------------- 1 | number_format($product->price, 2), 26 | 'description' => $product->description, 27 | 'full_description' => $product->description . ' For only £' . number_format($product->price, 2), 28 | ]; 29 | } 30 | } 31 | 32 | // Then we create the instances and refine the product 33 | $product = new Product(); 34 | $refinery = new ProductRefinery(); 35 | 36 | var_dump($refinery->refine($product)); 37 | 38 | // The above will output: 39 | // ['price' => '10.00', 'description' => 'Something really cool!', 'full_description' => 'Something really cool! For only £10.00'] 40 | ``` 41 | 42 | ## Navigation 43 | 44 | - [Installation](#installation) 45 | - [Usage](#usage) 46 | - [Example Usage](#example-usage) 47 | - [Multidimensional Refining](#multidimensional-refining) 48 | - [Advanced Refining](#advanced-refining) 49 | - [Using External Data](#using-external-data) 50 | - [Custom Attachments](#custom-attachments) 51 | - [Raw Attachments](#raw-attachments) 52 | 53 | ## Installation 54 | 55 | Run `composer require michaeljennings/refinery`. 56 | 57 | Or add the package to your `composer.json`. 58 | 59 | ``` 60 | "michaeljennings/refinery": "~1.0"; 61 | ``` 62 | 63 | ## Usage 64 | 65 | To create a new refinery simply extend the `Michaeljennings\Refinery\Refinery`. 66 | 67 | ```php 68 | use Michaeljennings\Refinery\Refinery; 69 | 70 | class Foo extends Refinery 71 | { 72 | 73 | } 74 | ``` 75 | 76 | The refinery has one abstract method which is `setTemplate`. The setTemplate method is passed the item being refined and then you just need to return it in the format you want. 77 | 78 | ```php 79 | class Foo extends Refinery 80 | { 81 | public function setTemplate($data) 82 | { 83 | // Refine data 84 | } 85 | } 86 | ``` 87 | 88 | To pass data to the refinery, create a new instance of the class and then use the `refine` method. 89 | 90 | ```php 91 | $foo = new Foo; 92 | 93 | $foo->refine($data); 94 | ``` 95 | 96 | You can pass the refinery either a single item or a multidimensional item. 97 | 98 | ```php 99 | $foo = new Foo; 100 | 101 | $data = $foo->refine(['foo', 'bar']); 102 | $multiDimensionalData = $foo->refine([['foo'], ['bar']]); 103 | ``` 104 | 105 | ### Example Usage 106 | As an example if I had a product and I wanted to make sure that its price always had two decimal figures I could do the 107 | following. 108 | 109 | ```php 110 | class Product { 111 | public price = '10'; 112 | } 113 | 114 | class ProductRefinery extends Refinery { 115 | public function setTemplate($product) 116 | { 117 | return [ 118 | 'price' => number_format($product->price, 2), 119 | ]; 120 | } 121 | } 122 | 123 | $product = new Product(); 124 | $refinery = new ProductRefinery(); 125 | 126 | $refinedProduct = $refinery->refine($product); // ['price' => 10.00] 127 | 128 | ``` 129 | 130 | ## Multidimensional Refining 131 | 132 | When you are working with database data, particularly relational data, it's common that you won't just be refining the main entity, you may have parent and child data that also needs to be refined. 133 | 134 | This is best displayed with an example. Below we have a product that has many variants, and belongs to a category. 135 | 136 | ```php 137 | $product = [ 138 | 'name' => 'Awesome Product', 139 | 'description' => 'This is an awesome product', 140 | 'price' => 10.00, 141 | 'category' => [ 142 | 'name' => 'Awesome Products', 143 | ], 144 | 'variants' => [ 145 | [ 146 | 'size' => 'Medium', 147 | ], 148 | [ 149 | 'size' => 'Large', 150 | ] 151 | ] 152 | ] 153 | ``` 154 | 155 | This parent and child data will usually be refined in the same way in every area it is used within your application, therefore it makes sense to move it into it's own class so that it can be reused. 156 | 157 | To do that we use attachments. 158 | 159 | Below we have a product refinery, that has category and variant attachments. 160 | 161 | ```php 162 | class ProductRefinery extends Refinery 163 | { 164 | public function setTemplate($product) 165 | { 166 | return [ 167 | 'name' => $product['name'], 168 | 'description' => $product['description'], 169 | 'price' => '£' . number_format($product['price'], 2), 170 | ]; 171 | } 172 | 173 | public function category() 174 | { 175 | return $this->attach(CategoryRefinery::class); 176 | } 177 | 178 | public function variants() 179 | { 180 | return $this->attach(CategoryRefinery::class); 181 | } 182 | } 183 | ``` 184 | 185 | These attachments will only be brought with the item when the `bring` method is called when refining. 186 | 187 | ```php 188 | // Will just refine the product data. 189 | $refinedProduct = $productRefinery->refine($product); 190 | 191 | // Will refine the product, category, and the variants. 192 | $refinedProductWithAttachments $productRefinery->bring('category', 'variants')->refine($product); 193 | ``` 194 | 195 | When you attempt to bring attachments it will look for methods on the refinery class with the specified attachment name. 196 | 197 | For example if we were to do the following: 198 | 199 | ```php 200 | $refinedProductWithAttachments $productRefinery->bring('category')->refine($product); 201 | ``` 202 | 203 | There would need to be a method called `category` on our product refinery. 204 | 205 | The refinery will then attempt to pass the category property from the product to the category refinery. 206 | 207 | Attachments can also be multidimensional. For example below we are bringing a product with it's category, and then we are bringing the category's meta data. 208 | 209 | ```php 210 | $refined = $productRefinery->bring(['category' => ['meta']])->refine($product); 211 | ``` 212 | 213 | This will look for the category attachment on the product refinery, and then a meta attachment on the category refinery. 214 | 215 | ## Advanced Refining 216 | 217 | ### Using External Data 218 | 219 | When refining you may need to access data that is not part of the main object. 220 | 221 | For example, below we want to get the correct currency symbol for the country the user is in, however the country cannot be accessed from the product. 222 | 223 | To get around this we use the `with` method to pass through the current country to the product refinery. 224 | 225 | ```php 226 | $country = [ 227 | 'name' => 'United Kingdom', 228 | 'symbol' => '£', 229 | ]; 230 | 231 | $refined = $productRefinery->with(['country' => $country])->refine($product); 232 | ``` 233 | 234 | Then in the `setTemplate` method in the refinery we can access the country as shown below. 235 | 236 | ```php 237 | class ProductRefinery extends Refinery 238 | { 239 | public function setTemplate($product) 240 | { 241 | $symbol = $this->country['symbol']; 242 | // OR 243 | $symbol = $this->attributes['country']['symbol']; 244 | 245 | return [ 246 | 'name' => $product['name'], 247 | 'description' => $product['description'], 248 | 'price' => $symbol . number_format($product['price'], 2), 249 | ]; 250 | } 251 | } 252 | ``` 253 | 254 | ### Custom Attachments 255 | 256 | Occasionally you may want to set attachments that don't actually exist in the original data. 257 | 258 | For example below we have added a `largeVariant` attachment to our product refinery. 259 | 260 | By passing a closure to the second argument of the `attach` method we can choose what data we want to send to the refinery. The closure will be passed the current item being refined, which in this case is the product. 261 | 262 | ```php 263 | class ProductRefinery extends Refinery 264 | { 265 | public function largeVariant() 266 | { 267 | return $this->attach(VariantRefinery::class, function($product) { 268 | return array_filter($product['variants'], function($variant) { 269 | return $variant['size'] == 'Large'; 270 | }); 271 | }); 272 | } 273 | } 274 | ``` 275 | 276 | ### Raw Attachments 277 | 278 | Refining can also be performed without a preset class, for instance if you want to refine a multidimensional array to a single array. 279 | 280 | Below we have added an attachment which gets all of the sizes for a product and returns it as an array. 281 | 282 | ```php 283 | class ProductRefinery extends Refinery 284 | { 285 | public function sizes() 286 | { 287 | return $this->attach(function($product) { 288 | $sizes = []; 289 | 290 | foreach ($product['variants'] as $variant) { 291 | $sizes[] = $variant['size']; 292 | } 293 | 294 | return $sizes; 295 | }); 296 | } 297 | } 298 | ``` -------------------------------------------------------------------------------- /tests/RefineryTest.php: -------------------------------------------------------------------------------- 1 | 'foo', 19 | 'bar' => 'bar', 20 | 'foo1' => 'foo1', 21 | 'bar2' => 'bar2', 22 | 'fooBarAttach' => [ 23 | 'foobar' => 'foobar', 24 | ], 25 | 'fooBarEmbed' => [ 26 | 'foobar' => 'foobar', 27 | ], 28 | 'foobar' => [ 29 | 'foobar' => 'foobar', 30 | ], 31 | ]; 32 | 33 | $refined = $refinery->with(['quux' => 'quux1'])->refine($raw); 34 | 35 | $this->assertArrayHasKey('baz', $refined); 36 | $this->assertArrayNotHasKey('foo1', $refined); 37 | $this->assertContains('foo', $refined); 38 | $this->assertContains('quux1', $refined); 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function it_refines_an_object() 45 | { 46 | $refinery = new ObjectRefinery(); 47 | 48 | $raw = new stdClass(); 49 | 50 | $raw->foo = 'foo'; 51 | $raw->bar = 'bar'; 52 | $raw->foo1 = 'foo1'; 53 | $raw->bar1 = 'bar1'; 54 | $raw->fooBarAttach = new stdClass(); 55 | $raw->fooBarAttach->foobar = 'foobar'; 56 | $raw->fooBarEmbed = new stdClass(); 57 | $raw->fooBarEmbed->foobar = 'foobar'; 58 | $raw->foobar = new stdClass(); 59 | $raw->foobar->foobar = 'foobar'; 60 | 61 | $refined = $refinery->with(['quux' => 'quux1'])->refine($raw); 62 | 63 | $this->assertArrayHasKey('baz', $refined); 64 | $this->assertContains('foo', $refined); 65 | $this->assertArrayNotHasKey('foo1', $refined); 66 | $this->assertContains('quux1', $refined); 67 | } 68 | 69 | /** 70 | * @test 71 | */ 72 | public function it_refines_multiple_arrays() 73 | { 74 | $refinery = new ArrayRefinery(); 75 | 76 | $raw = [ 77 | [ 78 | 'foo' => 'foo', 79 | 'bar' => 'bar', 80 | 'foo1' => 'foo1', 81 | 'bar2' => 'bar2', 82 | 'fooBarAttach' => [ 83 | 'foobar' => 'foobar', 84 | ], 85 | 'fooBarEmbed' => [ 86 | 'foobar' => 'foobar', 87 | ], 88 | 'foobar' => [ 89 | 'foobar' => 'foobar', 90 | ], 91 | ], 92 | [ 93 | 'foo' => 'foo', 94 | 'bar' => 'bar', 95 | 'foo1' => 'foo1', 96 | 'bar2' => 'bar2', 97 | 'fooBarAttach' => [ 98 | 'foobar' => 'foobar', 99 | ], 100 | 'fooBarEmbed' => [ 101 | 'foobar' => 'foobar', 102 | ], 103 | 'foobar' => [ 104 | 'foobar' => 'foobar', 105 | ], 106 | ], 107 | [ 108 | 'foo' => 'foo', 109 | 'bar' => 'bar', 110 | 'foo1' => 'foo1', 111 | 'bar2' => 'bar2', 112 | 'fooBarAttach' => [ 113 | 'foobar' => 'foobar', 114 | ], 115 | 'fooBarEmbed' => [ 116 | 'foobar' => 'foobar', 117 | ], 118 | 'foobar' => [ 119 | 'foobar' => 'foobar', 120 | ], 121 | ], 122 | ]; 123 | 124 | $refined = $refinery->with(['quux' => 'quux1'])->refine($raw); 125 | 126 | $this->assertCount(3, $refined); 127 | $this->assertArrayHasKey('baz', $refined[0]); 128 | $this->assertArrayNotHasKey('foo1', $refined[0]); 129 | $this->assertContains('foo', $refined[0]); 130 | $this->assertContains('quux1', $refined[0]); 131 | } 132 | 133 | /** 134 | * @test 135 | */ 136 | public function it_refines_multiple_objects() 137 | { 138 | $refinery = new ObjectRefinery(); 139 | 140 | $raw = new stdClass(); 141 | 142 | $raw->foo = 'foo'; 143 | $raw->bar = 'bar'; 144 | $raw->foo1 = 'foo1'; 145 | $raw->bar1 = 'bar1'; 146 | $raw->fooBarAttach = new stdClass(); 147 | $raw->fooBarAttach->foobar = 'foobar'; 148 | $raw->fooBarEmbed = new stdClass(); 149 | $raw->fooBarEmbed->foobar = 'foobar'; 150 | $raw->foobar = new stdClass(); 151 | $raw->foobar->foobar = 'foobar'; 152 | 153 | $rawCollection = [$raw, $raw, $raw]; 154 | 155 | $refined = $refinery->with(['quux' => 'quux1'])->refine($rawCollection); 156 | 157 | $this->assertCount(3, $refined); 158 | $this->assertArrayHasKey('baz', $refined[0]); 159 | $this->assertContains('foo', $refined[0]); 160 | $this->assertArrayNotHasKey('foo1', $refined[0]); 161 | $this->assertContains('quux1', $refined[0]); 162 | } 163 | 164 | /** 165 | * @test 166 | */ 167 | public function it_embeds_into_the_refined_array() 168 | { 169 | $refinery = new ArrayRefinery(); 170 | 171 | $raw = [ 172 | 'foo' => 'foo', 173 | 'bar' => 'bar', 174 | 'foo1' => 'foo1', 175 | 'bar2' => 'bar2', 176 | 'fooBarAttach' => [ 177 | 'foobar' => 'foobar', 178 | ], 179 | 'fooBarEmbed' => [ 180 | 'foobar' => 'foobar', 181 | ], 182 | 'foobar' => [ 183 | 'foobar' => 'foobar', 184 | ], 185 | ]; 186 | 187 | $refined = $refinery->bring('fooBarAttach', 'fooBarEmbed', 'fooBarNest')->with(['quux' => 'quux1'])->refine($raw); 188 | 189 | $this->assertArrayHasKey('baz', $refined); 190 | $this->assertArrayNotHasKey('foo1', $refined); 191 | $this->assertContains('foo', $refined); 192 | $this->assertArrayHasKey('fooBarAttach', $refined); 193 | $this->assertArrayHasKey('fooBarEmbed', $refined); 194 | $this->assertArrayHasKey('fooBarNest', $refined); 195 | $this->assertArrayHasKey('foobar', $refined['fooBarNest']); 196 | $this->assertContains('quux1', $refined); 197 | } 198 | 199 | /** 200 | * @test 201 | */ 202 | public function it_retains_keys_where_appropriate() 203 | { 204 | $refinery = new NamedArrayRefinery(); 205 | 206 | $raw = [ 207 | 'namedKey' => [ 208 | 'foo' => [ 209 | 'bar' => 'any number of things', 210 | ], 211 | ], 212 | 'anotherNamedKey' => [ 213 | 'foo' => [ 214 | 'unkeyed_array', 215 | ], 216 | ], 217 | ]; 218 | 219 | // Without Key 220 | $refined = $refinery->refine($raw); 221 | $this->assertArrayNotHasKey('namedKey', $refined); 222 | 223 | $retainKey = false; 224 | $refined = $refinery->refine($raw, $retainKey); 225 | $this->assertArrayNotHasKey('namedKey', $refined); 226 | 227 | // With Key 228 | $retainKey = true; 229 | $refined = $refinery->refine($raw, $retainKey); 230 | $this->assertArrayHasKey('namedKey', $refined); 231 | } 232 | 233 | /** 234 | * @test 235 | */ 236 | public function it_embeds_from_an_object() 237 | { 238 | $refinery = new ObjectRefinery(); 239 | 240 | $raw = new stdClass(); 241 | 242 | $raw->foo = 'foo'; 243 | $raw->bar = 'bar'; 244 | $raw->foo1 = 'foo1'; 245 | $raw->bar1 = 'bar1'; 246 | $raw->fooBarAttach = new stdClass(); 247 | $raw->fooBarAttach->foobar = 'foobar'; 248 | $raw->fooBarEmbed = new stdClass(); 249 | $raw->fooBarEmbed->foobar = 'foobar'; 250 | $raw->foobar = new stdClass(); 251 | $raw->foobar->foobar = 'foobar'; 252 | 253 | $refined = $refinery->bring('fooBarAttach', 'fooBarEmbed', 'fooBarNest')->with(['quux' => 'quux1'])->refine($raw); 254 | 255 | $this->assertArrayHasKey('baz', $refined); 256 | $this->assertContains('foo', $refined); 257 | $this->assertArrayNotHasKey('foo1', $refined); 258 | $this->assertArrayHasKey('fooBarAttach', $refined); 259 | $this->assertArrayHasKey('fooBarEmbed', $refined); 260 | $this->assertArrayHasKey('fooBarNest', $refined); 261 | $this->assertArrayHasKey('foobar', $refined['fooBarNest']); 262 | $this->assertContains('quux1', $refined); 263 | } 264 | 265 | /** 266 | * @test 267 | * @expectedException \Michaeljennings\Refinery\Exceptions\AttachmentClassNotFound 268 | */ 269 | public function it_throws_an_exception_if_attachment_class_is_not_found() 270 | { 271 | $refinery = new ArrayRefinery(); 272 | 273 | $raw = [ 274 | 'foo' => 'foo', 275 | 'bar' => 'bar', 276 | 'foo1' => 'foo1', 277 | 'bar2' => 'bar2', 278 | 'fooBarAttach' => [ 279 | 'foobar' => 'foobar', 280 | ], 281 | 'fooBarEmbed' => [ 282 | 'foobar' => 'foobar', 283 | ], 284 | 'foobar' => [ 285 | 'foobar' => 'foobar', 286 | ], 287 | ]; 288 | 289 | $refinery->bring('classDoesNotExist')->refine($raw); 290 | } 291 | 292 | /** 293 | * @test 294 | * @expectedException \Michaeljennings\Refinery\Exceptions\RefineryMethodNotFound 295 | */ 296 | public function it_throws_an_exception_if_the_attachment_has_not_been_set() 297 | { 298 | $refinery = new ArrayRefinery(); 299 | 300 | $raw = [ 301 | 'foo' => 'foo', 302 | 'bar' => 'bar', 303 | 'foo1' => 'foo1', 304 | 'bar2' => 'bar2', 305 | 'fooBarAttach' => [ 306 | 'foobar' => 'foobar', 307 | ], 308 | 'fooBarEmbed' => [ 309 | 'foobar' => 'foobar', 310 | ], 311 | 'foobar' => [ 312 | 'foobar' => 'foobar', 313 | ], 314 | ]; 315 | 316 | $refinery->bring('notSet')->refine($raw); 317 | } 318 | 319 | /** 320 | * @test 321 | */ 322 | public function it_attaches_a_raw_attachment() 323 | { 324 | $refinery = new ArrayRefinery(); 325 | 326 | $raw = [ 327 | 'foo' => 'foo', 328 | 'bar' => 'bar', 329 | 'foo1' => 'foo1', 330 | 'bar2' => 'bar2', 331 | ]; 332 | 333 | $refined = $refinery->bring('fooBarRaw')->with(['quux' => 'quux1'])->refine($raw); 334 | 335 | $this->assertArrayHasKey('fooBarRaw', $refined); 336 | $this->assertEquals('foobar', $refined['fooBarRaw']); 337 | } 338 | 339 | /** 340 | * @test 341 | */ 342 | public function it_attaches_a_raw_attachment_from_an_object() 343 | { 344 | $refinery = new ObjectRefinery(); 345 | 346 | $raw = new stdClass(); 347 | 348 | $raw->foo = 'foo'; 349 | $raw->bar = 'bar'; 350 | $raw->foo1 = 'foo1'; 351 | $raw->bar1 = 'bar1'; 352 | 353 | $refined = $refinery->bring('fooBarRaw')->with(['quux' => 'quux1'])->refine($raw); 354 | 355 | $this->assertArrayHasKey('fooBarRaw', $refined); 356 | $this->assertEquals('foobar', $refined['fooBarRaw']); 357 | } 358 | } -------------------------------------------------------------------------------- /src/Refinery.php: -------------------------------------------------------------------------------- 1 | isMultidimensional($raw)) || $raw instanceof Traversable) { 58 | return $this->refineCollection($raw, $retainKey); 59 | } 60 | 61 | return $this->refineItem($raw); 62 | } 63 | 64 | /** 65 | * Refine a single item using the supplied callback. 66 | * 67 | * @param mixed $raw 68 | * @return mixed 69 | */ 70 | public function refineItem($raw) 71 | { 72 | $refined = $this->setTemplate($raw); 73 | 74 | if ( ! empty($this->attachments)) { 75 | $refined = $this->merge($refined, $this->includeAttachments($raw)); 76 | } 77 | 78 | return $refined; 79 | } 80 | 81 | /** 82 | * Refine a collection of raw items. 83 | * 84 | * @param mixed $raw 85 | * @param boolean $retainKey 86 | * @return array 87 | */ 88 | public function refineCollection($raw, $retainKey = false) 89 | { 90 | $refined = []; 91 | 92 | foreach ($raw as $key => $item) { 93 | if ($retainKey) { 94 | $this->key = $key; 95 | $refined[ $key ] = $this->refineItem($item); 96 | } else { 97 | $refined[] = $this->refineItem($item); 98 | } 99 | } 100 | 101 | return $refined; 102 | } 103 | 104 | /** 105 | * Return any required attachments. Check if the attachment needs to 106 | * be filtered, if so they run the callback and then return the 107 | * attachments. 108 | * 109 | * @param mixed $raw 110 | * @return mixed 111 | */ 112 | protected function includeAttachments($raw) 113 | { 114 | $attachments = []; 115 | 116 | foreach ($this->attachments as $attachment => $refinery) { 117 | if (isset($refinery['raw'])) { 118 | $attachments[$attachment] = $refinery['raw']($raw); 119 | } else { 120 | $class = $refinery['class']; 121 | $callback = $refinery['callback']; 122 | 123 | if ( ! $callback) { 124 | $items = $this->getItems($raw, $class, $attachment); 125 | } else { 126 | $items = $this->getItemsUsingCallback($raw, $class, $callback); 127 | } 128 | 129 | $attachments[$attachment] = ! is_null($items) ? $class->refine($items) : null; 130 | } 131 | } 132 | 133 | return $attachments; 134 | } 135 | 136 | /** 137 | * Get the items to be refined from the raw object. 138 | * 139 | * @param mixed $raw 140 | * @param mixed $class 141 | * @param string $attachment 142 | * @return mixed 143 | */ 144 | protected function getItems($raw, $class, $attachment) 145 | { 146 | if ($class->hasFilter()) { 147 | $query = $class->getFilter(); 148 | 149 | return call_user_func_array($query, [$raw->$attachment()]); 150 | } else { 151 | return is_object($raw) ? $raw->$attachment : $raw[$attachment]; 152 | } 153 | } 154 | 155 | /** 156 | * Run a callback on the raw item(s) and then return the items. 157 | * 158 | * @param mixed $raw 159 | * @param mixed $class 160 | * @param callable $callback 161 | * @return mixed 162 | */ 163 | protected function getItemsUsingCallback($raw, $class, callable $callback) 164 | { 165 | if ($class->hasFilter()) { 166 | $query = $class->getFilter(); 167 | $items = $callback($raw); 168 | 169 | return call_user_func_array($query, [$items]); 170 | } else { 171 | return $callback($raw); 172 | } 173 | } 174 | 175 | /** 176 | * Set the attachments you want to bring with the refined items. 177 | * 178 | * @param string|array $attachments 179 | * @return $this 180 | */ 181 | public function bring($attachments) 182 | { 183 | if (is_string($attachments)) { 184 | $attachments = func_get_args(); 185 | } 186 | 187 | $this->attachments = $this->parseAttachments($attachments); 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * Get the classes needed for each attachment. 194 | * 195 | * @param array $relations 196 | * @return array 197 | */ 198 | protected function parseAttachments(array $relations) 199 | { 200 | $parsedRelations = []; 201 | 202 | foreach ($relations as $key => $relation) { 203 | if ( ! is_numeric($key)) { 204 | if (is_callable($relation)) { 205 | $parsedRelations[$key] = $this->attachItem($key); 206 | $parsedRelations[$key]['class']->with($this->attributes)->filter($relation); 207 | } else { 208 | $parsedRelations[$key] = $this->attachItem($key); 209 | $parsedRelations[$key]['class']->with($this->attributes)->bring($relation); 210 | } 211 | } else { 212 | $parsedRelations[$relation] = $this->attachItem($relation); 213 | 214 | if (isset($parsedRelations[$relation]['class'])) { 215 | $parsedRelations[$relation]['class']->with($this->attributes); 216 | } 217 | } 218 | } 219 | 220 | return $parsedRelations; 221 | } 222 | 223 | /** 224 | * Check the attachment method exists on the class being attached, if it 225 | * does then run the method. 226 | * 227 | * @param string $attachment 228 | * @return mixed 229 | * @throws RefineryMethodNotFound 230 | */ 231 | protected function attachItem($attachment) 232 | { 233 | if ( ! method_exists($this, $attachment)) { 234 | throw new RefineryMethodNotFound( 235 | "No attachment set with the name '{$attachment}' on '" . get_class($this) . "'." 236 | ); 237 | } 238 | 239 | return $this->$attachment(); 240 | } 241 | 242 | /** 243 | * Set the class to be used for the attachment. 244 | * 245 | * @param string|callable $className 246 | * @param callable|bool $callback 247 | * @return array 248 | * @throws AttachmentClassNotFound 249 | */ 250 | public function attach($className, callable $callback = null) 251 | { 252 | // If the user has passed through a callable item then we want 253 | // to attach the raw result of that call. 254 | if (is_callable($className)) { 255 | return ['raw' => $className]; 256 | } 257 | 258 | if ( ! class_exists($className)) { 259 | throw new AttachmentClassNotFound("No class found with the name '{$className}'."); 260 | } 261 | 262 | return ['class' => new $className, 'callback' => $callback]; 263 | } 264 | 265 | /** 266 | * Alias for the attach method. 267 | * 268 | * @param string $className 269 | * @param callable $callback 270 | * @return array 271 | * @throws AttachmentClassNotFound 272 | */ 273 | public function embed($className, callable $callback = null) 274 | { 275 | return $this->attach($className, $callback); 276 | } 277 | 278 | /** 279 | * Alias for the attach method. 280 | * 281 | * @param string $className 282 | * @param callable $callback 283 | * @return array 284 | * @throws AttachmentClassNotFound 285 | */ 286 | public function nest($className, callable $callback = null) 287 | { 288 | return $this->attach($className, $callback); 289 | } 290 | 291 | /** 292 | * Set a filter to run on the raw data. 293 | * 294 | * @param callable $filter 295 | * @return Refinery 296 | */ 297 | protected function setFilter(callable $filter) 298 | { 299 | $this->filter = $filter; 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * Alias for the setFilter method. 306 | * 307 | * @param callable $filter 308 | * @return Refinery 309 | */ 310 | protected function filter(callable $filter) 311 | { 312 | return $this->setFilter($filter); 313 | } 314 | 315 | /** 316 | * Check if a filter has been set 317 | * 318 | * @return boolean 319 | */ 320 | protected function hasFilter() 321 | { 322 | return ! empty($this->filter); 323 | } 324 | 325 | /** 326 | * Return the filter callback 327 | * 328 | * @return Closure 329 | */ 330 | protected function getFilter() 331 | { 332 | return $this->filter; 333 | } 334 | 335 | /** 336 | * Merge a set of data into another. 337 | * 338 | * @param mixed $original 339 | * @param mixed $merge 340 | * @return mixed 341 | */ 342 | protected function merge($original, $merge) 343 | { 344 | if (is_array($original) && is_array($merge)) { 345 | return array_merge($original, $merge); 346 | } 347 | 348 | if (is_array($original)) { 349 | foreach ($merge as $key => $value) { 350 | $original[$key] = $value; 351 | } 352 | } else { 353 | foreach ($merge as $key => $value) { 354 | $original->$key = $value; 355 | } 356 | } 357 | 358 | return $original; 359 | } 360 | 361 | /** 362 | * Check if the provided array is multidimensional. 363 | * 364 | * @param array $array 365 | * @return bool 366 | */ 367 | protected function isMultidimensional(array $array) 368 | { 369 | foreach ($array as $element) { 370 | if ( ! is_array($element) && ! is_object($element)) { 371 | return false; 372 | } 373 | } 374 | 375 | return true; 376 | } 377 | 378 | /** 379 | * Store the passed items within the $attributes property. 380 | * 381 | * @param array $items 382 | * @return $this 383 | */ 384 | public function with(array $items) 385 | { 386 | $this->attributes = $items; 387 | 388 | return $this; 389 | } 390 | 391 | /** 392 | * Get the current key. 393 | * 394 | * @return mixed 395 | */ 396 | public function getKey() 397 | { 398 | return $this->key; 399 | } 400 | 401 | /** 402 | * Use the __get magic method to access the items within the $attributes 403 | * array is if you were accessing a property on the class. 404 | * 405 | * @param $key 406 | * @return null 407 | */ 408 | public function __get($key) 409 | { 410 | if (array_key_exists($key, $this->attributes)) { 411 | return $this->attributes[$key]; 412 | } 413 | 414 | return null; 415 | } 416 | } --------------------------------------------------------------------------------