Skip to content

Commit

Permalink
Filling properties from current user
Browse files Browse the repository at this point in the history
  • Loading branch information
c-v-c-v committed Oct 9, 2024
1 parent 3c5d141 commit fa03fe4
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 0 deletions.
48 changes: 48 additions & 0 deletions src/Attributes/FromData/FromCurrentUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Spatie\LaravelData\Attributes\FromData;

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

#[Attribute(Attribute::TARGET_PROPERTY)]
class FromCurrentUser implements FromDataAttribute
{
/**
* @param class-string|null $userClass
*/
public function __construct(
public ?string $guard = null,
public bool $replaceWhenPresentInBody = true,
public ?string $userClass = null
) {
}

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

$name = $dataProperty->getInputName();
if (! $this->replaceWhenPresentInBody && array_key_exists($name, $properties)) {
return null;
}

$user = $payload->user($this->guard);

if (
$user === null
|| ($this->userClass && ! ($user instanceof $this->userClass))) {
return null;
}

return $user;
}
}
39 changes: 39 additions & 0 deletions src/Attributes/FromData/FromCurrentUserProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Spatie\LaravelData\Attributes\FromData;

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

#[Attribute(Attribute::TARGET_PROPERTY)]
class FromCurrentUserProperty implements FromDataAttribute
{
public function __construct(
public ?string $guard = null,
public ?string $property = null,
public bool $replaceWhenPresentInBody = true,
public ?string $userClass = null
) {
}

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

$fromCurrentUser = new FromCurrentUser($this->guard, $this->replaceWhenPresentInBody, $this->userClass);
$user = $fromCurrentUser->resolve($dataProperty, $payload, $properties, $creationContext);
if ($user === null) {
return null;
}

return data_get($user, $this->property ?? $dataProperty->name);
}
}
16 changes: 16 additions & 0 deletions src/Attributes/FromData/FromDataAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Spatie\LaravelData\Attributes\FromData;

use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty;

interface FromDataAttribute
{
public function resolve(
DataProperty $dataProperty,
mixed $payload,
array $properties,
CreationContext $creationContext
): mixed;
}
2 changes: 2 additions & 0 deletions src/Concerns/BaseData.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Spatie\LaravelData\DataPipes\AuthorizedDataPipe;
use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe;
use Spatie\LaravelData\DataPipes\FillDataPipe;
use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe;
Expand Down Expand Up @@ -72,6 +73,7 @@ public static function pipeline(): DataPipeline
->through(AuthorizedDataPipe::class)
->through(MapPropertiesDataPipe::class)
->through(FillRouteParameterPropertiesDataPipe::class)
->through(FillDataPipe::class)
->through(ValidatePropertiesDataPipe::class)
->through(DefaultValuesDataPipe::class)
->through(CastPropertiesDataPipe::class);
Expand Down
33 changes: 33 additions & 0 deletions src/DataPipes/FillDataPipe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Spatie\LaravelData\DataPipes;

use Spatie\LaravelData\Attributes\FromData\FromDataAttribute;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataClass;

class FillDataPipe implements DataPipe
{
public function handle(mixed $payload, DataClass $class, array $properties, CreationContext $creationContext): array
{

foreach ($class->properties as $dataProperty) {
/** @var null|FromDataAttribute $attribute */
$attribute = $dataProperty->attributes->first(
fn (object $attribute) => $attribute instanceof FromDataAttribute
);
if ($attribute === null) {
continue;
}

$value = $attribute->resolve($dataProperty, $payload, $properties, $creationContext);
if ($value === null) {
continue;
}

$dataProperty->setValueForProperties($properties, $value);
}

return $properties;
}
}
2 changes: 2 additions & 0 deletions src/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract;
use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe;
use Spatie\LaravelData\DataPipes\FillDataPipe;
use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe;
use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe;

Expand All @@ -40,6 +41,7 @@ public static function pipeline(): DataPipeline
->into(static::class)
->through(MapPropertiesDataPipe::class)
->through(FillRouteParameterPropertiesDataPipe::class)
->through(FillDataPipe::class)
->through(DefaultValuesDataPipe::class)
->through(CastPropertiesDataPipe::class);
}
Expand Down
15 changes: 15 additions & 0 deletions src/Support/DataProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ public function __construct(
public readonly Collection $attributes,
) {
}

public function getInputName(): string
{
return $this->inputMappedName ?? $this->name;
}

public function setValueForProperties(array &$properties, mixed $value): void
{
$name = $this->getInputName();
$properties[$name] = $value;

if ($this->name !== $name) {
$properties[$this->name] = $value;
}
}
}
172 changes: 172 additions & 0 deletions tests/FillCurrentUserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

