From 306619274775ad507dcfaf0ce7752c8b76386080 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Thu, 17 Jul 2025 19:14:16 +0200 Subject: [PATCH 01/14] feat: add custom resource states, refactor naming --- config/stateful-resources.php | 15 ++++- src/Builder.php | 7 +- src/Concerns/AsResourceState.php | 22 +++++++ src/Concerns/StatefullyLoadsAttributes.php | 10 ++- src/Contracts/ResourceState.php | 31 +++++++++ src/Enums/ResourceState.php | 10 --- src/Enums/Variant.php | 15 +++++ src/StateRegistry.php | 76 ++++++++++++++++++++++ src/StatefulJsonResource.php | 7 +- src/StatefulResourcesServiceProvider.php | 18 +++++ 10 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 src/Concerns/AsResourceState.php create mode 100644 src/Contracts/ResourceState.php delete mode 100644 src/Enums/ResourceState.php create mode 100644 src/Enums/Variant.php create mode 100644 src/StateRegistry.php diff --git a/config/stateful-resources.php b/config/stateful-resources.php index 3ac44ad..6b1deaf 100644 --- a/config/stateful-resources.php +++ b/config/stateful-resources.php @@ -1,5 +1,18 @@ [ + // App\Enums\CustomResourceState::class, + ], ]; diff --git a/src/Builder.php b/src/Builder.php index f8abb45..7c06bc3 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -2,9 +2,14 @@ namespace Farbcode\StatefulResources; -use Farbcode\StatefulResources\Enums\ResourceState; +use Farbcode\StatefulResources\Contracts\ResourceState; use Illuminate\Support\Facades\Context; +/** + * Builder for creating resource instances with a specific state. + * + * @internal + */ class Builder { private string $resourceClass; diff --git a/src/Concerns/AsResourceState.php b/src/Concerns/AsResourceState.php new file mode 100644 index 0000000..6e7fb29 --- /dev/null +++ b/src/Concerns/AsResourceState.php @@ -0,0 +1,22 @@ +value; + } + + /** + * Get the name of the state. + */ + public function name(): string + { + return $this->name; + } +} diff --git a/src/Concerns/StatefullyLoadsAttributes.php b/src/Concerns/StatefullyLoadsAttributes.php index 78cc36f..32679db 100644 --- a/src/Concerns/StatefullyLoadsAttributes.php +++ b/src/Concerns/StatefullyLoadsAttributes.php @@ -2,7 +2,8 @@ namespace Farbcode\StatefulResources\Concerns; -use Farbcode\StatefulResources\Enums\ResourceState; +use Farbcode\StatefulResources\Contracts\ResourceState; +use Farbcode\StatefulResources\StateRegistry; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; use Illuminate\Http\Resources\MergeValue; use Illuminate\Http\Resources\MissingValue; @@ -155,7 +156,12 @@ public function __call($method, $parameters) continue; } - return $this->{$singleStateMethod}(ResourceState::from($state), ...$parameters); + $stateInstance = StateRegistry::tryFrom($state); + if ($stateInstance === null) { + continue; + } + + return $this->{$singleStateMethod}($stateInstance, ...$parameters); } } diff --git a/src/Contracts/ResourceState.php b/src/Contracts/ResourceState.php new file mode 100644 index 0000000..f8365bd --- /dev/null +++ b/src/Contracts/ResourceState.php @@ -0,0 +1,31 @@ +state = Context::get('resource-state-'.static::class, ResourceState::Full); + $this->state = Context::get('resource-state-'.static::class, Variant::Full); parent::__construct($resource); } @@ -56,7 +57,7 @@ public function __construct($resource) */ public static function __callStatic($method, $parameters) { - $state = ResourceState::tryFrom($method); + $state = StateRegistry::tryFrom($method); if ($state === null) { return parent::__callStatic($method, $parameters); diff --git a/src/StatefulResourcesServiceProvider.php b/src/StatefulResourcesServiceProvider.php index ee288fe..435df74 100644 --- a/src/StatefulResourcesServiceProvider.php +++ b/src/StatefulResourcesServiceProvider.php @@ -2,6 +2,7 @@ namespace Farbcode\StatefulResources; +use Farbcode\StatefulResources\Enums\Variant; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -18,4 +19,21 @@ public function configurePackage(Package $package): void ->name('stateful-resources') ->hasConfigFile(); } + + public function bootingPackage(): void + { + $customStates = config()->array('stateful-resources.custom_states'); + + $this->app->singleton(StateRegistry::class, function () use ($customStates) { + $registry = new StateRegistry; + + $registry->register(Variant::class); + + foreach ($customStates as $stateClass) { + $registry->register($stateClass); + } + + return $registry; + }); + } } From 97bad6e2d3dfb98db896f0761e449d1d84c29e86 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Thu, 17 Jul 2025 19:15:38 +0200 Subject: [PATCH 02/14] feat: setup testing, add basic feature tests --- tests/Feature/CustomStatesTest.php | 25 ++++++ tests/Feature/DefaultStatesTest.php | 75 ++++++++++++++++++ tests/TestCase.php | 8 +- workbench/app/Enums/CustomResourceStates.php | 13 ++++ workbench/app/Http/Resources/CatResource.php | 28 +++++++ workbench/app/Models/Cat.php | 19 +++++ .../Providers/WorkbenchServiceProvider.php | 4 +- workbench/database/factories/CatFactory.php | 76 +++++++++++++++++++ .../2025_07_17_142334_create_cats_table.php | 31 ++++++++ workbench/database/seeders/DatabaseSeeder.php | 3 + 10 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/CustomStatesTest.php create mode 100644 tests/Feature/DefaultStatesTest.php create mode 100644 workbench/app/Enums/CustomResourceStates.php create mode 100644 workbench/app/Http/Resources/CatResource.php create mode 100644 workbench/app/Models/Cat.php create mode 100644 workbench/database/factories/CatFactory.php create mode 100644 workbench/database/migrations/2025_07_17_142334_create_cats_table.php diff --git a/tests/Feature/CustomStatesTest.php b/tests/Feature/CustomStatesTest.php new file mode 100644 index 0000000..4c6ff2e --- /dev/null +++ b/tests/Feature/CustomStatesTest.php @@ -0,0 +1,25 @@ +createOne(); +}); + +it('can use a custom user-defined state', function () { + /** @var TestCase $this */ + $cat = Cat::firstOrFail(); + + $resource = CatResource::state(CustomResourceStates::Custom)->make($cat)->toJson(); + + expect($resource)->toBeJson(); + + expect($resource)->json()->toEqual([ + 'id' => $cat->id, + 'name' => $cat->name, + 'custom_field' => 'custom_value', + ]); +}); diff --git a/tests/Feature/DefaultStatesTest.php b/tests/Feature/DefaultStatesTest.php new file mode 100644 index 0000000..143935f --- /dev/null +++ b/tests/Feature/DefaultStatesTest.php @@ -0,0 +1,75 @@ +createOne(); +}); + +it('can return a stateful resource with the default state', function () { + /** @var TestCase $this */ + $cat = Cat::firstOrFail(); + + $resource = CatResource::make($cat)->toJson(); + + expect($resource)->toBeJson(); + + expect($resource)->json()->toEqual([ + 'id' => $cat->id, + 'name' => $cat->name, + 'breed' => $cat->breed, + 'fluffyness' => $cat->fluffyness, + 'color' => $cat->color, + ]); + +}); + +it('can return a stateful resource with the correct "full" state', function () { + /** @var TestCase $this */ + $cat = Cat::firstOrFail(); + + $resource = CatResource::state(Variant::Full)->make($cat)->toJson(); + + expect($resource)->toBeJson(); + + expect($resource)->json()->toEqual([ + 'id' => $cat->id, + 'name' => $cat->name, + 'breed' => $cat->breed, + 'fluffyness' => $cat->fluffyness, + 'color' => $cat->color, + ]); +}); + +it('can use a stateful resource with the "minimal" state', function () { + /** @var TestCase $this */ + $cat = Cat::firstOrFail(); + + $resource = CatResource::state(Variant::Minimal)->make($cat)->toJson(); + + expect($resource)->toBeJson(); + + expect($resource)->json()->toEqual([ + 'id' => $cat->id, + 'name' => $cat->name, + ]); +}); + +it('can use a stateful resource with the "table" state', function () { + /** @var TestCase $this */ + $cat = Cat::firstOrFail(); + + $resource = CatResource::state(Variant::Table)->make($cat)->toJson(); + + expect($resource)->toBeJson(); + + expect($resource)->json()->toEqual([ + 'id' => $cat->id, + 'name' => $cat->name, + 'breed' => $cat->breed, + ]); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 52176b1..66f34de 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,12 +3,14 @@ namespace Farbcode\StatefulResources\Tests; use Farbcode\StatefulResources\StatefulResourcesServiceProvider; +use Illuminate\Foundation\Testing\RefreshDatabase; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as Orchestra; +use Workbench\App\Enums\CustomResourceStates; class TestCase extends Orchestra { - use WithWorkbench; + use RefreshDatabase, WithWorkbench; protected $loadEnvironmentVariables = false; @@ -26,7 +28,9 @@ protected function getPackageProviders($app) protected function getEnvironmentSetUp($app) { - // + $app['config']->set('stateful-resources.custom_states', [ + CustomResourceStates::class, + ]); } /** diff --git a/workbench/app/Enums/CustomResourceStates.php b/workbench/app/Enums/CustomResourceStates.php new file mode 100644 index 0000000..e8f87d9 --- /dev/null +++ b/workbench/app/Enums/CustomResourceStates.php @@ -0,0 +1,13 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'breed' => $this->whenStateIn([Variant::Full, Variant::Table], $this->breed), + 'fluffyness' => $this->whenStateIn([Variant::Full], $this->fluffyness), + 'color' => $this->whenStateIn([Variant::Full], $this->color), + 'custom_field' => $this->whenState(CustomResourceStates::Custom, 'custom_value'), + ]; + } +} diff --git a/workbench/app/Models/Cat.php b/workbench/app/Models/Cat.php new file mode 100644 index 0000000..ae8c6ab --- /dev/null +++ b/workbench/app/Models/Cat.php @@ -0,0 +1,19 @@ +publishes([ - __DIR__.'/../../../workbench/test-data' => public_path('test-data'), - ], 'test-assets'); + // } } diff --git a/workbench/database/factories/CatFactory.php b/workbench/database/factories/CatFactory.php new file mode 100644 index 0000000..2a95b5e --- /dev/null +++ b/workbench/database/factories/CatFactory.php @@ -0,0 +1,76 @@ + + */ +class CatFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var class-string + */ + protected $model = Cat::class; + + protected $names = [ + 'Whiskers', + 'Mittens', + 'Shadow', + 'Simba', + 'Luna', + 'Oliver', + 'Bella', + 'Charlie', + 'Lucy', + 'Max', + ]; + + protected $breeds = [ + 'Siamese', + 'Persian', + 'Maine Coon', + 'Bengal', + 'British Shorthair', + 'Ragdoll', + 'Sphynx', + 'Norwegian Forest', + 'Scottish Fold', + 'Abyssinian', + ]; + + protected $fluffyness = [ + 'superfluffy', + 'fluffy', + 'not-fluffy', + ]; + + protected $color = [ + 'black', + 'white', + 'gray', + 'orange', + 'brown', + ]; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->randomElement($this->names), + 'breed' => $this->faker->randomElement($this->breeds), + 'fluffyness' => $this->faker->randomElement($this->fluffyness), + 'color' => $this->faker->randomElement($this->color), + ]; + } +} diff --git a/workbench/database/migrations/2025_07_17_142334_create_cats_table.php b/workbench/database/migrations/2025_07_17_142334_create_cats_table.php new file mode 100644 index 0000000..356e819 --- /dev/null +++ b/workbench/database/migrations/2025_07_17_142334_create_cats_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->string('breed'); + $table->string('fluffyness'); + $table->string('color'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cats'); + } +}; diff --git a/workbench/database/seeders/DatabaseSeeder.php b/workbench/database/seeders/DatabaseSeeder.php index f10adbb..832a4dc 100644 --- a/workbench/database/seeders/DatabaseSeeder.php +++ b/workbench/database/seeders/DatabaseSeeder.php @@ -3,6 +3,7 @@ namespace Workbench\Database\Seeders; use Illuminate\Database\Seeder; +use Workbench\Database\Factories\CatFactory; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Workbench\Database\Factories\UserFactory; @@ -19,5 +20,7 @@ public function run(): void 'name' => 'Test User', 'email' => 'test@example.com', ]); + + CatFactory::new()->times(10)->create(); } } From ac92c769d510dcb90952989d2a6fa372e1b4d223 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 10:03:07 +0200 Subject: [PATCH 03/14] fix: make StateRegistry a real singleton --- src/Concerns/StatefullyLoadsAttributes.php | 3 ++- src/StateRegistry.php | 27 ++++++++++++---------- src/StatefulJsonResource.php | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Concerns/StatefullyLoadsAttributes.php b/src/Concerns/StatefullyLoadsAttributes.php index 32679db..a49a939 100644 --- a/src/Concerns/StatefullyLoadsAttributes.php +++ b/src/Concerns/StatefullyLoadsAttributes.php @@ -156,7 +156,8 @@ public function __call($method, $parameters) continue; } - $stateInstance = StateRegistry::tryFrom($state); + $stateInstance = app(StateRegistry::class)->tryFrom($state); + if ($stateInstance === null) { continue; } diff --git a/src/StateRegistry.php b/src/StateRegistry.php index 8052b65..17bb425 100644 --- a/src/StateRegistry.php +++ b/src/StateRegistry.php @@ -10,26 +10,29 @@ */ class StateRegistry { - private static array $stateClasses = []; + /** + * @var array + */ + private array $stateClasses = []; /** * Register a state enum class. */ - public static function register(string $stateClass): void + public function register(string $stateClass): void { if (! is_subclass_of($stateClass, ResourceState::class)) { - throw new InvalidArgumentException("State class {$stateClass} must implement the ResourceState interface."); + throw new InvalidArgumentException("State class {$stateClass} must be a valid ResourceState enum."); } - self::$stateClasses[] = $stateClass; + $this->stateClasses[] = $stateClass; } /** * Try to find a state by value across all registered state classes. */ - public static function tryFrom(string $value): ?ResourceState + public function tryFrom(string $value): ?ResourceState { - foreach (self::$stateClasses as $stateClass) { + foreach ($this->stateClasses as $stateClass) { $state = $stateClass::tryFrom($value); if ($state !== null) { return $state; @@ -42,9 +45,9 @@ public static function tryFrom(string $value): ?ResourceState /** * Find a state by value across all registered state classes. */ - public static function from(string $value): ResourceState + public function from(string $value): ResourceState { - $state = self::tryFrom($value); + $state = $this->tryFrom($value); if ($state === null) { throw new InvalidArgumentException("Unknown state: {$value}"); @@ -56,10 +59,10 @@ public static function from(string $value): ResourceState /** * Get all available states from all registered classes. */ - public static function all(): array + public function all(): array { $states = []; - foreach (self::$stateClasses as $stateClass) { + foreach ($this->stateClasses as $stateClass) { $states = array_merge($states, $stateClass::cases()); } @@ -69,8 +72,8 @@ public static function all(): array /** * Clear all registered state classes. */ - public static function clear(): void + public function clear(): void { - self::$stateClasses = []; + $this->stateClasses = []; } } diff --git a/src/StatefulJsonResource.php b/src/StatefulJsonResource.php index c7d64cd..542cd37 100755 --- a/src/StatefulJsonResource.php +++ b/src/StatefulJsonResource.php @@ -57,7 +57,7 @@ public function __construct($resource) */ public static function __callStatic($method, $parameters) { - $state = StateRegistry::tryFrom($method); + $state = app(StateRegistry::class)->tryFrom($method); if ($state === null) { return parent::__callStatic($method, $parameters); From 6d3a16b8239e0b0f6a7dbc721d609a4d3b980d01 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 10:04:13 +0200 Subject: [PATCH 04/14] refactor: simplify ResourceState enums --- config/stateful-resources.php | 3 +- src/Concerns/AsResourceState.php | 22 ------------- src/Contracts/ResourceState.php | 33 ++++---------------- src/Enums/Variant.php | 3 -- workbench/app/Enums/CustomResourceStates.php | 3 -- 5 files changed, 7 insertions(+), 57 deletions(-) delete mode 100644 src/Concerns/AsResourceState.php diff --git a/config/stateful-resources.php b/config/stateful-resources.php index 6b1deaf..5c3d771 100644 --- a/config/stateful-resources.php +++ b/config/stateful-resources.php @@ -8,8 +8,7 @@ | | Below you may register custom resource states that you want to use inside | your stateful resources. Each state must be a valid enum class that - | implements the ResourceState interface and uses the AsResourceState - | trait. + | implements the ResourceState interface. | */ 'custom_states' => [ diff --git a/src/Concerns/AsResourceState.php b/src/Concerns/AsResourceState.php deleted file mode 100644 index 6e7fb29..0000000 --- a/src/Concerns/AsResourceState.php +++ /dev/null @@ -1,22 +0,0 @@ -value; - } - - /** - * Get the name of the state. - */ - public function name(): string - { - return $this->name; - } -} diff --git a/src/Contracts/ResourceState.php b/src/Contracts/ResourceState.php index f8365bd..b8c08ab 100644 --- a/src/Contracts/ResourceState.php +++ b/src/Contracts/ResourceState.php @@ -2,30 +2,9 @@ namespace Farbcode\StatefulResources\Contracts; -interface ResourceState -{ - /** - * Get the string value of the state. - */ - public function value(): string; - - /** - * Get the name of the state. - */ - public function name(): string; - - /** - * Create a state instance from a string value. - */ - public static function from(string $value): static; - - /** - * Try to create a state instance from a string value. - */ - public static function tryFrom(string $value): ?static; - - /** - * Get all available states. - */ - public static function cases(): array; -} +/** + * Interface for resource state enums that must be backed by string values. + * + * @template T of string + */ +interface ResourceState extends \BackedEnum {} diff --git a/src/Enums/Variant.php b/src/Enums/Variant.php index c4fb5ed..77a10a2 100644 --- a/src/Enums/Variant.php +++ b/src/Enums/Variant.php @@ -2,13 +2,10 @@ namespace Farbcode\StatefulResources\Enums; -use Farbcode\StatefulResources\Concerns\AsResourceState; use Farbcode\StatefulResources\Contracts\ResourceState; enum Variant: string implements ResourceState { - use AsResourceState; - case Minimal = 'minimal'; case Table = 'table'; case Full = 'full'; diff --git a/workbench/app/Enums/CustomResourceStates.php b/workbench/app/Enums/CustomResourceStates.php index e8f87d9..0e79b60 100644 --- a/workbench/app/Enums/CustomResourceStates.php +++ b/workbench/app/Enums/CustomResourceStates.php @@ -2,12 +2,9 @@ namespace Workbench\App\Enums; -use Farbcode\StatefulResources\Concerns\AsResourceState; use Farbcode\StatefulResources\Contracts\ResourceState; enum CustomResourceStates: string implements ResourceState { - use AsResourceState; - case Custom = 'custom'; } From 355f95313e686bc4557318948667040727fe8cf2 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 10:29:40 +0200 Subject: [PATCH 05/14] chore: update illuminate/contracts dependency to ^12.1 --- composer.json | 2 +- docs/pages/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2488156..be77ddb 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ ], "require": { "php": "^8.4", - "illuminate/contracts": "^12.0", + "illuminate/contracts": "^12.1", "spatie/laravel-package-tools": "^1.16" }, "require-dev": { diff --git a/docs/pages/installation.md b/docs/pages/installation.md index a74362b..b50e16b 100644 --- a/docs/pages/installation.md +++ b/docs/pages/installation.md @@ -3,7 +3,7 @@ ## Requirements - PHP \>= 8.4 -- Laravel 12.x +- Laravel 12.1+ ## Installation From 1db20fcfc64bacb57ceb42ddba2859e4ca3d4af6 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 15:36:06 +0200 Subject: [PATCH 06/14] feat: add make:stateful-resource command, corresponding stub and test --- .../Commands/StatefulResourceMakeCommand.php | 85 +++++++++++++++++++ .../Commands/stubs/stateful-resource.stub | 19 +++++ src/StatefulResourcesServiceProvider.php | 6 +- tests/Feature/MakeCommandTest.php | 25 ++++++ workbench/app/Models/Cat.php | 8 ++ 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/Console/Commands/StatefulResourceMakeCommand.php create mode 100644 src/Console/Commands/stubs/stateful-resource.stub create mode 100644 tests/Feature/MakeCommandTest.php diff --git a/src/Console/Commands/StatefulResourceMakeCommand.php b/src/Console/Commands/StatefulResourceMakeCommand.php new file mode 100644 index 0000000..5a4a37f --- /dev/null +++ b/src/Console/Commands/StatefulResourceMakeCommand.php @@ -0,0 +1,85 @@ +resolveStubPath('/stubs/stateful-resource.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return __DIR__.$stub; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace.'\Http\Resources'; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the resource already exists'], + ]; + } +} diff --git a/src/Console/Commands/stubs/stateful-resource.stub b/src/Console/Commands/stubs/stateful-resource.stub new file mode 100644 index 0000000..a6718a0 --- /dev/null +++ b/src/Console/Commands/stubs/stateful-resource.stub @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/src/StatefulResourcesServiceProvider.php b/src/StatefulResourcesServiceProvider.php index 435df74..65d5598 100644 --- a/src/StatefulResourcesServiceProvider.php +++ b/src/StatefulResourcesServiceProvider.php @@ -2,7 +2,6 @@ namespace Farbcode\StatefulResources; -use Farbcode\StatefulResources\Enums\Variant; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -17,6 +16,9 @@ public function configurePackage(Package $package): void */ $package ->name('stateful-resources') + ->hasCommands([ + Console\Commands\StatefulResourceMakeCommand::class, + ]) ->hasConfigFile(); } @@ -27,7 +29,7 @@ public function bootingPackage(): void $this->app->singleton(StateRegistry::class, function () use ($customStates) { $registry = new StateRegistry; - $registry->register(Variant::class); + $registry->register(Enums\Variant::class); foreach ($customStates as $stateClass) { $registry->register($stateClass); diff --git a/tests/Feature/MakeCommandTest.php b/tests/Feature/MakeCommandTest.php new file mode 100644 index 0000000..5171fd7 --- /dev/null +++ b/tests/Feature/MakeCommandTest.php @@ -0,0 +1,25 @@ +testResourcePath = app_path('Http/Resources/TestResource.php'); +}); + +afterEach(function () { + if (File::exists($this->testResourcePath)) { + File::delete($this->testResourcePath); + } +}); + +it('can create a new stateful resource', function () { + /** @var TestCase $this */ + $this->artisan('make:stateful-resource', [ + 'name' => 'TestResource', + ]) + ->assertExitCode(0) + ->expectsOutputToContain('created successfully.'); + + $this->assertFileExists(app_path('Http/Resources/TestResource.php')); +}); \ No newline at end of file diff --git a/workbench/app/Models/Cat.php b/workbench/app/Models/Cat.php index ae8c6ab..14c38e4 100644 --- a/workbench/app/Models/Cat.php +++ b/workbench/app/Models/Cat.php @@ -2,6 +2,7 @@ namespace Workbench\App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; /** @@ -15,5 +16,12 @@ */ class Cat extends Model { + use HasFactory; + + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = ['name', 'breed', 'fluffyness', 'color']; } From 1300252be8809d815fe27969dbcf0de3f15ceafc Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 16:50:11 +0200 Subject: [PATCH 07/14] feat: improve state management with new state resolution and registration, add configurable default state --- config/stateful-resources.php | 26 +++++-- src/Builder.php | 17 ++++- src/Concerns/ResolvesState.php | 25 +++++++ src/Concerns/StatefullyLoadsAttributes.php | 28 +++++--- src/Enums/Variant.php | 4 +- src/StateRegistry.php | 74 ++++++++++---------- src/StatefulJsonResource.php | 11 +-- src/StatefulResourcesServiceProvider.php | 15 ++-- tests/Feature/CustomStatesTest.php | 11 ++- tests/TestCase.php | 6 +- workbench/app/Enums/CustomResourceStates.php | 10 --- workbench/app/Http/Resources/CatResource.php | 3 +- 12 files changed, 145 insertions(+), 85 deletions(-) create mode 100644 src/Concerns/ResolvesState.php delete mode 100644 workbench/app/Enums/CustomResourceStates.php diff --git a/config/stateful-resources.php b/config/stateful-resources.php index 5c3d771..fe4a2eb 100644 --- a/config/stateful-resources.php +++ b/config/stateful-resources.php @@ -1,17 +1,31 @@ [ - // App\Enums\CustomResourceState::class, + 'states' => [ + ...Variant::cases(), + // ], + + /* + |-------------------------------------------------------------------------- + | Default State + |-------------------------------------------------------------------------- + | + | This state will be used when no state is explicitly set on the resource. + | If not set, the first state in the states array will be used. + | + */ + 'default_state' => Variant::Full, ]; diff --git a/src/Builder.php b/src/Builder.php index 7c06bc3..a459ff7 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -2,6 +2,7 @@ namespace Farbcode\StatefulResources; +use Farbcode\StatefulResources\Concerns\ResolvesState; use Farbcode\StatefulResources\Contracts\ResourceState; use Illuminate\Support\Facades\Context; @@ -12,14 +13,24 @@ */ class Builder { + use ResolvesState; + private string $resourceClass; - private ResourceState $state; + private string $state; - public function __construct(string $resourceClass, ResourceState $state) + public function __construct(string $resourceClass, string|ResourceState $state) { + $state = $this->resolveState($state); + + $registeredState = app(StateRegistry::class)->tryFrom($state); + + if ($registeredState === null) { + throw new \InvalidArgumentException("State \"{$state}\" is not registered."); + } + $this->resourceClass = $resourceClass; - $this->state = $state; + $this->state = $registeredState; } /** diff --git a/src/Concerns/ResolvesState.php b/src/Concerns/ResolvesState.php new file mode 100644 index 0000000..2e5e61c --- /dev/null +++ b/src/Concerns/ResolvesState.php @@ -0,0 +1,25 @@ +value : $state; + + if (app(StateRegistry::class)->tryFrom($stateString) === null) { + throw new \InvalidArgumentException("State \"{$stateString}\" is not registered."); + } + + return $stateString; + } +} \ No newline at end of file diff --git a/src/Concerns/StatefullyLoadsAttributes.php b/src/Concerns/StatefullyLoadsAttributes.php index a49a939..0857a4e 100644 --- a/src/Concerns/StatefullyLoadsAttributes.php +++ b/src/Concerns/StatefullyLoadsAttributes.php @@ -26,18 +26,20 @@ */ trait StatefullyLoadsAttributes { - use ConditionallyLoadsAttributes; + use ConditionallyLoadsAttributes, ResolvesState; /** * Retrieve a value if the current state matches the given state. * - * @param ResourceState $state + * @param string|ResourceState $state * @param mixed $value * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ protected function whenState($state, $value, $default = null) { + $state = $this->resolveState($state); + if (func_num_args() === 3) { return $this->when($this->getState() === $state, $value, $default); } @@ -48,13 +50,15 @@ protected function whenState($state, $value, $default = null) /** * Retrieve a value unless the current state matches the given state. * - * @param ResourceState $state + * @param string|ResourceState $state * @param mixed $value * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ protected function unlessState($state, $value, $default = null) { + $state = $this->resolveState($state); + if (func_num_args() === 3) { return $this->unless($this->getState() === $state, $value, $default); } @@ -65,13 +69,15 @@ protected function unlessState($state, $value, $default = null) /** * Retrieve a value if the current state is one of the given states. * - * @param array $states + * @param array $states * @param mixed $value * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ protected function whenStateIn(array $states, $value, $default = null) { + $states = array_map(fn ($state) => $this->resolveState($state), $states); + $condition = in_array($this->getState(), $states, true); if (func_num_args() === 3) { @@ -84,13 +90,15 @@ protected function whenStateIn(array $states, $value, $default = null) /** * Retrieve a value unless the current state is one of the given states. * - * @param array $states + * @param array $states * @param mixed $value * @param mixed $default * @return \Illuminate\Http\Resources\MissingValue|mixed */ protected function unlessStateIn(array $states, $value, $default = null) { + $states = array_map(fn ($state) => $this->resolveState($state), $states); + $condition = in_array($this->getState(), $states, true); if (func_num_args() === 3) { @@ -103,13 +111,15 @@ protected function unlessStateIn(array $states, $value, $default = null) /** * Merge a value if the current state matches the given state. * - * @param ResourceState $state + * @param string|ResourceState $state * @param mixed $value * @param mixed $default * @return \Illuminate\Http\Resources\MergeValue|mixed */ protected function mergeWhenState($state, $value, $default = null) { + $state = $this->resolveState($state); + if (func_num_args() === 3) { return $this->mergeWhen($this->getState() === $state, $value, $default); } @@ -120,13 +130,15 @@ protected function mergeWhenState($state, $value, $default = null) /** * Merge a value unless the current state matches the given state. * - * @param ResourceState $state + * @param string|ResourceState $state * @param mixed $value * @param mixed $default * @return \Illuminate\Http\Resources\MergeValue|mixed */ protected function mergeUnlessState($state, $value, $default = null) { + $state = $this->resolveState($state); + if (func_num_args() === 3) { return $this->mergeUnless($this->getState() === $state, $value, $default); } @@ -138,7 +150,7 @@ protected function mergeUnlessState($state, $value, $default = null) * Get the current state of the resource. * This method should be implemented by the class using this trait. */ - abstract protected function getState(): ResourceState; + abstract protected function getState(): string; public function __call($method, $parameters) { diff --git a/src/Enums/Variant.php b/src/Enums/Variant.php index 77a10a2..2c11ec5 100644 --- a/src/Enums/Variant.php +++ b/src/Enums/Variant.php @@ -6,7 +6,7 @@ enum Variant: string implements ResourceState { - case Minimal = 'minimal'; - case Table = 'table'; case Full = 'full'; + case Table = 'table'; + case Minimal = 'minimal'; } diff --git a/src/StateRegistry.php b/src/StateRegistry.php index 17bb425..1b25c3d 100644 --- a/src/StateRegistry.php +++ b/src/StateRegistry.php @@ -11,69 +11,67 @@ class StateRegistry { /** - * @var array + * @var string[] List of registered states. */ - private array $stateClasses = []; + private array $states = []; /** - * Register a state enum class. + * Register a state. */ - public function register(string $stateClass): void + public function register(string $state): void { - if (! is_subclass_of($stateClass, ResourceState::class)) { - throw new InvalidArgumentException("State class {$stateClass} must be a valid ResourceState enum."); - } - - $this->stateClasses[] = $stateClass; + $this->states[] = $state; } /** - * Try to find a state by value across all registered state classes. + * Try to find a state by value across all registered states. */ - public function tryFrom(string $value): ?ResourceState + public function tryFrom(string $value): ?string { - foreach ($this->stateClasses as $stateClass) { - $state = $stateClass::tryFrom($value); - if ($state !== null) { - return $state; - } + if (in_array($value, $this->states, true)) { + return $value; } return null; } /** - * Find a state by value across all registered state classes. + * Get all available states from all registered states. + * + * @return string[] List of states. */ - public function from(string $value): ResourceState + public function all(): array { - $state = $this->tryFrom($value); - - if ($state === null) { - throw new InvalidArgumentException("Unknown state: {$value}"); - } - - return $state; + return $this->states; } /** - * Get all available states from all registered classes. + * Clear all registered states. */ - public function all(): array + public function clear(): void { - $states = []; - foreach ($this->stateClasses as $stateClass) { - $states = array_merge($states, $stateClass::cases()); - } - - return $states; + $this->states = []; } - /** - * Clear all registered state classes. - */ - public function clear(): void + public function getDefaultState(): string { - $this->stateClasses = []; + $explicitDefault = config('stateful-resources.default_state'); + + if ($explicitDefault instanceof ResourceState) { + $explicitDefault = $explicitDefault->value; + } + + if ($explicitDefault !== null) { + $state = $this->tryFrom($explicitDefault); + if ($state !== null) { + return $state; + } + } + + if (empty($this->states)) { + throw new InvalidArgumentException('No states registered in the StateRegistry.'); + } + + return $this->states[0]; } } diff --git a/src/StatefulJsonResource.php b/src/StatefulJsonResource.php index 542cd37..cda0ca3 100755 --- a/src/StatefulJsonResource.php +++ b/src/StatefulJsonResource.php @@ -4,7 +4,6 @@ use Farbcode\StatefulResources\Concerns\StatefullyLoadsAttributes; use Farbcode\StatefulResources\Contracts\ResourceState; -use Farbcode\StatefulResources\Enums\Variant; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Context; @@ -17,12 +16,12 @@ abstract class StatefulJsonResource extends JsonResource { use StatefullyLoadsAttributes; - private ResourceState $state; + private string $state; /** * Create a new stateful resource builder with a specific state. */ - public static function state(ResourceState $state): Builder + public static function state(string|ResourceState $state): Builder { return new Builder(static::class, $state); } @@ -30,7 +29,7 @@ public static function state(ResourceState $state): Builder /** * Retrieve the state of the stateful resource. */ - protected function getState(): ResourceState + protected function getState(): string { return $this->state; } @@ -42,7 +41,9 @@ protected function getState(): ResourceState */ public function __construct($resource) { - $this->state = Context::get('resource-state-'.static::class, Variant::Full); + $defaultState = app(StateRegistry::class)->getDefaultState(); + + $this->state = Context::get('resource-state-'.static::class, $defaultState); parent::__construct($resource); } diff --git a/src/StatefulResourcesServiceProvider.php b/src/StatefulResourcesServiceProvider.php index 435df74..54f1277 100644 --- a/src/StatefulResourcesServiceProvider.php +++ b/src/StatefulResourcesServiceProvider.php @@ -2,7 +2,7 @@ namespace Farbcode\StatefulResources; -use Farbcode\StatefulResources\Enums\Variant; +use Farbcode\StatefulResources\Contracts\ResourceState; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -22,15 +22,16 @@ public function configurePackage(Package $package): void public function bootingPackage(): void { - $customStates = config()->array('stateful-resources.custom_states'); + $states = config()->array('stateful-resources.states'); - $this->app->singleton(StateRegistry::class, function () use ($customStates) { + $this->app->singleton(StateRegistry::class, function () use ($states) { $registry = new StateRegistry; - $registry->register(Variant::class); - - foreach ($customStates as $stateClass) { - $registry->register($stateClass); + foreach ($states as $state) { + if ($state instanceof ResourceState) { + $state = $state->value; + } + $registry->register($state); } return $registry; diff --git a/tests/Feature/CustomStatesTest.php b/tests/Feature/CustomStatesTest.php index 4c6ff2e..3684b17 100644 --- a/tests/Feature/CustomStatesTest.php +++ b/tests/Feature/CustomStatesTest.php @@ -1,6 +1,5 @@ make($cat)->toJson(); + $resource = CatResource::state('custom')->make($cat)->toJson(); expect($resource)->toBeJson(); @@ -23,3 +22,11 @@ 'custom_field' => 'custom_value', ]); }); + +it('cannot use an unregistered state', function () { + /** @var TestCase $this */ + $cat = Cat::firstOrFail(); + + expect(fn() => CatResource::state('non_existent')->make($cat)->toJson()) + ->toThrow(InvalidArgumentException::class, 'State "non_existent" is not registered.'); +}); \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 66f34de..83d4095 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace Farbcode\StatefulResources\Tests; +use Farbcode\StatefulResources\Enums\Variant; use Farbcode\StatefulResources\StatefulResourcesServiceProvider; use Illuminate\Foundation\Testing\RefreshDatabase; use Orchestra\Testbench\Concerns\WithWorkbench; @@ -28,8 +29,9 @@ protected function getPackageProviders($app) protected function getEnvironmentSetUp($app) { - $app['config']->set('stateful-resources.custom_states', [ - CustomResourceStates::class, + $app['config']->set('stateful-resources.states', [ + ...Variant::cases(), + 'custom' ]); } diff --git a/workbench/app/Enums/CustomResourceStates.php b/workbench/app/Enums/CustomResourceStates.php deleted file mode 100644 index 0e79b60..0000000 --- a/workbench/app/Enums/CustomResourceStates.php +++ /dev/null @@ -1,10 +0,0 @@ - $this->whenStateIn([Variant::Full, Variant::Table], $this->breed), 'fluffyness' => $this->whenStateIn([Variant::Full], $this->fluffyness), 'color' => $this->whenStateIn([Variant::Full], $this->color), - 'custom_field' => $this->whenState(CustomResourceStates::Custom, 'custom_value'), + 'custom_field' => $this->whenState('custom', 'custom_value'), ]; } } From 9b0dc278606c2d24c6b2730c0a12355d923c5ebf Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 16:51:04 +0200 Subject: [PATCH 08/14] style: fix formatting --- src/Concerns/ResolvesState.php | 4 ++-- tests/Feature/CustomStatesTest.php | 4 ++-- tests/TestCase.php | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Concerns/ResolvesState.php b/src/Concerns/ResolvesState.php index 2e5e61c..ccd7406 100644 --- a/src/Concerns/ResolvesState.php +++ b/src/Concerns/ResolvesState.php @@ -7,7 +7,7 @@ trait ResolvesState { - /** + /** * Resolve the value of a given state. * * @throws \InvalidArgumentException @@ -22,4 +22,4 @@ private function resolveState(ResourceState|string $state): string return $stateString; } -} \ No newline at end of file +} diff --git a/tests/Feature/CustomStatesTest.php b/tests/Feature/CustomStatesTest.php index 3684b17..bff8187 100644 --- a/tests/Feature/CustomStatesTest.php +++ b/tests/Feature/CustomStatesTest.php @@ -27,6 +27,6 @@ /** @var TestCase $this */ $cat = Cat::firstOrFail(); - expect(fn() => CatResource::state('non_existent')->make($cat)->toJson()) + expect(fn () => CatResource::state('non_existent')->make($cat)->toJson()) ->toThrow(InvalidArgumentException::class, 'State "non_existent" is not registered.'); -}); \ No newline at end of file +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 83d4095..f2dc5ef 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,7 +7,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as Orchestra; -use Workbench\App\Enums\CustomResourceStates; class TestCase extends Orchestra { @@ -31,7 +30,7 @@ protected function getEnvironmentSetUp($app) { $app['config']->set('stateful-resources.states', [ ...Variant::cases(), - 'custom' + 'custom', ]); } From 6e0ea1249794fd786c421df57bfa05521be2d2c8 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 16:54:20 +0200 Subject: [PATCH 09/14] fix: remove PHPDoc hints for possibly nonexistent methods --- src/Concerns/StatefullyLoadsAttributes.php | 13 ------------- src/StatefulJsonResource.php | 5 ----- 2 files changed, 18 deletions(-) diff --git a/src/Concerns/StatefullyLoadsAttributes.php b/src/Concerns/StatefullyLoadsAttributes.php index 0857a4e..1f39afe 100644 --- a/src/Concerns/StatefullyLoadsAttributes.php +++ b/src/Concerns/StatefullyLoadsAttributes.php @@ -10,19 +10,6 @@ /** * @see \Illuminate\Http\Resources\ConditionallyLoadsAttributes - * - * @method MissingValue|mixed whenStateMinimal(mixed $value, mixed $default = null) - * @method MissingValue|mixed unlessStateMinimal(mixed $value, mixed $default = null) - * @method MissingValue|mixed whenStateFull(mixed $value, mixed $default = null) - * @method MissingValue|mixed unlessStateFull(mixed $value, mixed $default = null) - * @method MissingValue|mixed whenStateTable(mixed $value, mixed $default = null) - * @method MissingValue|mixed unlessStateTable(mixed $value, mixed $default = null) - * @method MergeValue|mixed mergeWhenStateMinimal(mixed $value, mixed $default = null) - * @method MergeValue|mixed mergeUnlessStateMinimal(mixed $value, mixed $default = null) - * @method MergeValue|mixed mergeWhenStateFull(mixed $value, mixed $default = null) - * @method MergeValue|mixed mergeUnlessStateFull(mixed $value, mixed $default = null) - * @method MissingValue|mixed whenStateTable(mixed $value, mixed $default = null) - * @method MissingValue|mixed unlessStateTable(mixed $value, mixed $default = null) */ trait StatefullyLoadsAttributes { diff --git a/src/StatefulJsonResource.php b/src/StatefulJsonResource.php index cda0ca3..8dcd62c 100755 --- a/src/StatefulJsonResource.php +++ b/src/StatefulJsonResource.php @@ -7,11 +7,6 @@ use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Context; -/** - * @method static \Farbcode\StatefulResources\Builder minimal() - * @method static \Farbcode\StatefulResources\Builder table() - * @method static \Farbcode\StatefulResources\Builder full() - */ abstract class StatefulJsonResource extends JsonResource { use StatefullyLoadsAttributes; From eb45c32a16637aa3bbe830ce70c9668d2c2489bd Mon Sep 17 00:00:00 2001 From: julian-farbcode <110020845+julian-farbcode@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:54:51 +0000 Subject: [PATCH 10/14] style: fix code style issues --- src/Concerns/StatefullyLoadsAttributes.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Concerns/StatefullyLoadsAttributes.php b/src/Concerns/StatefullyLoadsAttributes.php index 1f39afe..75cda5c 100644 --- a/src/Concerns/StatefullyLoadsAttributes.php +++ b/src/Concerns/StatefullyLoadsAttributes.php @@ -5,8 +5,6 @@ use Farbcode\StatefulResources\Contracts\ResourceState; use Farbcode\StatefulResources\StateRegistry; use Illuminate\Http\Resources\ConditionallyLoadsAttributes; -use Illuminate\Http\Resources\MergeValue; -use Illuminate\Http\Resources\MissingValue; /** * @see \Illuminate\Http\Resources\ConditionallyLoadsAttributes From 672f8c94c7b204c753f9303106ae487377fb9e5e Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 17:09:10 +0200 Subject: [PATCH 11/14] chore: remove unnecessary handle method --- src/Console/Commands/StatefulResourceMakeCommand.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Console/Commands/StatefulResourceMakeCommand.php b/src/Console/Commands/StatefulResourceMakeCommand.php index 5a4a37f..3a17233 100644 --- a/src/Console/Commands/StatefulResourceMakeCommand.php +++ b/src/Console/Commands/StatefulResourceMakeCommand.php @@ -29,16 +29,6 @@ class StatefulResourceMakeCommand extends \Illuminate\Console\GeneratorCommand */ protected $type = 'Stateful Resource'; - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - parent::handle(); - } - /** * Get the stub file for the generator. * From d242c961bf6d26b5b6c1310d27b3845f9e1b0740 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 17:09:28 +0200 Subject: [PATCH 12/14] style: fix code style issue --- tests/Feature/MakeCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/MakeCommandTest.php b/tests/Feature/MakeCommandTest.php index 5171fd7..7c28256 100644 --- a/tests/Feature/MakeCommandTest.php +++ b/tests/Feature/MakeCommandTest.php @@ -22,4 +22,4 @@ ->expectsOutputToContain('created successfully.'); $this->assertFileExists(app_path('Http/Resources/TestResource.php')); -}); \ No newline at end of file +}); From d6c9da6a9d82b12bc5d11a88b821e15a190b7f93 Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Tue, 22 Jul 2025 17:48:39 +0200 Subject: [PATCH 13/14] feat: add basic usage and extending states documentation --- docs/.vitepress/config.mts | 7 ++ docs/pages/basic-usage.md | 162 +++++++++++++++++++++++++++++++++ docs/pages/extending-states.md | 79 ++++++++++++++++ docs/pages/installation.md | 2 +- 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 docs/pages/basic-usage.md create mode 100644 docs/pages/extending-states.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 64faefc..fe72342 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -37,8 +37,15 @@ export default defineConfig({ text: 'Basics', items: [ { text: 'Installation', link: '/installation' }, + { text: 'Basic Usage', link: '/basic-usage' }, ] }, + { + text: 'Advanced Usage', + items: [ + { text: 'Extending States', link: '/extending-states' } + ] + } ], socialLinks: [ diff --git a/docs/pages/basic-usage.md b/docs/pages/basic-usage.md new file mode 100644 index 0000000..02be973 --- /dev/null +++ b/docs/pages/basic-usage.md @@ -0,0 +1,162 @@ +# Basic Usage + +Laravel Stateful Resources allows you to create dynamic API responses by changing the structure of your JSON resources based on different states. This is especially useful when you need to return different levels of detail for the same model depending on the context. + +## Generating a Stateful Resource + +The package provides an Artisan command to quickly generate a new stateful resource: + +```bash +php artisan make:stateful-resource UserResource +``` + +This command creates a new resource class in `app/Http/Resources/` that extends `StatefulJsonResource`: + +```php + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} +``` + +## Built-in States + +The package comes with three built-in states defined in the `Variant` enum: + +- **`Full`** - For all available attributes +- **`Table`** - For attributes suitable for table/listing views +- **`Minimal`** - For only essential attributes + +See the [Extending States](extending-states.md) documentation for how to configure this and add custom states. + +## Using States in Resources + +Inside your stateful resource, you can use conditional methods to control which attributes are included based on the current state: + +```php + $this->id, + 'name' => $this->name, + 'email' => $this->whenState(Variant::Full, $this->email), + 'profile' => $this->whenStateIn([Variant::Full], [ + 'bio' => $this->bio, + 'avatar' => $this->avatar, + 'created_at' => $this->created_at, + ]), + 'role' => $this->whenStateIn([Variant::Full, Variant::Table], $this->role), + 'last_login' => $this->unlessState(Variant::Minimal, $this->last_login_at), + ]; + } +} +``` + +You can also use the string representation of states instead of enum cases: + +```php +'email' => $this->whenState('full', $this->email), +'name' => $this->unlessState('minimal', $this->full_name), +``` + +## Available Conditional Methods + +The package provides several methods to conditionally include attributes: + +### `whenState` + +Include a value only when the current state matches the specified state: + +```php +'email' => $this->whenState(Variant::Full, $this->email), +'admin_notes' => $this->whenState(Variant::Full, $this->admin_notes, 'N/A'), +``` + +### `unlessState` + +Include a value unless the current state matches the specified state: + +```php +'public_info' => $this->unlessState(Variant::Minimal, $this->public_information), +``` + +### `whenStateIn` + +Include a value when the current state is one of the specified states: + +```php +'detailed_info' => $this->whenStateIn([Variant::Full, Variant::Table], [ + 'department' => $this->department, + 'position' => $this->position, +]), +``` + +### `unlessStateIn` + +Include a value unless the current state is one of the specified states: + +```php +'sensitive_data' => $this->unlessStateIn([Variant::Minimal, Variant::Table], $this->sensitive_info), +``` + +### Magic Conditionals + +You can also use magic methods with for cleaner syntax: + +```php +'email' => $this->whenStateFull($this->email), +'name' => $this->unlessStateMinimal($this->full_name), +``` + +## Using Stateful Resources + +### Setting the State Explicitly + +Use the static `state()` method to create a resource with a specific state: + +```php +$user = UserResource::state(Variant::Minimal)->make($user); +``` + +### Using Magic Methods + +You can also use magic methods for a more fluent syntax: + +```php +// This is equivalent to the explicit state() call +$user = UserResource::minimal()->make($user); +``` + +### Default State + +If no state is specified, the resource will use the default state. You can change the default state in the package's configuration file: `config/stateful-resources.php`. + +```php +// Uses the default state +$user = UserResource::make($user); +``` diff --git a/docs/pages/extending-states.md b/docs/pages/extending-states.md new file mode 100644 index 0000000..b5412d7 --- /dev/null +++ b/docs/pages/extending-states.md @@ -0,0 +1,79 @@ +# Extending States + +You may find yourself being too limited with the three Variant states included in the package's `Variant` enum. +This package allows you to register custom states that you can then use in your resources. + +## Registering Custom States + +Before using a custom state, register it in the package's `stateful-resources.states` configuration: + +```php + [ + ...Variant::cases(), // The built-in states + 'custom', // Your custom state as a string + ...CustomResourceState::cases(), // Or as cases of a custom enum + ], +]; +``` + +## Creating a Custom State Enum + +Instead of using strings, you may want to create your own state enum to define custom states. This enum should implement the `ResourceState` interface provided by the package. + +```php + $this->id, + 'name' => $this->name, + 'email' => $this->whenState(CustomResourceState::Extended, $this->email), + 'debug_info' => $this->whenStateDebug([ + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]), + 'avatar' => $this->unlessState('custom', $this->avatar), + ]; + } +} +``` + +You can then apply the custom states to your resource in the same way you would with the built-in states: + +```php +// Using the static method +UserResource::state(CustomResourceState::Compact)->make($user); + +// Using the magic method (if the state name matches the case name) +UserResource::compact()->make($user); +``` diff --git a/docs/pages/installation.md b/docs/pages/installation.md index b50e16b..a49857d 100644 --- a/docs/pages/installation.md +++ b/docs/pages/installation.md @@ -3,7 +3,7 @@ ## Requirements - PHP \>= 8.4 -- Laravel 12.1+ +- Laravel \>= 12.1 ## Installation From 68c362bd7122dc9b9ea2f4a3eb528287f2d2c7cd Mon Sep 17 00:00:00 2001 From: Julian Schramm Date: Wed, 23 Jul 2025 14:19:31 +0200 Subject: [PATCH 14/14] refactor: rename Variant enum to State --- config/stateful-resources.php | 6 ++--- docs/pages/basic-usage.md | 26 ++++++++++---------- docs/pages/extending-states.md | 5 ++-- src/Enums/{Variant.php => State.php} | 2 +- tests/Feature/DefaultStatesTest.php | 8 +++--- tests/TestCase.php | 4 +-- workbench/app/Http/Resources/CatResource.php | 8 +++--- 7 files changed, 30 insertions(+), 29 deletions(-) rename src/Enums/{Variant.php => State.php} (80%) diff --git a/config/stateful-resources.php b/config/stateful-resources.php index fe4a2eb..89d1543 100644 --- a/config/stateful-resources.php +++ b/config/stateful-resources.php @@ -1,6 +1,6 @@ [ - ...Variant::cases(), + ...State::cases(), // ], @@ -27,5 +27,5 @@ | If not set, the first state in the states array will be used. | */ - 'default_state' => Variant::Full, + 'default_state' => State::Full, ]; diff --git a/docs/pages/basic-usage.md b/docs/pages/basic-usage.md index 02be973..ddbc767 100644 --- a/docs/pages/basic-usage.md +++ b/docs/pages/basic-usage.md @@ -36,7 +36,7 @@ class UserResource extends StatefulJsonResource ## Built-in States -The package comes with three built-in states defined in the `Variant` enum: +The package comes with three built-in states defined in the `State` enum: - **`Full`** - For all available attributes - **`Table`** - For attributes suitable for table/listing views @@ -53,7 +53,7 @@ Inside your stateful resource, you can use conditional methods to control which namespace App\Http\Resources; -use Farbcode\StatefulResources\Enums\Variant; +use Farbcode\StatefulResources\Enums\State; use Farbcode\StatefulResources\StatefulJsonResource; use Illuminate\Http\Request; @@ -64,14 +64,14 @@ class UserResource extends StatefulJsonResource return [ 'id' => $this->id, 'name' => $this->name, - 'email' => $this->whenState(Variant::Full, $this->email), - 'profile' => $this->whenStateIn([Variant::Full], [ + 'email' => $this->whenState(State::Full, $this->email), + 'profile' => $this->whenStateIn([State::Full], [ 'bio' => $this->bio, 'avatar' => $this->avatar, 'created_at' => $this->created_at, ]), - 'role' => $this->whenStateIn([Variant::Full, Variant::Table], $this->role), - 'last_login' => $this->unlessState(Variant::Minimal, $this->last_login_at), + 'role' => $this->whenStateIn([State::Full, State::Table], $this->role), + 'last_login' => $this->unlessState(State::Minimal, $this->last_login_at), ]; } } @@ -93,8 +93,8 @@ The package provides several methods to conditionally include attributes: Include a value only when the current state matches the specified state: ```php -'email' => $this->whenState(Variant::Full, $this->email), -'admin_notes' => $this->whenState(Variant::Full, $this->admin_notes, 'N/A'), +'email' => $this->whenState(State::Full, $this->email), +'admin_notes' => $this->whenState(State::Full, $this->admin_notes, 'N/A'), ``` ### `unlessState` @@ -102,7 +102,7 @@ Include a value only when the current state matches the specified state: Include a value unless the current state matches the specified state: ```php -'public_info' => $this->unlessState(Variant::Minimal, $this->public_information), +'public_info' => $this->unlessState(State::Minimal, $this->public_information), ``` ### `whenStateIn` @@ -110,7 +110,7 @@ Include a value unless the current state matches the specified state: Include a value when the current state is one of the specified states: ```php -'detailed_info' => $this->whenStateIn([Variant::Full, Variant::Table], [ +'detailed_info' => $this->whenStateIn([State::Full, State::Table], [ 'department' => $this->department, 'position' => $this->position, ]), @@ -121,7 +121,7 @@ Include a value when the current state is one of the specified states: Include a value unless the current state is one of the specified states: ```php -'sensitive_data' => $this->unlessStateIn([Variant::Minimal, Variant::Table], $this->sensitive_info), +'sensitive_data' => $this->unlessStateIn([State::Minimal, State::Table], $this->sensitive_info), ``` ### Magic Conditionals @@ -140,7 +140,7 @@ You can also use magic methods with for cleaner syntax: Use the static `state()` method to create a resource with a specific state: ```php -$user = UserResource::state(Variant::Minimal)->make($user); +$user = UserResource::state(State::Minimal)->make($user); ``` ### Using Magic Methods @@ -148,7 +148,7 @@ $user = UserResource::state(Variant::Minimal)->make($user); You can also use magic methods for a more fluent syntax: ```php -// This is equivalent to the explicit state() call +// This is equivalent to the explicit state(State::Minimal) call $user = UserResource::minimal()->make($user); ``` diff --git a/docs/pages/extending-states.md b/docs/pages/extending-states.md index b5412d7..8c6a152 100644 --- a/docs/pages/extending-states.md +++ b/docs/pages/extending-states.md @@ -1,6 +1,6 @@ # Extending States -You may find yourself being too limited with the three Variant states included in the package's `Variant` enum. +You may find yourself being too limited with the three State states included in the package's `State` enum. This package allows you to register custom states that you can then use in your resources. ## Registering Custom States @@ -9,10 +9,11 @@ Before using a custom state, register it in the package's `stateful-resources.st ```php [ - ...Variant::cases(), // The built-in states + ...State::cases(), // The built-in states 'custom', // Your custom state as a string ...CustomResourceState::cases(), // Or as cases of a custom enum ], diff --git a/src/Enums/Variant.php b/src/Enums/State.php similarity index 80% rename from src/Enums/Variant.php rename to src/Enums/State.php index 2c11ec5..6e0e9a9 100644 --- a/src/Enums/Variant.php +++ b/src/Enums/State.php @@ -4,7 +4,7 @@ use Farbcode\StatefulResources\Contracts\ResourceState; -enum Variant: string implements ResourceState +enum State: string implements ResourceState { case Full = 'full'; case Table = 'table'; diff --git a/tests/Feature/DefaultStatesTest.php b/tests/Feature/DefaultStatesTest.php index 143935f..e02eb31 100644 --- a/tests/Feature/DefaultStatesTest.php +++ b/tests/Feature/DefaultStatesTest.php @@ -1,6 +1,6 @@ make($cat)->toJson(); + $resource = CatResource::state(State::Full)->make($cat)->toJson(); expect($resource)->toBeJson(); @@ -49,7 +49,7 @@ /** @var TestCase $this */ $cat = Cat::firstOrFail(); - $resource = CatResource::state(Variant::Minimal)->make($cat)->toJson(); + $resource = CatResource::state(State::Minimal)->make($cat)->toJson(); expect($resource)->toBeJson(); @@ -63,7 +63,7 @@ /** @var TestCase $this */ $cat = Cat::firstOrFail(); - $resource = CatResource::state(Variant::Table)->make($cat)->toJson(); + $resource = CatResource::state(State::Table)->make($cat)->toJson(); expect($resource)->toBeJson(); diff --git a/tests/TestCase.php b/tests/TestCase.php index f2dc5ef..4140942 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,7 @@ namespace Farbcode\StatefulResources\Tests; -use Farbcode\StatefulResources\Enums\Variant; +use Farbcode\StatefulResources\Enums\State; use Farbcode\StatefulResources\StatefulResourcesServiceProvider; use Illuminate\Foundation\Testing\RefreshDatabase; use Orchestra\Testbench\Concerns\WithWorkbench; @@ -29,7 +29,7 @@ protected function getPackageProviders($app) protected function getEnvironmentSetUp($app) { $app['config']->set('stateful-resources.states', [ - ...Variant::cases(), + ...State::cases(), 'custom', ]); } diff --git a/workbench/app/Http/Resources/CatResource.php b/workbench/app/Http/Resources/CatResource.php index e86240d..92f0e67 100644 --- a/workbench/app/Http/Resources/CatResource.php +++ b/workbench/app/Http/Resources/CatResource.php @@ -2,7 +2,7 @@ namespace Workbench\App\Http\Resources; -use Farbcode\StatefulResources\Enums\Variant; +use Farbcode\StatefulResources\Enums\State; use Farbcode\StatefulResources\StatefulJsonResource; use Illuminate\Http\Request; @@ -18,9 +18,9 @@ public function toArray(Request $request): array return [ 'id' => $this->id, 'name' => $this->name, - 'breed' => $this->whenStateIn([Variant::Full, Variant::Table], $this->breed), - 'fluffyness' => $this->whenStateIn([Variant::Full], $this->fluffyness), - 'color' => $this->whenStateIn([Variant::Full], $this->color), + 'breed' => $this->whenStateIn([State::Full, State::Table], $this->breed), + 'fluffyness' => $this->whenStateIn([State::Full], $this->fluffyness), + 'color' => $this->whenStateIn([State::Full], $this->color), 'custom_field' => $this->whenState('custom', 'custom_value'), ]; }