├── .chipperci.yml ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── MakePresenterCommand.php ├── Presentable.php ├── Presenter.php ├── Presenter.stub ├── PresenterFactory.php ├── PresenterServiceProvider.php └── helpers.php └── tests ├── Fixtures ├── CamelCaseAttributesPresenter.php ├── HiddenAndVisibleAttributesPresenter.php ├── HiddenAttributesPresenter.php ├── User.php ├── UserFactory.php ├── UserProfilePresenter.php ├── UserWithDefaultPresenter.php ├── UserWithDefaultPresenterFactory.php └── VisibleAttributesPresenter.php ├── IntegrationTest.php ├── Migrations └── 2019_03_07_000000_create_users_table.php └── PresenterTest.php /.chipperci.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | environment: 4 | php: 7.4 5 | node: 12 6 | 7 | 8 | 9 | pipeline: 10 | - name: Setup 11 | cmd: | 12 | composer install --no-interaction --prefer-dist --optimize-autoloader 13 | 14 | - name: PHPUnit 15 | cmd: | 16 | phpunit 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: true 11 | matrix: 12 | php: [8.3, 8.2, 8.1, 8.0] 13 | laravel: [11.*, 10.*, 9.*, 8.*] 14 | os: [ubuntu-latest] 15 | dependency-version: [prefer-stable] 16 | include: 17 | - laravel: 11.* 18 | testbench: 9.* 19 | - laravel: 10.* 20 | testbench: 8.* 21 | - laravel: 9.* 22 | testbench: 7.* 23 | - laravel: 8.* 24 | testbench: ^6.23 25 | exclude: 26 | - laravel: 11.* 27 | php: 8.1 28 | - laravel: 11.* 29 | php: 8.0 30 | - laravel: 10.* 31 | php: 8.0 32 | 33 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 34 | 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v3 38 | 39 | - name: Setup PHP 40 | uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: ${{ matrix.php }} 43 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 44 | coverage: none 45 | 46 | - name: Install dependencies 47 | run: | 48 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 49 | 50 | - name: Execute tests 51 | run: vendor/bin/phpunit 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit.xml 4 | .phpunit.result.cache 5 | /.idea 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Hemphill 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌿 Hemp Presenter 2 | 3 | This package makes it fast, fun, and profitable to decorate your Eloquent models for presentation in views, PDFs, CSV files, or anywhere else in your project. 4 | 5 | For a little primer on the problems presenters solve, take a look at this article: [Presenters in Laravel](https://gistlog.co/davidhemphill/1b7c749cdd6520eb19320c208ef32433). 6 | 7 | ## Installation 8 | 9 | Install the package via [Composer](https://getcomposer.org/): 10 | 11 | ``` 12 | composer require hemp/presenter 13 | ``` 14 | 15 | In Laravel 5.5+, the package's service provider should be auto-discovered, so you won't need to register it. If for some reason you need to register it manually you can do so by adding it to the `providers` array in `config/app.php`: 16 | 17 | ```php 18 | 'providers' => [ 19 | // ... 20 | Hemp\Presenter\PresenterServiceProvider::class, 21 | ], 22 | ``` 23 | 24 | ## Creating `Presenter` Classes 25 | 26 | You can easily generate a `Presenter` class by calling the `make:presenter` Artisan command: 27 | 28 | ```sh 29 | php artisan make:presenter ApiPresenter 30 | ``` 31 | 32 | This will generate an empty `Presenter` class inside of `app/Presenters`. 33 | 34 | ## Customizing `Presenter` Classes 35 | 36 | At their core, presenters are simple classes designed to encapsulate complex or repetitive view logic. What makes `hemp/presenter` nice is it allows you to attach magic accessors to these `Presenter` objects all the while allowing for the typical serialization workflow of using regular `Model` objects and collections. For example, take this `ApiPresenter` class: 37 | 38 | ```php 39 | created_at->format('n/j/Y'); 50 | } 51 | 52 | public function getFullNameAttribute() 53 | { 54 | return trim($this->first_name . ' ' . $this->last_name); 55 | } 56 | } 57 | ``` 58 | 59 | This class has a custom method `createdDate` that can be called wherever this `Presenter` is used. It also has a magic accessor `getFullNameAttribute` that will be accessible via the `Presenter` like so: `$user->full_name`. This works exactly like Eloquent's magic accessors...when the `Presenter` is serialized into a response (like for a view or API response), these magic accessors will be called and added to the rendered output. 60 | 61 | You'll notice we're calling `$this->first_name` and `$this->last_name`. These are not available on the `Presenter` class itself, but are being delegated to the underlying `Model` instance. 62 | 63 | This `Presenter` might output something like this: 64 | 65 | ```json5 66 | { 67 | id: 1, 68 | first_name: 'David', 69 | last_name: 'Hemphill', 70 | created_at: '2016-10-14 12:00:00', 71 | updated_at: '2016-12-14 12:00:00', 72 | full_name: 'David Hemphill', // The magic accessor 73 | } 74 | ``` 75 | 76 | Once you have a presented model instance (like inside a Blade view), you can use magic accessors like this: 77 | 78 | ```php 79 | $presentedUser->full_name; 80 | ``` 81 | 82 | Or use the methods available on the `Presenter` itself: 83 | 84 | ```php 85 | $presentedUser->createdAt(); 86 | ``` 87 | 88 | When outputting the `Presenter` to an `array` or JSON, if you'd like each of the rendered attributes to use `camelCase` formatting instead of the default `snake_case` formatting, you can set the `snakeCase` property on your `Presenter` to `false`: 89 | 90 | ```php 91 | class ApiPresenter extends Presenter 92 | { 93 | public $snakeCase = false; 94 | } 95 | ``` 96 | 97 | This will cause the rendered output to look like this: 98 | 99 | ```json 100 | { 101 | "id": 1, 102 | "firstName": "David", 103 | "lastName": "Hemphill", 104 | "createdAt": "2016-10-14 12:00:00", 105 | "updatedAt": "2016-12-14 12:00:00", 106 | "fullName": "David Hemphill" 107 | } 108 | ``` 109 | 110 | You might like this option if your front-end JavaScript style guide uses mostly camelCased variables. 111 | 112 | In addition, you can set the strategy used at runtime using the `snakeCase` and `camelCase` setters: 113 | 114 | ```php 115 | Presenter::make($user, ApiPresenter::class)->snakeCase(); 116 | Presenter::make($user, ApiPresenter::class)->camelCase(); 117 | ``` 118 | 119 | ## Presenting Single Models 120 | 121 | There are a number of different ways you can present your `Model` objects, depending on your personal preferences. For instance, you can use the `make` factory method of the `Presenter` class: 122 | 123 | ```php 124 | $user = User::first(); 125 | $presentedUser = Presenter::make($user, ApiPresenter::class); 126 | ``` 127 | 128 | You can also call the `make` method on any of your custom `Presenter` classes, without passing the second argument: 129 | 130 | ```php 131 | $user = User::first(); 132 | $presentedUser = ApiPresenter::make($user); 133 | ``` 134 | 135 | You may also use the `present` global function, if that's your jam: 136 | 137 | ```php 138 | $user = User::first(); 139 | $presentedUser = present($user, ApiPresenter::class); 140 | ``` 141 | 142 | Or you can use the `Hemp\Presenter\Presentable` trait on your `Model`. This will allow you to call `present` on it directly: 143 | 144 | ```php 145 | use Hemp\Presenter\Presentable; 146 | 147 | class User extends \Illuminate\Database\Eloquent\Model 148 | { 149 | use Presentable; 150 | } 151 | 152 | $presentedUser = User::first()->present(ApiPresenter::class); 153 | ``` 154 | 155 | Also, when using the `Presentable` trait, you can specify a default presenter using the `defaultPresenter` attribute on the `Model` and then calling `present`: 156 | 157 | ```php 158 | use Hemp\Presenter\Presentable; 159 | 160 | class User extends \Illuminate\Database\Eloquent\Model 161 | { 162 | use Presentable; 163 | 164 | public $defaultPresenter = App\Presenters\ApiPresenter::class; 165 | } 166 | 167 | $presentedUser = User::first()->present(); 168 | ``` 169 | 170 | ## Presenting Collections 171 | 172 | You can also create a collection of presented `Model` objects. One way is to use the static `collection` method on the `Presenter` class to present an array of `Model` objects: 173 | 174 | ```php 175 | $users = User::all(); 176 | $presenter = Presenter::collection($users, ApiPresenter::class); 177 | ``` 178 | 179 | You can also use the static `collection` method on any of your custom `Presenter` classes directly without passing the second argument: 180 | 181 | ```php 182 | $users = User::all(); 183 | $presenter = ApiPresenter::collection($users); 184 | ``` 185 | 186 | You may also use the `present` macro on a Collection object: 187 | 188 | ```php 189 | $presentedUsers = User::all()->present(ApiPresenter::class); 190 | ``` 191 | 192 | ## Hiding Model Attributes From Output 193 | 194 | There are times you may wish to keep certain keys from being rendered inside your `Presenter`. You can use the `hidden` property on the `Presenter` to keep any default `Model` attributes from being used in the output: 195 | 196 | ```php 197 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/MakePresenterCommand.php: -------------------------------------------------------------------------------- 1 | defaultPresenter; 19 | 20 | if (is_null($presenter)) { 21 | throw new BadMethodCallException('No presenter or default presenter passed to present()'); 22 | } 23 | 24 | return (new PresenterFactory)($this, $presenter); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Presenter.php: -------------------------------------------------------------------------------- 1 | model = $model; 51 | } 52 | 53 | /** 54 | * Render the output using camelCase keys. 55 | * 56 | * @return $this 57 | */ 58 | public function camelCase() 59 | { 60 | $this->snakeCase = false; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Render the output using snake_case keys. 67 | * 68 | * @return $this 69 | */ 70 | public function snakeCase() 71 | { 72 | $this->snakeCase = true; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * Create a new Presenter instance. 79 | * 80 | * @param Model $model 81 | * @param \Hemp\Presenter\Presenter|null $presenter 82 | * @return \Hemp\Presenter\Presenter 83 | */ 84 | public static function make(Model $model, $presenter = null) 85 | { 86 | return (new PresenterFactory)($model, $presenter ?? static::class); 87 | } 88 | 89 | /** 90 | * Return a collection of presented models. 91 | * 92 | * @param array|\Illuminate\Support\Collection $models 93 | * @return \Illuminate\Support\Collection 94 | */ 95 | public static function collection($models, $presenter = null) 96 | { 97 | return collect($models)->present($presenter ?? static::class); 98 | } 99 | 100 | /** 101 | * Return the keys for the presented model and cache the value. 102 | * 103 | * @return array 104 | */ 105 | protected function modelKeys() 106 | { 107 | return array_keys($this->model->toArray()); 108 | } 109 | 110 | /** 111 | * Call the Model's version of the method if available. 112 | * 113 | * @param string $method 114 | * @param array $args 115 | * @return mixed 116 | */ 117 | public function __call($method, $args) 118 | { 119 | return call_user_func_array([$this->model, $method], $args); 120 | } 121 | 122 | /** 123 | * Return the value of a magic accessor. 124 | * 125 | * @param string $name 126 | * @return mixed 127 | */ 128 | public function __get($attribute) 129 | { 130 | $method = $this->getStudlyAttributeMethod($attribute); 131 | 132 | // If the magic getter exists on this Presenter, let's call it and return the value, 133 | // passing in the original model instance, so the user can mutate it first. 134 | if (method_exists($this, $method)) { 135 | return $this->{$method}($this->model); 136 | } 137 | 138 | // If not, then let's delegate the magic call to the underlying Model instance. 139 | return $this->model->{$attribute}; 140 | } 141 | 142 | /** 143 | * Dynamically check a property exists on the underlying object. 144 | * 145 | * @param mixed $attribute 146 | * @return bool 147 | */ 148 | public function __isset($attribute) 149 | { 150 | $method = $this->getStudlyAttributeMethod($attribute); 151 | 152 | if (method_exists($this, $method)) { 153 | $value = $this->{$method}($this->model); 154 | 155 | return ! is_null($value); 156 | } 157 | 158 | return isset($this->model->{$attribute}); 159 | } 160 | 161 | /** 162 | * Convert the Presenter to a string. 163 | * 164 | * @return string 165 | */ 166 | public function __toString() 167 | { 168 | return $this->toJson(); 169 | } 170 | 171 | /** 172 | * Convert the Presenter to a JSON string. 173 | * 174 | * @return string 175 | */ 176 | public function toJson($options = 0) 177 | { 178 | return json_encode($this->toArray(), $options); 179 | } 180 | 181 | /** 182 | * Convert the Presenter to an array. 183 | * 184 | * @return array 185 | */ 186 | public function toArray() 187 | { 188 | return $this->processKeys( 189 | array_merge( 190 | $this->removeHiddenAttributes($this->model->toArray()), 191 | $this->additionalAttributes() 192 | ) 193 | ); 194 | } 195 | 196 | /** 197 | * Remove the non-visible attributes from the output. 198 | * 199 | * @param array $array 200 | * @return array 201 | */ 202 | protected function removeHiddenAttributes($attributes) 203 | { 204 | return Arr::only($attributes, $this->visibleAttributes()); 205 | } 206 | 207 | /** 208 | * Determine the visible attributes for the Presenter, taking into account the key might exist 209 | * in both the `hidden` and `visible` arrays. If a key is found in both, then let's assume 210 | * it is `visible`. 211 | * 212 | * @return void 213 | */ 214 | public function visibleAttributes() 215 | { 216 | if (empty($this->visible)) { 217 | return array_flip(Arr::except(array_flip($this->modelKeys()), $this->hidden)); 218 | } 219 | 220 | return array_flip(Arr::only(array_flip($this->modelKeys()), $this->visible)); 221 | } 222 | 223 | /** 224 | * Return the additional attributes for the Presenter. 225 | * 226 | * @return array 227 | */ 228 | protected function additionalAttributes() 229 | { 230 | return collect($this->availableAttributes())->mapWithKeys(function ($attribute) { 231 | $attributeKey = $this->snakeCase ? lcfirst(Str::snake($attribute)) : lcfirst(Str::camel($attribute)); 232 | 233 | return [$attributeKey => $this->mutateAttribute($attribute)]; 234 | })->all(); 235 | } 236 | 237 | /** 238 | * Return the mutable attributes for the Presenter;. 239 | * 240 | * @return array 241 | */ 242 | protected function availableAttributes() 243 | { 244 | return collect($this->getAttributeMatches())->map(function ($attribute) { 245 | return lcfirst(Str::snake($attribute)); 246 | }); 247 | } 248 | 249 | /** 250 | * Get any attributes with accessors defined on the Presenter. 251 | * 252 | * @return array 253 | */ 254 | protected function getAttributeMatches() 255 | { 256 | return with(implode(';', get_class_methods(static::class)), function ($attributeMethods) { 257 | preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', $attributeMethods, $matches); 258 | 259 | return $matches[1]; 260 | }); 261 | } 262 | 263 | /** 264 | * Mutate the given Presenter attribute. 265 | * 266 | * @param string $attribute 267 | * @return string 268 | */ 269 | protected function mutateAttribute($attribute) 270 | { 271 | return $this->{$this->getStudlyAttributeMethod($attribute)}($this->model); 272 | } 273 | 274 | /** 275 | * Get the studly attribute method name. 276 | * 277 | * @param string $attribute 278 | * @return string 279 | */ 280 | protected function getStudlyAttributeMethod($attribute) 281 | { 282 | $studlyAttribute = Str::studly($attribute); 283 | 284 | return "get{$studlyAttribute}Attribute"; 285 | } 286 | 287 | /** 288 | * Process the given attribute's keys. 289 | * 290 | * @param array $attributes 291 | * @return array 292 | */ 293 | protected function processKeys($attributes) 294 | { 295 | return collect($attributes)->mapWithKeys(function ($value, $key) { 296 | return [ 297 | lcfirst($this->snakeCase ? Str::snake($key) : Str::camel($key)) => $value, 298 | ]; 299 | })->all(); 300 | } 301 | 302 | /** 303 | * Determine if the given offset exists on the Presenter. 304 | * 305 | * @param string $offset 306 | * @return bool 307 | */ 308 | public function offsetExists($offset): bool 309 | { 310 | return $this->{$offset} !== null; 311 | } 312 | 313 | /** 314 | * Retrieve the value at the given offset. 315 | * 316 | * @param string $offset 317 | * @return mixed 318 | */ 319 | public function offsetGet($offset): mixed 320 | { 321 | return $this->{$offset}; 322 | } 323 | 324 | /** 325 | * Set the value at the given offset. 326 | * 327 | * @param string $offset 328 | * @param string $value 329 | * 330 | * @throws BadMethodCallException 331 | */ 332 | public function offsetSet($offset, $value): void 333 | { 334 | throw new BadMethodCallException('Hemp/Presenter does not support write methods'); 335 | } 336 | 337 | /** 338 | * Unset the value at the given offset. 339 | * 340 | * @param string $offset 341 | * 342 | * @throws BadMethodCallException 343 | */ 344 | public function offsetUnset($offset): void 345 | { 346 | throw new BadMethodCallException('Hemp/Presenter does not support write methods'); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/Presenter.stub: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 22 | } 23 | 24 | public function __get($attribute) 25 | { 26 | return $this->attributes[$attribute]; 27 | } 28 | }; 29 | } 30 | 31 | return new $presenter($model); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PresenterServiceProvider.php: -------------------------------------------------------------------------------- 1 | transform(function ($object) use ($class) { 19 | return (new PresenterFactory)($object, $class); 20 | }); 21 | }); 22 | } 23 | 24 | /** 25 | * Register any application services. 26 | * 27 | * @return void 28 | */ 29 | public function register() 30 | { 31 | $this->commands([ 32 | MakePresenterCommand::class, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | name)[0]; 14 | } 15 | 16 | public function getLastNameAttribute() 17 | { 18 | return explode(' ', $this->name)[1]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/HiddenAndVisibleAttributesPresenter.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $model = User::class; 16 | 17 | /** 18 | * Define the model's default state. 19 | * 20 | * @return array 21 | */ 22 | public function definition(): array 23 | { 24 | return [ 25 | 'name' => $this->faker->name(), 26 | 'email' => $this->faker->unique()->safeEmail(), 27 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 28 | 'remember_token' => Str::random(10), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Fixtures/UserProfilePresenter.php: -------------------------------------------------------------------------------- 1 | setUpDatabase(); 17 | } 18 | 19 | protected function setUpDatabase() 20 | { 21 | Schema::create('users', function (Blueprint $table) { 22 | $table->increments('id'); 23 | $table->string('name'); 24 | $table->string('email')->unique(); 25 | $table->string('password'); 26 | $table->rememberToken(); 27 | $table->timestamps(); 28 | $table->softDeletes(); 29 | }); 30 | } 31 | 32 | protected function getPackageProviders($app) 33 | { 34 | return [ 35 | \Hemp\Presenter\PresenterServiceProvider::class, 36 | ]; 37 | } 38 | 39 | protected function getEnvironmentSetUp($app) 40 | { 41 | $app['config']->set('database.default', 'testbench'); 42 | $app['config']->set('database.connections.testbench', [ 43 | 'driver' => 'sqlite', 44 | 'database' => ':memory:', 45 | 'prefix' => '', 46 | ]); 47 | } 48 | 49 | protected function defineWebRoutes($router) 50 | { 51 | Route::get('/users', function () { 52 | return \Hemp\Presenter\Tests\Fixtures\User::all()->present(function ($user) { 53 | return ['full_name' => $user->name]; 54 | }); 55 | }); 56 | 57 | Route::get('/paginated', function () { 58 | return \Hemp\Presenter\Tests\Fixtures\User::paginate(1)->present(function ($user) { 59 | return ['full_name' => $user->name]; 60 | }); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Migrations/2019_03_07_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->string('password'); 21 | $table->rememberToken(); 22 | $table->string('restricted')->default('Yes'); 23 | $table->timestamps(); 24 | $table->softDeletes(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('users'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/PresenterTest.php: -------------------------------------------------------------------------------- 1 | create(); 23 | $presenter = new class($user) extends Presenter { 24 | }; 25 | $this->assertSame($user, $presenter->getModel()); 26 | } 27 | 28 | /** @test */ 29 | public function its_own_methods_take_priority() 30 | { 31 | $user = User::factory()->create(); 32 | $presenter = new class($user) extends Presenter 33 | { 34 | public function middleName() 35 | { 36 | return 'Isles'; 37 | } 38 | }; 39 | 40 | $this->assertEquals('Isles', $presenter->middleName()); 41 | } 42 | 43 | /** @test */ 44 | public function delegates_undefined_method_calls_to_the_underlying_model_instance() 45 | { 46 | $user = User::factory()->create(); 47 | $presenter = new class($user) extends Presenter { 48 | }; 49 | $this->assertEquals('Hello from the Model!', $presenter->sayHello()); 50 | } 51 | 52 | /** @test */ 53 | public function delegates_magic_properties_to_the_presenter() 54 | { 55 | $user = User::factory()->create(); 56 | $presenter = new class($user) extends Presenter 57 | { 58 | public function getSayHelloAttribute() 59 | { 60 | return 'Hello from the Presenter!'; 61 | } 62 | }; 63 | 64 | $this->assertEquals('Hello from the Presenter!', $presenter->say_hello); 65 | } 66 | 67 | /** @test */ 68 | public function can_be_converted_to_an_array() 69 | { 70 | Carbon::setTestNow(Carbon::parse('Oct 14 2019')); 71 | 72 | $user = User::factory()->create([ 73 | 'name' => 'David Hemphill', 74 | 'email' => 'david@laravel.com', 75 | 'created_at' => Carbon::now()->subDays(4), 76 | 'updated_at' => Carbon::now(), 77 | ]); 78 | 79 | $presenter = new class($user) extends Presenter 80 | { 81 | public function getCreatedAtAttribute($model) 82 | { 83 | return $model->created_at->format('M j, Y'); 84 | } 85 | 86 | public function getUpdatedAtAttribute($model) 87 | { 88 | return $model->updated_at->format('M j, Y'); 89 | } 90 | }; 91 | 92 | $this->assertEquals([ 93 | 'id' => $user->getKey(), 94 | 'name' => 'David Hemphill', 95 | 'email' => 'david@laravel.com', 96 | 'created_at' => 'Oct 10, 2019', 97 | 'updated_at' => 'Oct 14, 2019', 98 | ], $presenter->toArray()); 99 | } 100 | 101 | /** @test */ 102 | public function can_be_converted_to_json() 103 | { 104 | Carbon::setTestNow(Carbon::parse('Oct 14 2019')); 105 | 106 | $user = User::factory()->create([ 107 | 'name' => 'David Hemphill', 108 | 'email' => 'david@laravel.com', 109 | 'created_at' => Carbon::now()->subDays(4), 110 | 'updated_at' => Carbon::now(), 111 | ]); 112 | 113 | $presenter = new class($user) extends Presenter 114 | { 115 | public function getCreatedAtAttribute($model) 116 | { 117 | return $model->created_at->format('M j, Y'); 118 | } 119 | 120 | public function getUpdatedAtAttribute($model) 121 | { 122 | return $model->updated_at->format('M j, Y'); 123 | } 124 | }; 125 | 126 | $this->assertEquals(json_encode([ 127 | 'name' => 'David Hemphill', 128 | 'email' => 'david@laravel.com', 129 | 'created_at' => 'Oct 10, 2019', 130 | 'updated_at' => 'Oct 14, 2019', 131 | 'id' => $user->getKey(), // The ID is always the last key 132 | ]), $presenter->toJson()); 133 | } 134 | 135 | /** @test */ 136 | public function can_be_converted_to_a_string() 137 | { 138 | Carbon::setTestNow(Carbon::parse('Oct 14 2019')); 139 | 140 | $user = User::factory()->create([ 141 | 'name' => 'David Hemphill', 142 | 'email' => 'david@laravel.com', 143 | 'created_at' => Carbon::now()->subDays(4), 144 | 'updated_at' => Carbon::now(), 145 | ]); 146 | 147 | $presenter = new class($user) extends Presenter 148 | { 149 | public function getCreatedAtAttribute($model) 150 | { 151 | return $model->created_at->format('M j, Y'); 152 | } 153 | 154 | public function getUpdatedAtAttribute($model) 155 | { 156 | return $model->updated_at->format('M j, Y'); 157 | } 158 | }; 159 | 160 | $this->assertEquals(json_encode([ 161 | 'name' => 'David Hemphill', 162 | 'email' => 'david@laravel.com', 163 | 'created_at' => 'Oct 10, 2019', 164 | 'updated_at' => 'Oct 14, 2019', 165 | 'id' => $user->getKey(), // The ID is always the last key 166 | ]), (string) $presenter); 167 | } 168 | 169 | /** @test */ 170 | public function can_call_present_on_an_eloquent_model_using_the_trait() 171 | { 172 | $user = User::factory()->create()->present(UserProfilePresenter::class); 173 | $this->assertInstanceOf(UserProfilePresenter::class, $user); 174 | } 175 | 176 | /** @test */ 177 | public function can_call_present_on_an_eloquent_model_using_the_trait_and_use_default_presenter() 178 | { 179 | $user = UserWithDefaultPresenter::factory()->create()->present(); 180 | $this->assertInstanceOf(UserProfilePresenter::class, $user); 181 | } 182 | 183 | /** @test */ 184 | public function throws_if_theres_no_default_presenter_and_none_is_passed_in() 185 | { 186 | $this->expectException(BadMethodCallException::class); 187 | User::factory()->create()->present(null); 188 | } 189 | 190 | /** @test */ 191 | public function can_use_a_helper_function_to_decorate_a_model() 192 | { 193 | $user = present(User::factory()->create(), UserProfilePresenter::class); 194 | $this->assertInstanceOf(UserProfilePresenter::class, $user); 195 | } 196 | 197 | /** @test */ 198 | public function can_present_a_model_using_a_closure() 199 | { 200 | $presenter = User::factory()->create(['name' => 'David'])->present(function ($user) { 201 | return ['name' => strtolower($user->name)]; 202 | }); 203 | 204 | $this->assertEquals('david', $presenter->name); 205 | } 206 | 207 | /** @test */ 208 | public function can_present_an_object_that_is_not_a_model() 209 | { 210 | $notAModel = new class 211 | { 212 | public $name = 'david'; 213 | 214 | public function fullName() 215 | { 216 | return 'David Hemphill'; 217 | } 218 | }; 219 | 220 | $presenter = new class($notAModel) extends Presenter { 221 | }; 222 | 223 | $this->assertEquals('david', $presenter->name); 224 | $this->assertEquals('David Hemphill', $presenter->fullName()); 225 | } 226 | 227 | /** @test */ 228 | public function can_present_a_collection_of_eloquent_models() 229 | { 230 | $user = User::factory()->create(); 231 | $users = collect([$user])->present(UserProfilePresenter::class); 232 | 233 | $users->each(function ($user) { 234 | $this->assertInstanceOf(UserProfilePresenter::class, $user); 235 | }); 236 | } 237 | 238 | /** @test */ 239 | public function can_present_a_collection_of_models_using_a_closure() 240 | { 241 | $user = User::factory()->create(['name' => 'David Hemphill']); 242 | $users = collect([$user])->present(function ($user) { 243 | return ['name' => strtolower($user->name)]; 244 | }); 245 | 246 | $firstUser = $users->first(); 247 | $this->assertNotNull($firstUser); 248 | $this->assertEquals('david hemphill', $firstUser->name); 249 | } 250 | 251 | /** @test */ 252 | public function can_create_presenters_using_the_make_method() 253 | { 254 | $user = User::factory()->create(['name' => 'David Hemphill']); 255 | $presenter = Presenter::make($user, UserProfilePresenter::class); 256 | 257 | $this->assertInstanceOf(UserProfilePresenter::class, $presenter); 258 | } 259 | 260 | /** @test */ 261 | public function can_call_make_on_the_presenter_itself() 262 | { 263 | $user = User::factory()->create(['name' => 'David Hemphill']); 264 | $presenter = UserProfilePresenter::make($user); 265 | 266 | $this->assertInstanceOf(UserProfilePresenter::class, $presenter); 267 | } 268 | 269 | /** @test */ 270 | public function can_present_a_collection_of_models_using_collection_method() 271 | { 272 | $user1 = User::factory()->create(['name' => 'David Hemphill']); 273 | $user2 = User::factory()->create(['name' => 'David Hemphill']); 274 | 275 | $collection = Presenter::collection([$user1, $user2], UserProfilePresenter::class); 276 | 277 | $this->assertInstanceOf(Collection::class, $collection); 278 | } 279 | 280 | /** @test */ 281 | public function can_present_a_collection_of_models_using_collection_method_on_the_presenter_itself() 282 | { 283 | $user1 = User::factory()->create(['name' => 'David Hemphill']); 284 | $user2 = User::factory()->create(['name' => 'David Hemphill']); 285 | 286 | $collection = UserProfilePresenter::collection([$user1, $user2]); 287 | 288 | $this->assertInstanceOf(Collection::class, $collection); 289 | } 290 | 291 | /** @test */ 292 | public function can_camel_case_the_attributes_instead_of_snake_casing_them() 293 | { 294 | Carbon::setTestNow(Carbon::parse('Oct 14 2019')); 295 | 296 | $presenter = User::factory() 297 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 298 | ->present(CamelCaseAttributesPresenter::class); 299 | 300 | $this->assertEquals([ 301 | 'firstName' => 'David', 302 | 'lastName' => 'Hemphill', 303 | 'name' => 'David Hemphill', 304 | 'email' => 'david@laravel.com', 305 | 'id' => 1, 306 | 'updatedAt' => '2019-10-14T00:00:00.000000Z', 307 | 'createdAt' => '2019-10-14T00:00:00.000000Z', 308 | ], $presenter->toArray()); 309 | } 310 | 311 | /** @test */ 312 | public function can_set_the_casing_strategy_at_runtime() 313 | { 314 | $presenter = User::factory() 315 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 316 | ->present(UserProfilePresenter::class); 317 | 318 | $this->assertTrue($presenter->snakeCase); 319 | 320 | $presenter->camelCase(); 321 | 322 | $this->assertFalse($presenter->snakeCase); 323 | 324 | $this->assertEquals( 325 | ['name', 'email', 'updatedAt', 'createdAt', 'id'], 326 | array_keys($presenter->toArray()) 327 | ); 328 | 329 | $presenter->snakeCase(); 330 | 331 | $this->assertTrue($presenter->snakeCase); 332 | 333 | $this->assertEquals( 334 | ['name', 'email', 'updated_at', 'created_at', 'id'], 335 | array_keys($presenter->toArray()) 336 | ); 337 | } 338 | 339 | /** @test */ 340 | public function a_collection_of_presented_eloquent_models_will_still_return_json() 341 | { 342 | User::factory()->create(['name' => 'David Hemphill']); 343 | 344 | $response = $this 345 | ->withoutExceptionHandling() 346 | ->json('GET', '/users') 347 | ->assertOk() 348 | ->assertHeader('Content-Type', 'application/json'); 349 | 350 | $this->assertEquals('David Hemphill', $response->original[0]->full_name); 351 | } 352 | 353 | /** @test */ 354 | public function can_paginate_a_presented_collection() 355 | { 356 | User::factory()->create(['name' => 'David Hemphill']); 357 | User::factory()->create(['name' => 'Taylor Otwell']); 358 | 359 | $response = $this 360 | ->withoutExceptionHandling() 361 | ->json('GET', '/paginated?page=2') 362 | ->assertOk() 363 | ->assertHeader('Content-Type', 'application/json'); 364 | 365 | $this->assertEquals('Taylor Otwell', $response->original[0]->full_name); 366 | } 367 | 368 | /** @test */ 369 | public function presenter_removes_hidden_model_attributes_from_output() 370 | { 371 | $presenter = User::factory() 372 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 373 | ->present(HiddenAttributesPresenter::class); 374 | 375 | $this->assertEquals([ 376 | 'name' => 'David Hemphill', 377 | 'email' => 'david@laravel.com', 378 | ], $presenter->toArray()); 379 | } 380 | 381 | /** @test */ 382 | public function presenter_removes_hidden_attributes_and_leaves_visible_model_attributes_in_output() 383 | { 384 | $presenter = User::factory() 385 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 386 | ->present(HiddenAndVisibleAttributesPresenter::class); 387 | 388 | $this->assertEquals([ 389 | 'name' => 'David Hemphill', 390 | ], $presenter->toArray()); 391 | } 392 | 393 | /** @test */ 394 | public function supports_offset_exists_via_array_access() 395 | { 396 | $presenter = User::factory() 397 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 398 | ->present(HiddenAndVisibleAttributesPresenter::class); 399 | 400 | $this->assertTrue(isset($presenter['name'])); 401 | } 402 | 403 | /** @test */ 404 | public function presenter_leaves_visible_model_attributes_in_output() 405 | { 406 | $presenter = User::factory() 407 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 408 | ->present(VisibleAttributesPresenter::class); 409 | 410 | $this->assertEquals([ 411 | 'id' => 1, 412 | 'email' => 'david@laravel.com', 413 | ], $presenter->toArray()); 414 | } 415 | 416 | /** @test */ 417 | public function can_be_array_accessed() 418 | { 419 | $presenter = User::factory() 420 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 421 | ->present(UserProfilePresenter::class); 422 | 423 | $this->assertEquals('David Hemphill', $presenter['name']); 424 | } 425 | 426 | /** @test */ 427 | public function cannot_be_written_to_via_array_access() 428 | { 429 | $this->expectException(BadMethodCallException::class); 430 | 431 | $presenter = User::factory() 432 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 433 | ->present(HiddenAndVisibleAttributesPresenter::class); 434 | 435 | $presenter['email'] = 'david@monarkee.com'; 436 | } 437 | 438 | /** @test */ 439 | public function output_keys_cannot_be_unset_via_array_access() 440 | { 441 | $this->expectException(BadMethodCallException::class); 442 | 443 | $presenter = User::factory() 444 | ->create(['name' => 'David Hemphill', 'email' => 'david@laravel.com']) 445 | ->present(HiddenAndVisibleAttributesPresenter::class); 446 | 447 | unset($presenter['email']); 448 | } 449 | 450 | /** @test */ 451 | public function can_check_isset_on_presenter_for_model_attribute() 452 | { 453 | $presenter = User::factory() 454 | ->create(['name' => 'David Hemphill']) 455 | ->present(UserProfilePresenter::class); 456 | 457 | $this->assertTrue(isset($presenter->name)); 458 | } 459 | 460 | /** @test */ 461 | public function can_check_isset_on_presenter_for_accessor() 462 | { 463 | $user = User::factory()->create(); 464 | $presenter = new class($user) extends Presenter 465 | { 466 | public function getSayHelloAttribute() 467 | { 468 | return 'Hello from the Presenter!'; 469 | } 470 | }; 471 | 472 | $this->assertTrue(isset($presenter->say_hello)); 473 | } 474 | 475 | /** @test */ 476 | public function can_check_isset_is_false_on_presenter_for_accessor_if_returns_null() 477 | { 478 | $user = User::factory()->create(); 479 | $presenter = new class($user) extends Presenter 480 | { 481 | public function getSayHelloAttribute() 482 | { 483 | return null; 484 | } 485 | }; 486 | 487 | $this->assertFalse(isset($presenter->say_hello)); 488 | } 489 | } 490 | --------------------------------------------------------------------------------