Skip to content
Merged
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
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"keywords": [
"laravel",
"laravel-stateful-resources",
"api",
"api",
"resources"
],
"homepage": "https://github.com/farbcodegmbh/laravel-stateful-resources",
Expand Down Expand Up @@ -48,7 +48,10 @@
"Workbench\\App\\": "workbench/app/",
"Workbench\\Database\\Factories\\": "workbench/database/factories/",
"Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
}
},
"files": [
"src/helpers.php"
]
},
"scripts": {
"post-autoload-dump": [
Expand Down
13 changes: 13 additions & 0 deletions config/stateful-resources.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,17 @@
|
*/
'default_state' => State::Full,

/*
|--------------------------------------------------------------------------
| Shared State
|--------------------------------------------------------------------------
|
| Here you can specify whether the stateful resources should use a shared
| for the resource state. If set to true, the state will be shared across
| all resources. If set to false, the state will be scoped to the
| resource instance.
|
*/
'shared_state' => true,
];
3 changes: 2 additions & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export default defineConfig({
{
text: 'Advanced Usage',
items: [
{ text: 'Extending States', link: '/extending-states' }
{ text: 'Extending States', link: '/extending-states' },
{ text: 'State Sharing', link: '/state-sharing' },
]
}
],
Expand Down
56 changes: 56 additions & 0 deletions docs/pages/state-sharing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# State Sharing

State sharing lets you set a resource state once and have it automatically applied to all subsequent resources using the same state—ideal for keeping nested or related resources in sync without repeating state declarations.

## Enabling Shared State

By default, state sharing is enabled. To disable it or change the default, update your `config/stateful-resources.php`:

```php
'shared_state' => true,
```

::: info
Disable shared state when you prefer to assign states explicitly on each resource.
:::

## Setting a Shared State

When you set a state on a resource, all further resources will inherit that state unless you override it:

```php
UserResource::state(State::Minimal)->make($user);
InvoiceResource::make($invoice); // Also in the Minimal state
SubscriptionResource::state(State::Full)->make($subscription); // Overrides to Full
```

## Nested Resources Example

In nested resources, the shared state will be used automatically:

```php
class UserResource extends StatefulJsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'alias' => $this->whenStateFull($this->alias),
'invoices' => $this->whenStateFull(InvoiceResource::collection($this->invoices)), // InvoiceResource automatically has the same state as UserResource
];
}
}
```

## Facade & Helper
You may also set the shared state explicitly through the `ActiveState` facade or the `activeResourceState` helper function instead:

```php
use Farbcode\StatefulResources\Facades\ActiveState;

ActiveState::set('minimal');
// or
activeResourceState()->set('minimal');
```
120 changes: 120 additions & 0 deletions src/ActiveState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace Farbcode\StatefulResources;

use Farbcode\StatefulResources\Concerns\ResolvesState;
use Farbcode\StatefulResources\Contracts\ResourceState;

/**
* ActiveState manages which state is currently active for resources.
*/
class ActiveState
{
use ResolvesState;

private ?string $sharedState;

private array $resourceStates = [];

public function __construct()
{
$this->sharedState = null;
}

/**
* Set the shared state for all resources.
*/
public function setShared(string|ResourceState $state): void
{
$state = $this->resolveState($state);
$this->sharedState = $state;
}

/**
* Get the shared state.
*/
public function getShared(): string
{
return $this->sharedState ?? app(StateRegistry::class)->getDefaultState();
}

/**
* Check if a specific resource class has a state set.
*/
public function matchesShared(string|ResourceState $state): bool
{
$state = $this->resolveState($state);

return $this->getShared() === $state;
}

/**
* Set the state for a specific resource class.
*/
public function setForResource(string $resourceClass, string|ResourceState $state): void
{
$state = $this->resolveState($state);
$this->resourceStates[$resourceClass] = $state;
}

/**
* Get the state for a specific resource class.
*/
public function getForResource(string $resourceClass): string
{
return $this->resourceStates[$resourceClass] ?? app(StateRegistry::class)->getDefaultState();
}

/**
* Check if the current state matches the resource's state.
*/
public function matchesResource(string $resourceClass, string|ResourceState $state): bool
{
$state = $this->resolveState($state);

return $this->getForResource($resourceClass) === $state;
}

/**
* Get the current state for a resource.
* If no resource class is provided, returns the shared state.
*/
public function get(?string $resourceClass = null): string
{
if ($resourceClass === null) {
return $this->getShared();
}

return $this->getForResource($resourceClass);
}

/**
* Set the current state for a resource.
* If no resource class is provided, sets the shared state.
*/
public function set(string|ResourceState $state, ?string $resourceClass = null): void
{
$state = $this->resolveState($state);

if ($resourceClass === null) {
$this->setShared($state);
} else {
$this->setForResource($resourceClass, $state);
}
}

/**
* Check if the current state matches the given state for a resource.
* If no resource class is provided, checks against the shared state.
*/
public function matches(string|ResourceState $state, ?string $resourceClass = null): bool
{
$state = $this->resolveState($state);

if ($resourceClass === null) {
return $this->matchesShared($state);
}

return $this->matchesResource($resourceClass, $state);
}
}
18 changes: 10 additions & 8 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Farbcode\StatefulResources\Concerns\ResolvesState;
use Farbcode\StatefulResources\Contracts\ResourceState;
use Illuminate\Support\Facades\Context;

/**
* Builder for creating resource instances with a specific state.
Expand All @@ -15,6 +14,9 @@ class Builder
{
use ResolvesState;

/**
* @var class-string<StatefulJsonResource>
*/
private string $resourceClass;

private string $state;
Expand All @@ -36,21 +38,21 @@ public function __construct(string $resourceClass, string|ResourceState $state)
/**
* Create a single resource instance.
*/
public function make($resource)
public function make(...$parameters)
{
return Context::scope(function () use ($resource) {
return $this->resourceClass::make($resource);
}, ['resource-state-'.$this->resourceClass => $this->state]);
$this->setActiveState($this->resourceClass, $this->state);

return $this->resourceClass::make(...$parameters);
}

/**
* Create a resource collection.
*/
public function collection($resource)
{
return Context::scope(function () use ($resource) {
return $this->resourceClass::collection($resource);
}, ['resource-state-'.$this->resourceClass => $this->state]);
$this->setActiveState($this->resourceClass, $this->state);

return $this->resourceClass::collection($resource);
}

/**
Expand Down
21 changes: 21 additions & 0 deletions src/Concerns/ResolvesState.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Farbcode\StatefulResources\Concerns;

use Farbcode\StatefulResources\ActiveState;
use Farbcode\StatefulResources\Contracts\ResourceState;
use Farbcode\StatefulResources\StateRegistry;
use Illuminate\Support\Str;
Expand Down Expand Up @@ -46,4 +47,24 @@ protected static function resolveStateFromMethodName(string $state): ?string

return null;
}

/**
* Get the active state for the resource.
*/
protected function getActiveState($resourceClass): string
{
return config()->boolean('stateful-resources.shared_state', false)
? app(ActiveState::class)->getShared()
: app(ActiveState::class)->getForResource($resourceClass);
}

/**
* Set the active state for the resource.
*/
protected function setActiveState(string $resourceClass, string $state): void
{
config()->boolean('stateful-resources.shared_state', false)
? app(ActiveState::class)->setShared($state)
: app(ActiveState::class)->setForResource($resourceClass, $state);
}
}
29 changes: 29 additions & 0 deletions src/Facades/ActiveState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Farbcode\StatefulResources\Facades;

use Farbcode\StatefulResources\ActiveState as ActiveStateService;
use Farbcode\StatefulResources\Contracts\ResourceState;
use Illuminate\Support\Facades\Facade;

/**
* @method static void setShared(string|ResourceState $state)
* @method static string getShared()
* @method static void setForResource(string $resourceClass, string|ResourceState $state)
* @method static string getForResource(string $resourceClass)
* @method static bool matchesShared(string|ResourceState $state)
* @method static bool matchesResource(string $resourceClass, string|ResourceState $state)
* @method static string get(?string $resourceClass = null)
* @method static void set(string|ResourceState $state, ?string $resourceClass = null)
* @method static bool matches(string|ResourceState $state, ?string $resourceClass = null)
*/
class ActiveState extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return ActiveStateService::class;
}
}
5 changes: 1 addition & 4 deletions src/StatefulJsonResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Farbcode\StatefulResources\Concerns\StatefullyLoadsAttributes;
use Farbcode\StatefulResources\Contracts\ResourceState;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Context;

abstract class StatefulJsonResource extends JsonResource
{
Expand Down Expand Up @@ -37,9 +36,7 @@ protected function getState(): string
*/
public function __construct($resource)
{
$defaultState = app(StateRegistry::class)->getDefaultState();

$this->state = Context::get('resource-state-'.static::class, $defaultState);
$this->state = $this->getActiveState(static::class);
parent::__construct($resource);
}

Expand Down
4 changes: 4 additions & 0 deletions src/StatefulResourcesServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,9 @@ public function bootingPackage(): void

return $registry;
});

$this->app->singleton(ActiveState::class, function () {
return new ActiveState;
});
}
}
10 changes: 10 additions & 0 deletions src/helpers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

use Farbcode\StatefulResources\ActiveState;

if (! function_exists('activeResourceState')) {
function activeResourceState(): ActiveState
{
return app(ActiveState::class);
}
}
Loading