namespace Spatie\LaravelData\Tests;

use Illuminate\Foundation\Auth\User;
use Illuminate\Http\Request;
use Spatie\LaravelData\Attributes\FromData\FromCurrentUser;
use Spatie\LaravelData\Attributes\FromData\FromCurrentUserProperty;
use Spatie\LaravelData\Data;
use stdClass;

it('can fill data properties with current user', function () {
$dataClass = new class () extends Data {
#[FromCurrentUser]
public User $user;
};

$user = new User();
$requestMock = mock(Request::class);
$requestMock->expects('user')->once()->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns([]);

$data = $dataClass::from($requestMock);

expect($data->user)->toBe($user);
});

it('can fill data properties from current user properties', function () {
$dataClass = new class () extends Data {
#[FromCurrentUserProperty]
public int $id;
#[FromCurrentUserProperty]
public string $name;
#[FromCurrentUserProperty]
public string $email;
};

$user = new User();
$user->id = 123;
$user->name = 'John Doe';
$user->email = 'john.doe@example.com';

$requestMock = mock(Request::class);
$requestMock->expects('user')->times(3)->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns([]);

$data = $dataClass::from($requestMock);

expect($data->id)->toEqual($user->id);
expect($data->name)->toEqual($user->name);
expect($data->email)->toEqual($user->email);
});

it('can fill data properties from specified current user property', function () {
$dataClass = new class () extends Data {
#[FromCurrentUserProperty(property: 'id')]
public int $userId;
#[FromCurrentUserProperty(property: 'name')]
public string $userName;
#[FromCurrentUserProperty(property: 'email')]
public string $userEmail;
};

$user = new User();
$user->id = 123;
$user->name = 'Jane Doe';
$user->email = 'jane.doe@example.com';

$requestMock = mock(Request::class);
$requestMock->expects('user')->times(3)->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns([]);

$data = $dataClass::from($requestMock);

expect($data->userId)->toEqual($user->id);
expect($data->userName)->toEqual($user->name);
expect($data->userEmail)->toEqual($user->email);
});

it('can fill data properties with current user using specified guard', function () {
$dataClass = new class () extends Data {
#[FromCurrentUser('api')]
public User $user;
};

$user = new User();
$requestMock = mock(Request::class);
$requestMock->expects('user')->with('api')->once()->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns([]);

$data = $dataClass::from($requestMock);

expect($data->user)->toBe($user);
});

it('replaces properties when current user exists', function () {
$dataClass = new class () extends Data {
#[FromCurrentUser]
public User $user;
};

$user = new User();
$requestMock = mock(Request::class);
$requestMock->expects('user')->once()->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns(['user' => new User()]);

$data = $dataClass::from($requestMock);

expect($data->user)->toBe($user);
});

it('disables replacing properties when current user exists', function () {
$dataClass = new class () extends Data {
#[FromCurrentUser(replaceWhenPresentInBody: false)]
public User $user;
};

$requestMock = mock(Request::class);
$requestMock->expects('user')->never();
$user = new User();
$requestMock->expects('toArray')->once()->andReturns(['user' => $user]);

$data = $dataClass::from($requestMock);

expect($data->user)->toBe($user);
});

it('fills data properties when replacement is disabled and current user exists with non-existent input property', function () {
$dataClass = new class () extends Data {
#[FromCurrentUser(replaceWhenPresentInBody: false)]
public ?User $user = null;
};

$user = new User();
$requestMock = mock(Request::class);
$requestMock->expects('user')->once()->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns([]);

$data = $dataClass::from($requestMock);

expect($data->user)->toBe($user);
});

it('can fill data properties with current user using specified userClass', function () {
$dataClass = new class () extends Data {
#[FromCurrentUser(userClass: User::class)]
public User $user;
};

$user = new User();
$requestMock = mock(Request::class);
$requestMock->expects('user')->once()->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns([]);
$data = $dataClass::factory()->withoutValidation()->from($requestMock);
expect($data->user)->toBeInstanceOf(User::class);
});

it('skips filling data properties when specified userClass does not match', function () {
$dataClass = new class () extends Data {
#[FromCurrentUser(userClass: User::class)]
public ?User $user = null;
};

$user = new stdClass();
$requestMock = mock(Request::class);
$requestMock->expects('user')->once()->andReturns($user);
$requestMock->expects('toArray')->once()->andReturns([]);

$data = $dataClass::from($requestMock);

expect($data->user)->toBeNull();
});
Loading

0 comments on commit fa03fe4

Please sign in to comment.