├── .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 |
--------------------------------------------------------------------------------