Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit be6fede

Browse files
committedMay 28, 2021
wip
1 parent 6196054 commit be6fede

37 files changed

+1118
-262
lines changed
 

‎.github/FUNDING.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
github: :vendor_name
1+
github: spatie

‎CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
All notable changes to `:package_name` will be documented in this file.
3+
All notable changes to `laravel-data-resource` will be documented in this file.
44

55
## 1.0.0 - 202X-XX-XX
66

‎LICENSE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) :vendor_name <author@domain.com>
3+
Copyright (c) spatie <ruben@spatie.be>
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

‎README.md

+293-14
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
# :package_description
1+
# This is my package LaravelDataResource
22

3-
[![Latest Version on Packagist](https://img.shields.io/packagist/v/vendor_slug/package_slug.svg?style=flat-square)](https://packagist.org/packages/vendor_slug/package_slug)
4-
[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/vendor_slug/package_slug/run-tests?label=tests)](https://github.com/vendor_slug/package_slug/actions?query=workflow%3Arun-tests+branch%3Amain)
5-
[![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/vendor_slug/package_slug/Check%20&%20fix%20styling?label=code%20style)](https://github.com/vendor_slug/package_slug/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain)
6-
[![Total Downloads](https://img.shields.io/packagist/dt/vendor_slug/package_slug.svg?style=flat-square)](https://packagist.org/packages/vendor_slug/package_slug)
3+
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-data-resource.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-data-resource)
4+
[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/spatie/laravel-data-resource/run-tests?label=tests)](https://github.com/spatie/laravel-data-resource/actions?query=workflow%3Arun-tests+branch%3Amain)
5+
[![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/spatie/laravel-data-resource/Check%20&%20fix%20styling?label=code%20style)](https://github.com/spatie/laravel-data-resource/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain)
6+
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-data-resource.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-data-resource)
77

88
---
99
This repo can be used as to scaffold a Laravel package. Follow these steps to get started:
1010

11-
1. Press the "Use template" button at the top of this repo to create a new repo with the contents of this skeleton
12-
2. Run "./configure-skeleton.sh" to run a script that will replace all placeholders throughout all the files
11+
1. Press the "Use template" button at the top of this repo to create a new repo with the contents of this laravel-data-resource
12+
2. Run "./configure-laravel-data-resource.sh" to run a script that will replace all placeholders throughout all the files
1313
3. Remove this block of text.
1414
4. Have fun creating your package.
1515
5. If you need help creating a package, consider picking up our <a href="https://laravelpackage.training">Laravel Package Training</a> video course.
@@ -19,7 +19,7 @@ This is where your description should go. Limit it to a paragraph or two. Consid
1919

2020
## Support us
2121

22-
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/:package_name.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/:package_name)
22+
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-data-resource.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-data-resource)
2323

2424
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
2525

@@ -30,19 +30,19 @@ We highly appreciate you sending us a postcard from your hometown, mentioning wh
3030
You can install the package via composer:
3131

3232
```bash
33-
composer require vendor_slug/package_slug
33+
composer require spatie/laravel-data-resource
3434
```
3535

3636
You can publish and run the migrations with:
3737

3838
```bash
39-
php artisan vendor:publish --provider="VendorName\Skeleton\SkeletonServiceProvider" --tag="package_slug-migrations"
39+
php artisan vendor:publish --provider="Spatie\LaravelDataResource\LaravelDataResourceServiceProvider" --tag="laravel-data-resource-migrations"
4040
php artisan migrate
4141
```
4242

4343
You can publish the config file with:
4444
```bash
45-
php artisan vendor:publish --provider="VendorName\Skeleton\SkeletonServiceProvider" --tag="package_slug-config"
45+
php artisan vendor:publish --provider="Spatie\LaravelDataResource\LaravelDataResourceServiceProvider" --tag="laravel-data-resource-config"
4646
```
4747

4848
This is the contents of the published config file:
@@ -54,9 +54,288 @@ return [
5454

5555
## Usage
5656

57+
Data objects are structured entities within Sail that fullfill the role of a data transfer object and a resource.
58+
59+
They have a few advantages:
60+
61+
- One structure for both the resource and data
62+
- Can be lazy (=only send required data needed)
63+
- They are type safe
64+
- TypeScript transformer knows exactly what to do with them
65+
66+
There are situations where the unified data objects aren't a good fit. For example when the structure of the DTO and resource differ too much or in the case where a DTO is required but a resource isn't. In such case you'd better create a seperate resource and/or dto.
67+
68+
## Creating Data objects
69+
70+
A data object extends from `Data` and looks like this:
71+
72+
```php
73+
class ContentThemeData extends Data
74+
{
75+
public function __construct(
76+
public string $name
77+
) {
78+
}
79+
80+
public static function create(ContentTheme $theme): self
81+
{
82+
return new self(
83+
$theme->name
84+
);
85+
}
86+
}
87+
88+
```
89+
90+
In the constructor we define the properties associated with this data object. Each data object also should have a static `create` method that will create the object based upon a model. This is required for automatically creating collections of resources and logging activity.
91+
92+
## Using Data objects as dto
93+
94+
Since the data objects are just simple PHP objects with some extra methods added to them, you can use them like regular PHP dto's:
95+
96+
```php
97+
$data = new ContentThemeResource('Hello world');
98+
```
99+
100+
You probably going to create a dto when receiving data from a form in the frontend. There are going to be two points where this happens: when you create something and when you edit something. That's why we'll create the data object within the request:
101+
102+
```php
103+
class ContentThemeRequest extends Request
104+
{
105+
public function rules(): array
106+
{
107+
return [
108+
'name' => ['required', 'string'],
109+
];
110+
}
111+
112+
public function getData(): ContentThemeData
113+
{
114+
$validated = $this->validated();
115+
116+
return new ContentThemeData(
117+
$validated['name']
118+
);
119+
}
120+
}
121+
```
122+
123+
This has two advantages:
124+
125+
- your validation rules and data objects will be created in the same class
126+
- you can create the same data object in different requests with slightly different properties
127+
128+
Since PHP supports the spread operator, for simple data objects you could do the following:
129+
130+
```php
131+
public function getData(): ContentThemeData
132+
{
133+
return new ContentThemeData(...$this->validated());
134+
}
135+
```
136+
137+
## Using Data objects as resource
138+
139+
When creating a resource you'll probably have a model, so you can call the `create` method:
140+
141+
```php
142+
ContentThemeData::create($this->contentTheme);
143+
```
144+
145+
At the moment you're creating a new model, in this case the model is `null`, you can use the `empty` method:
146+
147+
```php
148+
ContentThemeData::empty();
149+
```
150+
151+
This will return an array that follows the structure of the data object, it is possible to change the default values within this array by providing them in the constructor of the data object:
152+
153+
```php
154+
class ContentThemeData extends Data
155+
{
156+
public function __construct(
157+
public string $name = 'Hello world'
158+
) {
159+
}
160+
161+
public static function create(ContentTheme $theme): self
162+
{
163+
return new self(
164+
$theme->name
165+
);
166+
}
167+
}
168+
```
169+
170+
Or by passing defaults within the `empty` call:
171+
172+
173+
```php
174+
ContentThemeData::empty([
175+
'name' => 'Hello world',
176+
]);
177+
```
178+
179+
In your view models the `values method will now look like this:
180+
181+
```php
182+
public function values(): ContentThemeData | array
183+
{
184+
return $this->contentTheme
185+
? ContentThemeData::create($this->contentTheme)
186+
: ContentThemeData::empty();
187+
}
188+
```
189+
190+
### Indexes
191+
192+
We already took a look at using data objects within create and edit pages, but index pages also need resources albeit a collection of resources. You can easily create a collection of resources as such:
193+
194+
```php
195+
ContentThemeIndexData::collection($contentThemes);
196+
```
197+
198+
Within the `index` method of your controller you now can do the following:
199+
200+
```php
201+
public function index(ContentThemesIndexQuery $indexQuery)
202+
{
203+
return ContentThemeIndexData::collection($indexQuery->paginate());
204+
}
205+
```
206+
207+
As you can see we provide the `collection` method a paginated collection, the data object is smart enough to create a paginated response from this with links to the next, previous, last, ... pages.
208+
209+
When you yust provide a collection or array of resources to the `collection` method of a data object, then it will just return a `DataCollection` which is just a simple collection of resources:
210+
211+
```php
212+
ContentThemeIndexData::collection($contentThemes); // No pagination here
213+
```
214+
215+
### endpoints
216+
217+
Each data object can also have endpoints, you define these within a seperate `endpoints` method:
218+
219+
```php
220+
class ContentThemeIndexData extends Data
221+
{
222+
public function __construct(
223+
public ContentThemeUuid $uuid,
224+
public string $name
225+
) {
226+
}
227+
228+
public static function create(ContentTheme $theme): self
229+
{
230+
return new self(
231+
$theme->getUuid(),
232+
$theme->name,
233+
);
234+
}
235+
236+
public function endpoints(): array
237+
{
238+
return [
239+
'edit' => action([ContentThemesController::class, 'edit'], $this->uuid),
240+
'destroy' => action([ContentThemesController::class, 'destroy'], $this->uuid),
241+
];
242+
}
243+
}
244+
```
245+
246+
When this object is transformed to a resource, an extra key will be present with the endpoints. Also TypeScript transformer is smart enough to understand which endpoints this data object has.
247+
248+
When creating this data object as an dto, no endpoints will be added. And when an `endpoints` method wasn't added to the data object, then the resource representation of the data object won't include an endpoints key.
249+
250+
### Using collections of data objects within data objects
251+
252+
When you have a data object which has a collection of data objects as a property, then always type this as a `DataCollection` with an annotation for the resources within the collection:
253+
254+
```php
255+
class ApplicationData extends Data
256+
{
257+
public function __construct(
258+
/** @var \App\Data\ContentThemeData[] */
259+
public DataCollection $themes
260+
) {
261+
}
262+
263+
public static function create(Event $event): static
264+
{
265+
return new self(
266+
ContentThemeData::collection($app->themes)
267+
);
268+
}
269+
}
270+
```
271+
272+
This will make sure data objects can be lazy loaded and that activity logging works as expected.
273+
274+
### Resolving resources from Data objects
275+
276+
You can convert a data object to a resource(array) as such:
277+
278+
```php
279+
ContentThemeData::create($theme)->toArray();
280+
```
281+
282+
When you want an array representation of the data object without transforming the properties like converting the underlying resources into arrays, then you can use:
283+
284+
285+
```php
286+
ContentThemeData::create($theme)->toArray();
287+
```
288+
289+
A data object is also `Responsable` so it can be returned in a controller:
290+
291+
```php
292+
public function (ContentTheme $theme): ContentThemeData
293+
{
294+
return ContentThemeData::create($theme;)
295+
}
296+
```
297+
298+
Collections of data objects are also `Responsable` and `toArray()` can be called on them.
299+
300+
## Lazy properties
301+
302+
Data objects support lazy properties by default, they allow you to only output certain properties when converting a data object to a resource. You create a lazy property as such:
303+
304+
```php
305+
class ContentThemeData extends Data
306+
{
307+
public function __construct(
308+
public string $name,
309+
public int | Lazy $usages,
310+
) {
311+
}
312+
313+
public static function create(ContentTheme $theme): self
314+
{
315+
return new self(
316+
$theme->name,
317+
Lazy::create(fn() => $theme->calculateUsages()),
318+
);
319+
}
320+
}
321+
```
322+
323+
Now when you output this data object as a resource, the `usages` property will be missing since it is quite expensive to calculate:
324+
325+
```php
326+
ContentThemeData::create($theme)->toArray(); // missing usages
327+
```
328+
329+
We can include `usages` in the resource as such:
330+
331+
```php
332+
ContentThemeData::create($theme)->include('usages')->toArray(); // with usages
333+
```
334+
335+
This will also work when we use a data object within a collection, let's take a look at the `ApplicationData` resource from earlier. This one has a `DataCollection` property with `ContentThemeResources` within it. We now can include the `usages` property for all these resources by using the dot notation:
336+
57337
```php
58-
$skeleton = new VendorName\Skeleton();
59-
echo $skeleton->echoPhrase('Hello, Spatie!');
338+
ContentThemeData::create($theme)->include('themes.usages')->toArray(); // with usages
60339
```
61340

62341
## Testing
@@ -79,7 +358,7 @@ Please review [our security policy](../../security/policy) on how to report secu
79358

80359
## Credits
81360

82-
- [:author_name](https://github.com/:author_username)
361+
- [Ruben Van Assche](https://github.com/rubenvanassche)
83362
- [All Contributors](../../contributors)
84363

85364
## License

‎composer.json

+15-17
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
{
2-
"name": "vendor_slug/package_slug",
3-
"description": ":package_description",
2+
"name": "spatie/laravel-data",
3+
"description": "Create unified resources and data transfer objects",
44
"keywords": [
5-
":vendor_name",
5+
"spatie",
66
"laravel",
7-
"package_slug"
7+
"laravel-data"
88
],
9-
"homepage": "https://github.com/vendor_slug/package_slug",
9+
"homepage": "https://github.com/spatie/laravel-data",
1010
"license": "MIT",
1111
"authors": [
1212
{
13-
"name": ":author_name",
14-
"email": "author@domain.com",
13+
"name": "Ruben Van Assche",
14+
"email": "ruben@spatie.be",
1515
"role": "Developer"
1616
}
1717
],
1818
"require": {
1919
"php": "^8.0",
20-
"spatie/laravel-package-tools": "^1.4.3",
21-
"illuminate/contracts": "^8.37"
20+
"fakerphp/faker": "^1.14",
21+
"illuminate/contracts": "^8.37",
22+
"spatie/laravel-package-tools": "^1.4.3"
2223
},
2324
"require-dev": {
2425
"brianium/paratest": "^6.2",
@@ -30,13 +31,13 @@
3031
},
3132
"autoload": {
3233
"psr-4": {
33-
"VendorName\\Skeleton\\": "src",
34-
"VendorName\\Skeleton\\Database\\Factories\\": "database/factories"
34+
"Spatie\\LaravelData\\": "src",
35+
"Spatie\\LaravelData\\Database\\Factories\\": "database/factories"
3536
}
3637
},
3738
"autoload-dev": {
3839
"psr-4": {
39-
"VendorName\\Skeleton\\Tests\\": "tests"
40+
"Spatie\\LaravelData\\Tests\\": "tests"
4041
}
4142
},
4243
"scripts": {
@@ -50,11 +51,8 @@
5051
"extra": {
5152
"laravel": {
5253
"providers": [
53-
"VendorName\\Skeleton\\SkeletonServiceProvider"
54-
],
55-
"aliases": {
56-
"Skeleton": "VendorName\\Skeleton\\SkeletonFacade"
57-
}
54+
"Spatie\\LaravelData\\LaravelDataServiceProvider"
55+
]
5856
}
5957
},
6058
"minimum-stability": "dev",

‎config/data.php

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
return [
4+
'transformers' => [
5+
\Spatie\LaravelData\Transformers\DateTransformer::class,
6+
\Spatie\LaravelData\Transformers\ArrayableTransformer::class,
7+
]
8+
];

‎config/skeleton.php

-5
This file was deleted.

‎configure-skeleton.sh

-137
This file was deleted.

‎database/factories/ModelFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace VendorName\Skeleton\Database\Factories;
3+
namespace Spatie\LaravelDataResource\Database\Factories;
44

55
use Illuminate\Database\Eloquent\Factories\Factory;
66

‎database/migrations/create_skeleton_table.php.stub ‎database/migrations/create_data-resource_table.php.stub

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ return new class extends Migration
88
{
99
public function up()
1010
{
11-
Schema::create('skeleton_table', function (Blueprint $table) {
11+
Schema::create('laravel-data-resource_table', function (Blueprint $table) {
1212
$table->id();
1313

1414
// add fields

‎phpunit.xml.dist

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
verbose="true"
2020
>
2121
<testsuites>
22-
<testsuite name="VendorName Test Suite">
22+
<testsuite name="Spatie Test Suite">
2323
<directory>tests</directory>
2424
</testsuite>
2525
</testsuites>

‎resources/views/.gitkeep

Whitespace-only changes.

‎src/Commands/SkeletonCommand.php

-17
This file was deleted.

‎src/Data.php

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use DateTime;
6+
use DateTimeImmutable;
7+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
8+
use Illuminate\Contracts\Support\Arrayable;
9+
use Illuminate\Contracts\Support\Responsable;
10+
use Illuminate\Support\Collection;
11+
use Illuminate\Support\Str;
12+
use ReflectionClass;
13+
use ReflectionParameter;
14+
use ReflectionProperty;
15+
use Spatie\LaravelData\Transformers\DataTransformer;
16+
use Spatie\LaravelData\Transformers\DateTransformer;
17+
use Spatie\LaravelData\Transformers\LazyTransformer;
18+
19+
/**
20+
* @method static array create()
21+
*/
22+
abstract class Data implements Arrayable, Responsable
23+
{
24+
use ResponsableData;
25+
26+
private array $includes = [];
27+
28+
public static function collection(Collection | array | LengthAwarePaginator $items): DataCollection | PaginatedDataCollection
29+
{
30+
if ($items instanceof LengthAwarePaginator) {
31+
return new PaginatedDataCollection(static::class, $items);
32+
}
33+
34+
return new DataCollection(static::class, $items);
35+
}
36+
37+
public function endpoints(): array
38+
{
39+
return [];
40+
}
41+
42+
public function include(string ...$includes): static
43+
{
44+
$this->includes = array_unique(array_merge($this->includes, $includes));
45+
46+
return $this;
47+
}
48+
49+
public function all(): array
50+
{
51+
$reflection = new ReflectionClass($this);
52+
53+
$includes = $this->getIncludesForResource();
54+
$endpoints = $this->endpoints();
55+
56+
$payload = [];
57+
58+
if (count($endpoints) > 0) {
59+
$payload['endpoints'] = $endpoints;
60+
}
61+
62+
foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
63+
$name = $property->getName();
64+
$value = $this->{$name};
65+
66+
if ($this->shouldIncludeProperty($name, $value, $includes)) {
67+
$payload[$name] = $value;
68+
}
69+
}
70+
71+
return $payload;
72+
}
73+
74+
public function toArray(): array
75+
{
76+
$reflection = new ReflectionClass($this);
77+
78+
$includes = $this->getIncludesForResource();
79+
80+
/** @var \Spatie\LaravelData\DataTransformers $transformers */
81+
$transformers = app(DataTransformers::class);
82+
83+
$payload = array_reduce(
84+
$reflection->getProperties(ReflectionProperty::IS_PUBLIC),
85+
function (array $payload, ReflectionProperty $property) use ($transformers, $includes) {
86+
$name = $property->getName();
87+
$value = $this->{$name};
88+
89+
if ($this->shouldIncludeProperty($name, $value, $includes)) {
90+
if($value instanceof Lazy){
91+
$value = $value->resolve();
92+
}
93+
94+
$payload[$name] = $transformers->forValue($value)?->transform(
95+
$value,
96+
$includes[$name] ?? []
97+
) ?? $value;
98+
}
99+
100+
return $payload;
101+
},
102+
[]
103+
);
104+
105+
$endpoints = $this->endpoints();
106+
107+
if (count($endpoints) > 0) {
108+
$payload['endpoints'] = $endpoints;
109+
}
110+
111+
return $payload;
112+
}
113+
114+
public static function empty(array $extra = []): array
115+
{
116+
$reflection = new ReflectionClass(static::class);
117+
118+
$defaultConstructorProperties = $reflection->hasMethod('__construct')
119+
? collect($reflection->getMethod('__construct')->getParameters())
120+
->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable())
121+
->mapWithKeys(fn (ReflectionParameter $parameter) => [
122+
$parameter->name => $parameter->getDefaultValue(),
123+
])
124+
->toArray()
125+
: [];
126+
127+
$defaults = array_merge(
128+
$reflection->getDefaultProperties(),
129+
$defaultConstructorProperties
130+
);
131+
132+
return array_reduce(
133+
$reflection->getProperties(ReflectionProperty::IS_PUBLIC),
134+
function (array $payload, ReflectionProperty $property) use ($defaults, $extra) {
135+
$name = $property->getName();
136+
137+
if (array_key_exists($name, $extra)) {
138+
$payload[$name] = $extra[$name];
139+
140+
return $payload;
141+
}
142+
143+
if (array_key_exists($name, $defaults)) {
144+
$payload[$name] = $defaults[$name];
145+
146+
return $payload;
147+
}
148+
149+
$propertyHelper = new DataPropertyHelper($property);
150+
151+
$payload[$name] = $propertyHelper->getEmptyValue();
152+
153+
return $payload;
154+
},
155+
[]
156+
);
157+
}
158+
159+
private function shouldIncludeProperty(string $name, $value, array $includes): bool
160+
{
161+
if (! $value instanceof Lazy) {
162+
return true;
163+
}
164+
165+
return array_key_exists($name, $includes);
166+
}
167+
168+
private function getPropertyValue(string $name, $value, array $includes): mixed
169+
{
170+
if($value instanceof Lazy){
171+
$value = $value->resolve();
172+
}
173+
174+
175+
return $value;
176+
}
177+
178+
private function getIncludesForResource(): array
179+
{
180+
return array_reduce($this->includes, function (array $includes, $include) {
181+
if (! Str::contains($include, '.') && array_key_exists($include, $includes) === false) {
182+
$includes[$include] = [];
183+
184+
return $includes;
185+
}
186+
187+
$property = Str::before($include, '.');
188+
$otherIncludes = Str::after($include, '.');
189+
190+
if (array_key_exists($property, $includes)) {
191+
$includes[$property][] = $otherIncludes;
192+
} else {
193+
$includes[$property] = [$otherIncludes];
194+
}
195+
196+
return $includes;
197+
}, []);
198+
}
199+
}

‎src/DataCollection.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use Illuminate\Contracts\Support\Responsable;
6+
use Illuminate\Support\Collection;
7+
8+
class DataCollection extends Collection implements Responsable
9+
{
10+
use ResponsableData;
11+
12+
private array $includes = [];
13+
14+
public function __construct(
15+
private string $dataClass,
16+
Collection | array $items
17+
) {
18+
parent::__construct($items);
19+
}
20+
21+
public function include(string ...$includes): static
22+
{
23+
$this->includes = array_merge($this->includes, $includes);
24+
25+
return $this;
26+
}
27+
28+
public function toArray(): array
29+
{
30+
return array_map(
31+
function ($item) {
32+
$data = $this->dataClass::create($item);
33+
34+
return $data->include(...$this->includes)->toArray();
35+
},
36+
$this->items instanceof Collection ? $this->items->all() : $this->items
37+
);
38+
}
39+
}

‎src/DataPropertyHelper.php

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use Exception;
6+
use Illuminate\Support\Collection;
7+
use ReflectionNamedType;
8+
use ReflectionProperty;
9+
use ReflectionUnionType;
10+
11+
class DataPropertyHelper
12+
{
13+
public function __construct(private ReflectionProperty $property)
14+
{
15+
}
16+
17+
public function getEmptyValue(): mixed
18+
{
19+
$type = $this->property->getType();
20+
21+
if ($type === null) {
22+
return null;
23+
}
24+
25+
if ($type instanceof ReflectionNamedType) {
26+
return $this->getValueForNamedType($type);
27+
}
28+
29+
if ($type instanceof ReflectionUnionType) {
30+
return $this->getValueForUnionType($type);
31+
}
32+
33+
throw new Exception("Unknown reflection type");
34+
}
35+
36+
private function getValueForNamedType(
37+
ReflectionNamedType $type,
38+
): mixed {
39+
$name = $type->getName();
40+
41+
if ($name === 'array') {
42+
return [];
43+
}
44+
45+
if ($type->isBuiltin()) {
46+
return null;
47+
}
48+
49+
if (is_subclass_of($name, Data::class)) {
50+
/** @var \Spatie\LaravelData\Data $name */
51+
return $name::empty();
52+
}
53+
54+
if (is_subclass_of($name, Collection::class)) {
55+
return [];
56+
}
57+
58+
return null;
59+
}
60+
61+
private function getValueForUnionType(
62+
ReflectionUnionType $type
63+
): mixed {
64+
$types = $type->getTypes();
65+
66+
if ($type->allowsNull() && count($types) !== 3) {
67+
throw new Exception("Union lazy type can only have one real type");
68+
}
69+
70+
if ($type->allowsNull() === false && count($types) !== 2) {
71+
throw new Exception("Union lazy type can only have one real type");
72+
}
73+
74+
foreach ($types as $childType) {
75+
if (in_array($childType->getName(), ['null', Lazy::class]) === false) {
76+
return $this->getValueForNamedType($childType);
77+
}
78+
}
79+
80+
return null;
81+
}
82+
83+
public function isLazy(): bool
84+
{
85+
$type = $this->property->getType();
86+
87+
if ($type === null || $type instanceof ReflectionNamedType) {
88+
return false;
89+
}
90+
91+
if ($type instanceof ReflectionUnionType) {
92+
foreach ($type->getTypes() as $childtype) {
93+
if ($childtype->getName() === Lazy::class) {
94+
return true;
95+
}
96+
}
97+
}
98+
99+
return false;
100+
}
101+
}

‎src/DataTransformers.php

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use Spatie\LaravelData\Transformers\DataTransformer;
6+
use Spatie\LaravelData\Transformers\Transformer;
7+
8+
class DataTransformers
9+
{
10+
/** @var \Spatie\LaravelData\Transformers\Transformer[] */
11+
protected array $transformers = [];
12+
13+
public function __construct(array $userTransformers)
14+
{
15+
$this->transformers = array_map(
16+
function (string $transformer) {
17+
return app($transformer);
18+
},
19+
array_merge($this->defaultTransformers(), $userTransformers)
20+
);
21+
}
22+
23+
public function forValue(mixed $value): ?Transformer
24+
{
25+
foreach ($this->transformers as $transformer){
26+
if($transformer->canTransform($value)){
27+
return $transformer;
28+
}
29+
}
30+
31+
return null;
32+
}
33+
34+
public function get(): array
35+
{
36+
return $this->transformers;
37+
}
38+
39+
protected function defaultTransformers(): array
40+
{
41+
return [
42+
DataTransformer::class,
43+
DataCollection::class,
44+
];
45+
}
46+
}

‎src/LaravelDataServiceProvider.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use Spatie\LaravelPackageTools\Package;
6+
use Spatie\LaravelPackageTools\PackageServiceProvider;
7+
8+
class LaravelDataServiceProvider extends PackageServiceProvider
9+
{
10+
public function register()
11+
{
12+
$this->app->singleton(new DataTransformers(config('data.transformers')));
13+
}
14+
15+
public function configurePackage(Package $package): void
16+
{
17+
$package
18+
->name('laravel-data')
19+
->hasConfigFile();
20+
}
21+
}

‎src/Lazy.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use Closure;
6+
7+
class Lazy
8+
{
9+
private function __construct(
10+
private Closure $closure
11+
) {
12+
}
13+
14+
public static function create(Closure $closure): self
15+
{
16+
return new self($closure);
17+
}
18+
19+
public function resolve(): mixed
20+
{
21+
return ($this->closure)();
22+
}
23+
}

‎src/PaginatedDataCollection.php

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
6+
use Illuminate\Contracts\Support\Arrayable;
7+
use Illuminate\Contracts\Support\Responsable;
8+
use Illuminate\Support\Collection;
9+
10+
class PaginatedDataCollection implements Arrayable, Responsable
11+
{
12+
use ResponsableData;
13+
14+
private array $includes = [];
15+
16+
public function __construct(
17+
/** @var \Spatie\LaravelData\Data */
18+
private string $dataClass,
19+
private Collection | LengthAwarePaginator $items
20+
) {
21+
}
22+
23+
public function include(string ...$includes): static
24+
{
25+
$this->includes = array_merge($this->includes, $includes);
26+
27+
return $this;
28+
}
29+
30+
public function toArray()
31+
{
32+
return [
33+
'data' => $this->resolveData(),
34+
'meta' => $this->resolveMeta(),
35+
];
36+
}
37+
38+
private function resolveData(): array
39+
{
40+
return array_map(
41+
function (Model $model) {
42+
/** @var \App\Support\Data\Data $data */
43+
$data = $this->dataClass::create($model);
44+
45+
return $data->include(...$this->includes)->toArray();
46+
},
47+
$this->items->all()
48+
);
49+
}
50+
51+
private function resolveMeta(): array
52+
{
53+
if (! $this->items instanceof LengthAwarePaginator) {
54+
return [];
55+
}
56+
57+
return [
58+
'current_page' => $this->items->currentPage(),
59+
'first_page_url' => $this->items->url(1),
60+
'from' => $this->items->firstItem(),
61+
'last_page' => $this->items->lastPage(),
62+
'last_page_url' => $this->items->url($this->items->lastPage()),
63+
'next_page_url' => $this->items->nextPageUrl(),
64+
'path' => $this->items->path(),
65+
'per_page' => $this->items->perPage(),
66+
'prev_page_url' => $this->items->previousPageUrl(),
67+
'to' => $this->items->lastItem(),
68+
'total' => $this->items->total(),
69+
];
70+
}
71+
}

‎src/ResponsableData.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData;
4+
5+
use Illuminate\Http\JsonResponse;
6+
7+
/** @mixin \Spatie\LaravelData\Data|\Spatie\LaravelData\DataCollection|\Spatie\LaravelData\PaginatedDataCollection */
8+
trait ResponsableData
9+
{
10+
/**
11+
* @param \Illuminate\Http\Request $request
12+
*
13+
* @return \Illuminate\Http\JsonResponse
14+
*/
15+
public function toResponse($request)
16+
{
17+
if ($request->has('include')) {
18+
$this->include(...explode(',', $request->get('includes')));
19+
}
20+
21+
return new JsonResponse($this->toArray());
22+
}
23+
}

‎src/Skeleton.php

-7
This file was deleted.

‎src/SkeletonFacade.php

-16
This file was deleted.

‎src/SkeletonServiceProvider.php

-25
This file was deleted.
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Transformers;
4+
5+
use Illuminate\Contracts\Support\Arrayable;
6+
7+
class ArrayableTransformer implements Transformer
8+
{
9+
public function canTransform(mixed $value): bool
10+
{
11+
return $value instanceof Arrayable;
12+
}
13+
14+
public function transform(mixed $value, array $includes): mixed
15+
{
16+
/** @var \Illuminate\Contracts\Support\Arrayable $value */
17+
return $value->toArray();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Transformers;
4+
5+
use Spatie\LaravelData\DataCollection;
6+
7+
class DataCollectionTransformer implements Transformer
8+
{
9+
public function canTransform(mixed $value): bool
10+
{
11+
return $value instanceof DataCollection;
12+
}
13+
14+
public function transform(mixed $value, array $includes): mixed
15+
{
16+
/** @var \Spatie\LaravelData\DataCollection $value */
17+
return $value->include(...$includes)->toArray();
18+
}
19+
}

‎src/Transformers/DataTransformer.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Transformers;
4+
5+
use Spatie\LaravelData\Data;
6+
7+
class DataTransformer implements Transformer
8+
{
9+
public function canTransform(mixed $value): bool
10+
{
11+
return $value instanceof Data;
12+
}
13+
14+
public function transform(mixed $value, array $includes): mixed
15+
{
16+
/** @var \Spatie\LaravelData\Data $value */
17+
return $value->include(...$includes)->toArray();
18+
}
19+
}

‎src/Transformers/DateTransformer.php

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Transformers;
4+
5+
use DateTimeInterface;
6+
7+
class DateTransformer implements Transformer
8+
{
9+
public function canTransform(mixed $value): bool
10+
{
11+
return $value instanceof DateTimeInterface;
12+
}
13+
14+
public function transform(mixed $value, array $includes): mixed
15+
{
16+
/** @var \DateTimeInterface $value */
17+
return $value->format('Y-m-d H:i:s');
18+
}
19+
}

‎src/Transformers/Transformer.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Transformers;
4+
5+
interface Transformer
6+
{
7+
public function canTransform(mixed $value): bool;
8+
9+
public function transform(mixed $value, array $includes): mixed;
10+
}

‎testbench.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
providers:
2-
- VendorName\Skeleton\SkeletonServiceProvider
2+
- Spatie\LaravelDataResource\LaravelDataResourceServiceProvider

‎tests/DataResourceTest.php

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Tests;
4+
5+
use Illuminate\Foundation\Auth\User;
6+
use Spatie\LaravelData\Lazy;
7+
use Spatie\LaravelData\Tests\Fakes\LazyResource;
8+
use Spatie\LaravelData\Tests\Fakes\UserResource;
9+
10+
class DataResourceTest extends TestCase
11+
{
12+
/** @test */
13+
public function it_can_create_a_resource()
14+
{
15+
$user = $this->makeUser();
16+
17+
$resource = UserResource::create($user);
18+
19+
$this->assertEquals([
20+
'id' => $user->id,
21+
'name' => $user->name,
22+
'email' => $user->email,
23+
], $resource->toArray());
24+
}
25+
26+
/** @test */
27+
public function it_can_create_a_collection_of_resources()
28+
{
29+
$collection = UserResource::collection(collect([
30+
$user1 = $this->makeUser(),
31+
$user2 = $this->makeUser(),
32+
$user3 = $this->makeUser(),
33+
]));
34+
35+
$this->assertEquals([
36+
[
37+
"id" => $user1->id,
38+
"name" => $user1->name,
39+
"email" => $user1->email,
40+
],
41+
[
42+
"id" => $user2->id,
43+
"name" => $user2->name,
44+
"email" => $user2->email,
45+
],
46+
[
47+
"id" => $user3->id,
48+
"name" => $user3->name,
49+
"email" => $user3->email,
50+
],
51+
], $collection->toArray());
52+
}
53+
54+
/** @test */
55+
public function it_can_include_a_lazy_property()
56+
{
57+
$resource = new LazyResource(
58+
Lazy::create(fn() => 'test')
59+
);
60+
61+
$this->assertEquals([], $resource->toArray());
62+
63+
$this->assertEquals([
64+
'name' => 'test',
65+
], $resource->include('name')->toArray());
66+
}
67+
68+
/** @test */
69+
public function it_can_have_a_filled_in_lazy_property()
70+
{
71+
$resource = new LazyResource('test');
72+
73+
$this->assertEquals([
74+
'name' => 'test',
75+
], $resource->toArray());
76+
77+
$this->assertEquals([
78+
'name' => 'test',
79+
], $resource->include('name')->toArray());
80+
}
81+
82+
private function makeUser(): User
83+
{
84+
return User::make([
85+
'id' => $this->faker()->numberBetween(),
86+
'name' => $this->faker()->name,
87+
'email' => $this->faker()->email,
88+
]);
89+
}
90+
}

‎tests/ExampleTest.php

-12
This file was deleted.

‎tests/Fakes/LazyResource.php

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Tests\Fakes;
4+
5+
use Spatie\LaravelData\Data;
6+
use Spatie\LaravelData\Lazy;
7+
8+
class LazyResource extends Data
9+
{
10+
public function __construct(
11+
public string|Lazy $name
12+
) {
13+
}
14+
}

‎tests/Fakes/SimpleResource.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Tests\Fakes;
4+
5+
use Spatie\LaravelData\Data;
6+
7+
class SimpleResource extends Data
8+
{
9+
public function __construct(
10+
public string $string
11+
) {
12+
}
13+
14+
public static function create($resource): static
15+
{
16+
return new self($resource);
17+
}
18+
}

‎tests/Fakes/TestResource.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Tests\Fakes;
4+
5+
use App\Support\Data\Lazy;
6+
use Spatie\LaravelData\Data;
7+
8+
class TestResource extends Data
9+
{
10+
public function __construct(
11+
public string $string,
12+
public ?string $nullable,
13+
public string | Lazy $lazy,
14+
public array $collection,
15+
) {
16+
}
17+
18+
// public static function make(array $resource): static
19+
// {
20+
// return new self(
21+
// $resource['string'] => $resource['string']
22+
// )
23+
// }
24+
}

‎tests/Fakes/UserResource.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Spatie\LaravelData\Tests\Fakes;
4+
5+
use Illuminate\Foundation\Auth\User;
6+
use Spatie\LaravelData\Data;
7+
8+
class UserResource extends Data
9+
{
10+
public function __construct(
11+
public int $id,
12+
public string $name,
13+
public string $email,
14+
) {
15+
}
16+
17+
public static function create(User $user)
18+
{
19+
return new self(
20+
$user->id,
21+
$user->name,
22+
$user->email,
23+
);
24+
}
25+
}

‎tests/TestCase.php

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
<?php
22

3-
namespace VendorName\Skeleton\Tests;
3+
namespace Spatie\LaravelData\Tests;
44

5+
use Faker\Factory as FakerFactory;
6+
use Faker\Generator;
57
use Illuminate\Database\Eloquent\Factories\Factory;
8+
use Illuminate\Database\Eloquent\Model;
69
use Orchestra\Testbench\TestCase as Orchestra;
7-
use VendorName\Skeleton\SkeletonServiceProvider;
10+
use Spatie\LaravelData\LaravelDataServiceProvider;
811

912
class TestCase extends Orchestra
1013
{
1114
public function setUp(): void
1215
{
1316
parent::setUp();
1417

18+
Model::unguard();
19+
1520
Factory::guessFactoryNamesUsing(
16-
fn (string $modelName) => 'Spatie\\Skeleton\\Database\\Factories\\'.class_basename($modelName).'Factory'
21+
fn (string $modelName) => 'Spatie\\LaravelData\\Database\\Factories\\'.class_basename($modelName).'Factory'
1722
);
1823
}
1924

2025
protected function getPackageProviders($app)
2126
{
2227
return [
23-
SkeletonServiceProvider::class,
28+
LaravelDataServiceProvider::class,
2429
];
2530
}
2631

@@ -29,8 +34,13 @@ public function getEnvironmentSetUp($app)
2934
config()->set('database.default', 'testing');
3035

3136
/*
32-
include_once __DIR__.'/../database/migrations/create_skeleton_table.php.stub';
37+
include_once __DIR__.'/../database/migrations/create_laravel-data-resource_table.php.stub';
3338
(new \CreatePackageTable())->up();
3439
*/
3540
}
41+
42+
public function faker(): Generator
43+
{
44+
return FakerFactory::create();
45+
}
3646
}

0 commit comments

Comments
 (0)
Please sign in to comment.