Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/as-a-data-transfer-object/injecting-property-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,71 @@ class SongData extends Data {

Here, `$slug` will be `never-gonna-give-you-up` even though the route parameter value is `never`.

## Filling properties from route defaults

The `FromRouteDefault` attribute allows filling properties from route default values set via Laravel's `->defaults()` method. This is useful for route grouping and middleware logic where you need to access default values that aren't part of the URL.

```php
Route::get('/songs/archived', [SongController::class, 'index'])
->defaults('status', 'archived');

class SongData extends Data {
#[FromRouteDefault('status')]
public string $status;
}
```

Here, the `$status` property will be filled with `'archived'` from the route defaults.

### Combining with route parameters

You can stack `FromRouteParameter` and `FromRouteDefault` attributes to create a fallback behavior. The first attribute that finds a value will be used:

```php
Route::get('/songs/{status}', [SongController::class, 'index'])
->defaults('status', 'published');

class SongData extends Data {
#[FromRouteParameter('status')]
#[FromRouteDefault('status')]
public string $status;
}
```

When accessing `/songs/archived`, the `$status` property will be `'archived'` (from the route parameter). For a route without a `{status}` parameter, it would fall back to `'published'` from the route defaults.

### Working with enums

Route defaults work seamlessly with enum values:

```php
enum SongStatus: string {
case PUBLISHED = 'published';
case ARCHIVED = 'archived';
}

Route::get('/songs/archived', [SongController::class, 'index'])
->defaults('status', SongStatus::ARCHIVED);

class SongData extends Data {
#[FromRouteDefault('status')]
public SongStatus $status;
}
```

### Controlling payload replacement

Like `FromRouteParameter`, the `FromRouteDefault` attribute supports the `replaceWhenPresentInPayload` flag:

```php
class SongData extends Data {
#[FromRouteDefault('status', replaceWhenPresentInPayload: false)]
public string $status;
}
```

When set to `false`, values from the request payload will take priority over route defaults.

## Filling properties from the authenticated user

The `FromCurrentUser` attribute allows filling properties with values from the authenticated user.
Expand Down
46 changes: 46 additions & 0 deletions src/Attributes/FromRouteDefault.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Spatie\LaravelData\Attributes;

use Attribute;
use Illuminate\Http\Request;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\Skipped;

#[Attribute(Attribute::TARGET_PROPERTY)]
class FromRouteDefault implements InjectsPropertyValue
{
public function __construct(
public string $routeParameter,
public bool $replaceWhenPresentInPayload = true,
) {
}

public function resolve(
DataProperty $dataProperty,
mixed $payload,
array $properties,
CreationContext $creationContext
): mixed {
if (! $payload instanceof Request) {
return Skipped::create();
}

$route = $payload->route();

// Only get from route defaults
$defaults = $route->defaults;

if (isset($defaults[$this->routeParameter])) {
return $defaults[$this->routeParameter];
}

return Skipped::create();
}

public function shouldBeReplacedWhenPresentInPayload(): bool
{
return $this->replaceWhenPresentInPayload;
}
}
109 changes: 109 additions & 0 deletions tests/Attributes/FromRouteDefaultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace Spatie\LaravelData\Tests\Attributes;

use Illuminate\Http\Request;
use Illuminate\Routing\Route;

use function Pest\Laravel\mock;

use Spatie\LaravelData\Attributes\FromRouteDefault;
use Spatie\LaravelData\Data;

test('it can get a value from route defaults', function () {
$dataClass = new class () extends Data {
#[FromRouteDefault('value')]
public string $value;
};

$route = mock(Route::class);
$route->defaults = ['value' => 'test'];

$requestMock = mock(Request::class);
$requestMock->expects('route')->andReturns($route);
$requestMock->expects('toArray')->andReturns([]);

expect($dataClass::from($requestMock)->value)->toBe('test');
});

test('it wont fill property if default does not exist', function () {
$dataClass = new class () extends Data {
#[FromRouteDefault('missing')]
public ?string $missing;
};

$route = mock(Route::class);
$route->defaults = [];

$requestMock = mock(Request::class);
$requestMock->expects('route')->andReturns($route);
$requestMock->expects('toArray')->andReturns([]);

expect(isset($dataClass::from($requestMock)->missing))->toBeFalse();
});

it('wont fill property from attribute if the payload is not a request', function () {
$dataClass = new class () extends Data {
#[FromRouteDefault('other')]
public ?string $value;
};

$data = $dataClass::from(['value' => 'test']);

expect($data->value)->toBe('test')
->and(isset($data->other))->toBeFalse();
});

test('it can work with enum values in defaults', function () {
enum Status: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
}

$dataClass = new class () extends Data {
#[FromRouteDefault('status')]
public Status $status;
};

$route = mock(Route::class);
$route->defaults = ['status' => Status::ACTIVE];

$requestMock = mock(Request::class);
$requestMock->expects('route')->andReturns($route);
$requestMock->expects('toArray')->andReturns([]);

expect($dataClass::from($requestMock)->status)->toBe(Status::ACTIVE);
});

it('can use route defaults when replaceWhenPresentInPayload is enabled', function () {
$dataClass = new class () extends Data {
#[FromRouteDefault('key')]
public string $key;
};

$route = mock(Route::class);
$route->defaults = ['key' => 'default_value'];

$requestMock = mock(Request::class);
$requestMock->expects('route')->andReturns($route);
$requestMock->expects('toArray')->andReturns(['key' => 'payload_value']);

expect($dataClass::from($requestMock)->key)->toBe('default_value');
});

it('can use payload value when replaceWhenPresentInPayload is disabled', function () {
$dataClass = new class () extends Data {
#[FromRouteDefault('key', replaceWhenPresentInPayload: false)]
public string $key;
};

$route = mock(Route::class);
$route->defaults = ['key' => 'default_value'];

$requestMock = mock(Request::class);
$requestMock->allows('route')->andReturns($route);
$requestMock->expects('toArray')->andReturns(['key' => 'payload_value']);

expect($dataClass::from($requestMock)->key)->toBe('payload_value');
});