├── .gitignore ├── phpunit.xml ├── composer.json ├── src ├── traits │ ├── Delegates.php │ └── GettersSetters.php ├── Str.php └── Presenter.php ├── LICENSE ├── tests ├── PresenterTest.php ├── DelegatesTest.php └── GettersSettersTest.php ├── examples └── example.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"brandonshar/presenters", 3 | "description": "JSONable Presenters Objects", 4 | "require": { 5 | "php": ">=7" 6 | }, 7 | "require-dev": { 8 | "phpunit/phpunit": "~6.0", 9 | "mockery/mockery": "0.9.*" 10 | }, 11 | "autoload": { 12 | "psr-4": { 13 | "brandonshar\\": "src/" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/traits/Delegates.php: -------------------------------------------------------------------------------- 1 | handleDelegates($attr); 10 | } 11 | 12 | protected function handleDelegates($attr) 13 | { 14 | if ($this->hasDelegateFor($attr)) { 15 | return $this->{$this->delegateFor($attr)}->$attr; 16 | } 17 | } 18 | 19 | protected function hasDelegateFor($attr) 20 | { 21 | return (bool) $this->delegateFor($attr); 22 | } 23 | 24 | private function delegateFor($attr) 25 | { 26 | foreach ($this->delegatesTo as $delegate => $attributes) { 27 | if (in_array($attr, $attributes)) { 28 | return $delegate; 29 | } 30 | } 31 | } 32 | 33 | 34 | } -------------------------------------------------------------------------------- /src/Str.php: -------------------------------------------------------------------------------- 1 | 'my title']; 13 | $presenter = BasicPresenter::present($obj)->tap(function($p) { 14 | $p->data = 'some data'; 15 | $p->test = 'my test'; 16 | }); 17 | 18 | $this->assertEquals( 19 | json_decode(json_encode($presenter), true), 20 | [ 21 | 'data' => 'some data', 22 | 'title' => 'my title', 23 | 'test' => 'MY TEST', 24 | 'read_only' => 'some attribute', 25 | ] 26 | ); 27 | } 28 | } 29 | 30 | class BasicPresenter extends Presenter { 31 | 32 | protected $obj; 33 | protected $delegatesTo = [ 34 | 'obj' => ['title'] 35 | ]; 36 | 37 | public function __construct($obj) 38 | { 39 | $this->obj = $obj; 40 | } 41 | 42 | public function getReadOnlyAttribute() 43 | { 44 | return 'some attribute'; 45 | } 46 | 47 | public function getTestAttribute($test) 48 | { 49 | return strtoupper($test); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/DelegatesTest.php: -------------------------------------------------------------------------------- 1 | a = new DelegateeA; 19 | $this->b = new DelegateeB; 20 | $this->stub = new DelegatesStub( 21 | $this->a, 22 | $this->b 23 | ); 24 | } 25 | 26 | public function test_it_delegates_to_the_correct_method() 27 | { 28 | $this->stub->delegatesTo = [ 29 | 'a' => ['title', 'description'], 30 | 'b' => ['something', 'somethingElse'], 31 | ]; 32 | 33 | $this->assertEquals($this->stub->title, 'the title'); 34 | $this->assertEquals($this->stub->description, 'lorem ipsum'); 35 | $this->assertEquals($this->stub->something, 'hello'); 36 | $this->assertEquals($this->stub->somethingElse, 'something else'); 37 | 38 | } 39 | } 40 | 41 | class DelegatesStub 42 | { 43 | use Delegates; 44 | 45 | public $a; 46 | public $b; 47 | public $delegatesTo = []; 48 | 49 | public function __construct($a, $b) 50 | { 51 | $this->a = $a; 52 | $this->b = $b; 53 | } 54 | } 55 | 56 | class DelegateeA 57 | { 58 | public $title = 'the title'; 59 | public $description = 'lorem ipsum'; 60 | } 61 | 62 | class DelegateeB 63 | { 64 | public $something = 'hello'; 65 | public $somethingElse = 'something else'; 66 | } -------------------------------------------------------------------------------- /examples/example.php: -------------------------------------------------------------------------------- 1 | ['title', 'condition'], 31 | 'dealership' => ['name'], 32 | ]; 33 | 34 | public function __construct(Vehicle $vehicle, Dealership $dealership) 35 | { 36 | $this->vehicle = $vehicle; 37 | $this->dealership = $dealership; 38 | } 39 | 40 | public function getAddressAttribute() 41 | { 42 | return "{$this->dealership->city}, {$this->dealership->state}"; 43 | } 44 | 45 | public function getDataPulledOnAttribute($passedInDate) 46 | { 47 | return DateTime::createFromFormat('Y-m-d', $passedInDate)->format('l, M jS'); 48 | } 49 | } 50 | 51 | $vehicle = new Vehicle; 52 | $dealership = new Dealership; 53 | $vehiclePresenter = VehiclePresenter::present($vehicle, $dealership)->tap(function ($p) { 54 | $p->dataPulledOn = date('Y-m-d'); 55 | }); 56 | 57 | echo json_encode($vehiclePresenter) . PHP_EOL; 58 | echo $vehiclePresenter->toJson() . PHP_EOL; 59 | 60 | //{"title":"2014 Fiat 500L","condition":"Used","name":"Value Cars","data_pulled_on":"Tuesday, Jul 11th","address":"Boston, MA"} 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/Presenter.php: -------------------------------------------------------------------------------- 1 | handleGetters($attr) ?? $this->handleDelegates($attr); 24 | } 25 | 26 | public function tap(callable $callback) 27 | { 28 | $callback($this); 29 | 30 | return $this; 31 | } 32 | 33 | public function toJson() 34 | { 35 | return json_encode($this); 36 | } 37 | 38 | public function jsonSerialize() 39 | { 40 | $results = []; 41 | 42 | foreach ($this->delegatesTo as $delegate => $attributes) { 43 | foreach ($attributes as $attribute) { 44 | $results[$attribute] = $this->$attribute; 45 | } 46 | } 47 | 48 | $results = array_merge($results, $this->attributes); 49 | 50 | foreach (get_class_methods($this) as $method) { 51 | if (substr($method, 0, 3) === 'get' && substr($method, -9) === 'Attribute') { 52 | $attribute = $this->convertStringCase(substr($method, 3, strlen($method) - 12)); 53 | if ($attribute) { 54 | $results[$attribute] = $this->$attribute; 55 | } 56 | } 57 | } 58 | 59 | return $results; 60 | } 61 | 62 | protected function convertStringCase($value) 63 | { 64 | return Str::{$this->jsonEncodeCase}($value); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/traits/GettersSetters.php: -------------------------------------------------------------------------------- 1 | handleGetters($attr); 14 | } 15 | 16 | public function __set($attr, $value) 17 | { 18 | $this->handleSetters($attr, $value); 19 | } 20 | 21 | protected function handleGetters($attr) 22 | { 23 | if ($this->hasGetter($attr)) { 24 | return $this->{$this->getGetterMethodName($attr)}($this->getAttribute($attr)); 25 | } 26 | 27 | return $this->getAttribute($attr); 28 | } 29 | 30 | protected function handleSetters($attr, $value) 31 | { 32 | if ($this->hasSetter($attr)) { 33 | $this->{$this->getSetterMethodName($attr)}($value); 34 | } else { 35 | $this->setAttribute($attr, $value); 36 | } 37 | } 38 | 39 | protected function setAttribute($key, $attr) 40 | { 41 | $this->attributes[$this->convertStringCase($key)] = $attr; 42 | } 43 | 44 | protected function getAttribute($key) 45 | { 46 | return $this->attributes[$this->convertStringCase($key)] ?? null; 47 | } 48 | 49 | protected function convertStringCase($value) 50 | { 51 | return Str::snake($value); 52 | } 53 | 54 | private function getGetterMethodName($attr) 55 | { 56 | return 'get' . Str::studly($attr) . 'Attribute'; 57 | } 58 | 59 | private function hasGetter($attr) 60 | { 61 | return method_exists($this, $this->getGetterMethodName($attr)); 62 | } 63 | 64 | private function getSetterMethodName($attr) 65 | { 66 | return 'set' . Str::studly($attr) . 'Attribute'; 67 | } 68 | 69 | private function hasSetter($attr) 70 | { 71 | return method_exists($this, $this->getSetterMethodName($attr)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/GettersSettersTest.php: -------------------------------------------------------------------------------- 1 | title = 'test title'; 14 | 15 | $this->assertEquals($stub->title, 'getting: test title'); 16 | } 17 | 18 | public function test_it_gets_flexibly() 19 | { 20 | $stub = new GettersSettersStub; 21 | 22 | $this->assertEquals($stub->read_only, 'hello there'); 23 | $this->assertEquals($stub->readOnly, 'hello there'); 24 | } 25 | 26 | public function test_it_sets_flexibly() 27 | { 28 | $stub = new GettersSettersStub; 29 | $stub->someValue = 'some value'; 30 | $stub->other_value = 'other value'; 31 | 32 | $this->assertEquals($stub->some_value, 'some value'); 33 | $this->assertEquals($stub->otherValue, 'other value'); 34 | } 35 | 36 | public function test_it_sets_without_getter() 37 | { 38 | $stub = new GettersSettersStub; 39 | $stub->description = 'the test description'; 40 | 41 | $this->assertEquals($stub->description, 'setting: the test description'); 42 | } 43 | 44 | public function test_it_gets_and_sets() 45 | { 46 | $stub = new GettersSettersStub; 47 | $stub->other = 'some great text'; 48 | 49 | $this->assertEquals($stub->other, 'I GOT SOME GREAT TEXT'); 50 | } 51 | } 52 | 53 | class GettersSettersStub 54 | { 55 | use GettersSetters; 56 | 57 | public function getReadOnlyAttribute() 58 | { 59 | return 'hello there'; 60 | } 61 | 62 | public function getTitleAttribute($title) 63 | { 64 | return "getting: {$title}"; 65 | } 66 | 67 | public function setOtherAttribute($value) 68 | { 69 | $this->setAttribute('other', strtoupper($value)); 70 | } 71 | 72 | public function getOtherAttribute($value) 73 | { 74 | return "I GOT {$value}"; 75 | } 76 | 77 | public function setDescriptionAttribute($value) 78 | { 79 | $this->setAttribute('description', "setting: {$value}"); 80 | } 81 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Presenters 2 | 3 | **WARNING** While this is tested, I have not used it in a production or extensively in a development environment yet. I'm pretty sure I'm going to love using it, but until I do that I feel obligated to offer a warning about its usefulness. 4 | ### Installation 5 | Install via composer and just start using it. Couldn't be easier! 6 | ``` 7 | composer require brandonshar/presenters 8 | ``` 9 | ### So what is this? 10 | Have you ever had a model, or combination of models, that you wanted to return with different data depending on the endpoint? Maybe you have a resource controller called `VehicleOnCraigslistController` and when someone hits your `VehicleOnCraigslistController@show` method, you want to return a set of data that represents a vehicle listed on Craigslist. This set of data is likely comprised of your `Vehicle` model and your `CraigslistAd` model, but different than what you return from your `VehicleController@show` method, your `CraigslistAdController@show`, or just a combination of the two. This is where **Presenters** come in to save the day! 11 | 12 | ### How do I do those great sounding things? 13 | Presenters have a number of great features. Let's see them all in action! 14 | 15 | #### Basic Presenter Example 16 | ```php 17 | use brandonshar\Presenter; 18 | 19 | class VehicleOnCraigslistPresenter extends Presenter 20 | { 21 | protected $vehicle; 22 | protected $craigslistAd; 23 | 24 | public function __construct(Vehicle $vehicle, CraigslistAd $craigslistAd) 25 | { 26 | $this->vehicle = $vehicle; 27 | $this->craigslistAd = $craigslistAd; 28 | } 29 | } 30 | 31 | $presenter = VehicleOnCraigslistPresenter::present($vehicle, $vehicle->craiglistAd); 32 | ``` 33 | There's the minimum code for a presenter! (`present` is just some nice syntactic sugar that provides a more fluent way to instantiate your presenter. If it makes you nervous, you can always new it up like usual). 34 | 35 | Ok... I can see that you're not very impressed yet. Let's do things with it! 36 | #### Delegation 37 | Delegation allow you to reach through your presenter to the models (or other objects) it contains while avoiding much of the boilerplate of typical getters. 38 | ```php 39 | class VehicleOnCraigslistPresenter extends Presenter 40 | { 41 | //... 42 | protected $delegatesTo = [ 43 | 'vehicle' => ['year', 'make'], 44 | 'craigslistAd' => ['listedAt'], 45 | ]; 46 | //... 47 | } 48 | ``` 49 | Now we can "reach through" the presenter to access the delegates properties! 50 | ```php 51 | $presenter = VehicleOnCraigslistPresenter::present($vehicle, $vehicle->craiglistAd); 52 | 53 | $presenter->year; //$presenter->vehicle->year; 54 | $presenter->make; //$presenter->vehicle->make; 55 | $presenter->listedAt; // $presenter->craigslistAd->listedAt; 56 | ``` 57 | Ok, a little cooler, right? What if we need something more complicated? 58 | #### Getters (Accessors) 59 | The presenter has getters that work just like Laravel's Eloquent getters: just create a method called getPropertyNameAttribute (if your property is called "propertyName") that optionally accepts a single argument (the currently set value of the named property) : 60 | ```php 61 | class VehicleOnCraigslistPresenter extends Presenter 62 | { 63 | //... 64 | public function getVehicleTitleAttribute() 65 | { 66 | return "{$this->vehicle->year} {$this->vehicle->make} {$this->vehicle->model}"; 67 | } 68 | 69 | public function getCachedAtAttribute($currentValue) 70 | { 71 | return DateTime::createFromFormat('Y-m-d', $currentValue)->format('l, M jS'); 72 | } 73 | //... 74 | } 75 | ``` 76 | We can now access this like any other property: 77 | ```php 78 | $presenter->vehicleTitle; 79 | //or if you prefer 80 | $presenter->vehicle_title; 81 | ``` 82 | Both camelCase and snake_case work the same. 83 | If you put information into your presenter manually, it will automatically be passed as an argument to your getter. 84 | ```php 85 | $presenter->cachedAt = '2017-09-21'; 86 | echo $presenter->cachedAt; //Thursday, Sept 21st 87 | ``` 88 | **Note:** Wish you had a better way to put data on your presenter? Just hang tight for the tap method below! 89 | ##### getAttribute 90 | If you need to access an attribute that isn't the attribute named in your getter, you should use the `getAttribute` method: 91 | ```php 92 | return $this->getAttribute('cachedAt'); 93 | //or (they both work the same) 94 | return $this->getAttribute('cached_at'); 95 | ``` 96 | You could go directly to the `attributes` array property, but this requires you to make sure you match case with the json casting below (defaulting to snake_case). Because of this, the preferred way is to use the get (and corresponding set) attribute methods. 97 | #### Setters (Mutators) 98 | Technically, the presenters do have setters, but I haven't thought of a use case for them. They exist in order to aid the getters, but could be used standalone. I haven't thought of a real use case for them, but here's a quick example in case you do: 99 | ```php 100 | public function setSomeExampleAttribute($value) 101 | { 102 | $this->setAttribute('someExample', strtoupper($value)); 103 | } 104 | ``` 105 | Now you can: 106 | ```php 107 | $presenter->someExample = 'my message'; 108 | echo $presenter->someExample; //MY MESSAGE 109 | ``` 110 | ##### setAttribute 111 | Just like getting above, the preferred way to set internal attributes is as follows: 112 | ```php 113 | $this->setAttribute('someExample', $value); 114 | // or 115 | $this->setAttribute('some_example', $value); 116 | ``` 117 | #### Tap 118 | I'm a sucker for fluid syntax and avoiding temporary variables, so if you want to add some instance variables to your presenter when it's instantiated, you can do so with the tap method: 119 | ```php 120 | $presenter = VehicleOnCraigslistPresenter::present($vehicle, $vehicle->craiglistAd)->tap(function ($presenter) { 121 | $presenter->cachedAt = date('Y-m-d'); 122 | }); 123 | 124 | echo $presenter->cachedAt; //Thursday, Sept 21st (don't forget our getter from above) 125 | ``` 126 | #### Json 127 | I try to exclusively develop API endpoints to be consumed by my front-end, so my use cases for this are always json. If you're like me, then you'll be happy to know that you have three great ways to get json out of your presenter: 128 | You can use the toJson method: 129 | ```php 130 | echo $presenter->toJson(); 131 | ``` 132 | You can simply use the php function json_encode (presenters implement `JsonSerializable`) 133 | ```php 134 | echo json_encode($presenter); 135 | ``` 136 | OR if you use Laravel (and if you don't, you should strongly reconsider), you can just directly return presenters from any controller route and they'll be converted to json automatically! 137 | This is where the code really starts to get sexy: 138 | ```php 139 | return VehicleOnCraigslistPresenter::present($vehicle, $vehicle->craiglistAd)->tap(function ($p) { 140 | $p->cachedAt = date('Y-m-d'); 141 | }); 142 | ``` 143 | 144 | #### Wait, what is converted to JSON? 145 | Presenters automatically turn all of their delegates, any getters, and any variables that were manually set (through tap or otherwise) as JSON. 146 | Our json from the presenter we've been building will look something like this: 147 | ```javascript 148 | {"year": 2015, "make": "Chevy", "listed_at": "2017-08-01", "vehicle_title": "2015 Chevy Volt", "cached_at": "Thursday, Sept 21st", "some_example": "MY MESSAGE"} 149 | ``` 150 | 151 | #### But hang on, I prefer my JSON to be camelCase? Is there any hope for me? 152 | Oh come on, how on earth would we manage to make something like.. OH WAIT ONE SECOND 153 | ```php 154 | class VehicleOnCraigslistPresenter extends Presenter 155 | { 156 | //... 157 | protected $jsonEncodeCase = 'camel'; 158 | //... 159 | } 160 | ``` 161 | Happy? Valid options are `camel`, `snake`, or (if you're an irredeemable monster), `studly` 162 | 163 | ## That's all for now! I hope to update as I begin using this in my projects more and would be irrationally thrilled to receive a pull request if you think you can improve it. 164 | 165 | --------------------------------------------------------------------------------