diff --git a/README.md b/README.md
index ba7b7ed..99a1348 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ This Laravel package aims to store and manage user settings/preferences in a sim
* [Concepts](#concepts)
* [Define your preferences](#define-your-preferences)
* [Create a Preference](#create-a-preference)
+ * [Preference Building](#preference-building)
* [Working with preferences](#working-with-preferences)
* [Examples](#examples)
* [Casting](#casting)
@@ -25,7 +26,6 @@ This Laravel package aims to store and manage user settings/preferences in a sim
* [Available Rules](#available-rules)
* [Custom Rules](#custom-rules)
* [Policies](#policies)
-* [Preference Building](#preference-building)
* [Routing](#routing)
* [Anantomy](#anantomy)
* [Example](#config-example)
@@ -118,15 +118,20 @@ php artisan migrate
### Concepts
-Each preference has at least a name and a caster. For additional validation you can add you custom Rule object
-> [!TIP]
-> The default caster supports all major primitives, enums, objects, as well as time/datetime/date and timestamp which
-> get converted with `Carbon/Carbon`
+Each preference has at least a name and a caster.
+Names are stored in one or more enums and are the unique identifier for that preference
+
+For additional validation you can add you custom Rule object.
+
+For additional security you can add Policies
### Define your preferences
Organize them in one or more **string backed** enum.
+> [!NOTE]
+> while it does not need to be string backed, its way more developer friendly. Especially when interacting over the APi
+
Each enum gets scoped and does not conflict with other enums with the same case
e.g.
@@ -143,7 +148,7 @@ enum Preferences :string implements PreferenceGroup
enum General :string implements PreferenceGroup
{
- case LANGUAGE="language";
+ case LANGUAGE="language";
case THEME="theme";
}
```
@@ -166,7 +171,7 @@ public function up(): void
// Or
PreferenceBuilder::init(Preferences::LANGUAGE)->create()
// different enums with the same value do not conflict
- PreferenceBuilder::init(OtherPreferences::LANGUAGE)->create()
+ PreferenceBuilder::init(General::LANGUAGE)->create()
// update
PreferenceBuilder::init(Preferences::LANGUAGE)
@@ -184,8 +189,6 @@ public function up(): void
->nullable()
->create()
-
-
}
public function down(): void
@@ -197,10 +200,7 @@ public function down(): void
#### Bulk mode
```php
-use Illuminate\Database\Migrations\Migration;
-use Matteoc99\LaravelPreference\Enums\Cast;
-use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;
-use Matteoc99\LaravelPreference\Rules\InRule;
+use Illuminate\Database\Migrations\Migration;use Matteoc99\LaravelPreference\Enums\Cast;use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;use Matteoc99\LaravelPreference\Rules\InRule;
return new class extends Migration {
@@ -233,15 +233,53 @@ return new class extends Migration {
['name' => Preferences::CONFIGURATION,
'nullable' => true // or nullable for only one configuration
],
+ // or an array of initialized single-mode builders
+ PreferenceBuilder::init(Preferences::LANGUAGE)->withRule(new InRule("en", "it", "de")),
+ PreferenceBuilder::init(Preferences::THEME)->withRule(new InRule("light", "dark"))
+ //mixing both in one array is also possible
];
}
};
```
+## Preference Building
+
+
+Check all methods available to build a Preference
+
+### Available Methods
+
+This table includes a complete list of all features available,
+when building a preference.
+
+| Single-Mode | Bulk-Mode (array-keys) | Constrains | Description |
+|-------------------------------------|---------------------------------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| init(>name<,>cast<) | ```["name"=> >name<]``` | \>name< = instanceof PreferenceGroup | Unique identifier for the preference |
+| init(>name<,>cast<) | ```["cast"=> >cast<]``` | \>cast< = instanceof CastableEnum | Caster to translate the value between all different scenarios. Currently: Api-calls as well as saving to and retrieving fron the DB |
+| nullable(>nullable<) | ```["nullable"=> >nullable<]``` | \>nullable< = bool | Whether the default value can be null and if the preference can be set to null |
+| withDefaultValue(>default_value<) | ```["default_value"=> >default_value<]``` | \>default_value< = mixed, but must comply with the cast & validationRule | Initial value for this preference |
+| withDescription(>description<) | ```["description"=> >description<]``` | \>description< = string | Legacy code from v1.x has no actual use as of now |
+| withPolicy(>policy<) | ```["policy"=> >policy<]``` | \>policy< = instanceof PreferencePolicy | Authorize actions such as update/delete etc. on certain preferences. |
+| withRule(>rule<) | ```["rule"=> >rule<]``` | \>rule< = instanceof ValidationRule | Additional validation Rule, to validate values before setting them |
+| setAllowedClasses(>allowed_values<) | ```["allowed_values"=> >allowed_values<]``` | \>allowed_values< = array of string classes. For non Primitive Casts only | Current use-cases:
- restrict classes of enum or object that can be set to this preference
- reconstruct the original class when sending data via api. |
+
+### Available helper functions
+
+Optionally, pass the default value as a second parameter
+
+```php
+ // quickly build a nullable Array preference
+ PreferenceBuilder::buildArray(VideoPreferences::CONFIG);
+
+ PreferenceBuilder::buildString(VideoPreferences::LANGUAGE);
+```
+
+
+
## Working with preferences
-two things are needed:
+Two things are needed:
- `HasPreferences` trait to access the helper functions
- `PreferenceableModel` Interface to have access to the implementation
@@ -249,7 +287,7 @@ two things are needed:
#### isUserAuthorized
-guard function to validate if the currently logged in (if any) user has access to this model
+Guard function to validate if the currently logged in (if any) user has access to this model
Signature:
- $user the logged in user
@@ -299,11 +337,16 @@ class User extends \Illuminate\Foundation\Auth\User implements PreferenceableMod
$user->getPreferences(General::class)
//or all
$user->getPreferences(): Collection of UserPreferences
+
+
+ // removes all preferences set for tht user
+ $user->removeAllPreferences();
+
```
## Casting
-set the cast when creating a Preference
+Set the cast when creating a Preference
> [!NOTE]
> a cast has 3 main jobs
@@ -341,7 +384,7 @@ PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
### Custom Caster
-implement `CastableEnum`
+Implement `CastableEnum`
> [!IMPORTANT]
> The custom caster needs to be a **string backed** enum
@@ -407,19 +450,20 @@ Additional validation, which can be way more complex than provided by the Cast
### Available Rules
-| Rule | Example | Description |
-|----------------|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
+| Rule | Example | Description |
+|----------------|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| AndRule | `new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures all pass |
-| OrRule | `new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures at least one passes |
-| BetweenRule | `new BetweenRule(2.4, 5.5)` | For INT and FLOAT, check that the value is between min and max |
-| InRule | `new InRule("it","en","de")` | Expects the value to be validated to be in that equal to one of the `n` params |
-| InstanceOfRule | `new InstanceOfRule(Theme::class)` | For non primitive casts, checks the instance of the value's class to validate. Tip: goes along well with the `OrRule` |
-| IsRule | `new IsRule(Type::ITERABLE)` | Expects a `Matteoc99\LaravelPreference\Enums\Type` Enum. Checks e.g. if the value is iterable |
-| LowerThanRule | `new LowerThanRule(5)` | For INT and FLOAT, check that the value to be validated is less than the one passed in the constructor |
+| OrRule | `new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures at least one passes |
+| LaravelRule | `new LaravelRule("required\|numeric")` | Expects a string, containing a Laravel Validation Rule |
+| BetweenRule | `new BetweenRule(2.4, 5.5)` | For INT and FLOAT, check that the value is between min and max |
+| InRule | `new InRule("it","en","de")` | Expects the value to be validated to be in that equal to one of the `n` params |
+| InstanceOfRule | `new InstanceOfRule(Theme::class)` | For non primitive casts, checks the instance of the value's class to validate. Tip: goes along well with the `OrRule` |
+| IsRule | `new IsRule(Type::ITERABLE)` | Expects a `Matteoc99\LaravelPreference\Enums\Type` Enum. Checks e.g. if the value is iterable |
+| LowerThanRule | `new LowerThanRule(5)` | For INT and FLOAT, check that the value to be validated is less than the one passed in the constructor |
### Custom Rules
-implement `ValidationRule`
+Implement Laravel's `ValidationRule`
#### Example:
@@ -453,11 +497,11 @@ class MyRule implements ValidationRule
## Policies
-each preference can have a Policy, should [isUserAuthorized](#isuserauthorized) not be enough for your usecase
+Each preference can have a Policy, should [isUserAuthorized](#isuserauthorized) not be enough for your usecase
### Creating policies
-implement `PreferencePolicy` and the 4 methods defined by the contract
+Implement `PreferencePolicy` and the 4 methods defined by the contract
| parameter | description |
|-----------------------------|------------------------------------------------------------|
@@ -480,22 +524,9 @@ implement `PreferencePolicy` and the 4 methods defined by the contract
````
-## Preference Building
-
-| Single-Mode | Bulk-Mode (array-keys) | Constrains | Description |
-|-------------------------------------|---------------------------------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| init(>name<,>cast<) | ```["name"=> >name<]``` | \>name< = instanceof PreferenceGroup | Unique identifier for the preference |
-| init(>name<,>cast<) | ```["cast"=> >cast<]``` | \>cast< = instanceof CastableEnum | Caster to translate the value between all different scenarios. Currently: Api-calls as well as saving to and retrieving fron the DB |
-| nullable(>nullable<) | ```["nullable"=> >nullable<]``` | \>nullable< = bool | Whether the default value can be null and if the preference can be set to null |
-| withDefaultValue(>default_value<) | ```["default_value"=> >default_value<]``` | \>default_value< = mixed, but must comply with the cast & validationRule | Initial value for this preference |
-| withDescription(>description<) | ```["description"=> >description<]``` | \>description< = string | Legacy code from v1.x has no actual use as of now |
-| withPolicy(>policy<) | ```["policy"=> >policy<]``` | \>policy< = instanceof PreferencePolicy | Authorize actions such as update/delete etc. on certain preferences. |
-| withRule(>rule<) | ```["rule"=> >rule<]``` | \>rule< = instanceof ValidationRule | Additional validation Rule, to validate values before setting them |
-| setAllowedClasses(>allowed_values<) | ```["allowed_values"=> >allowed_values<]``` | \>allowed_values< = array of string classes. For non Primitive Casts only | Current use-cases:
- restrict classes of enum or object that can be set to this preference
- reconstruct the original class when sending data via api. |
-
## Routing
-off by default, enable it in the config
+Off by default, enable it in the config
> [!WARNING]
> **(Current) limitation**: it's not possible to set object casts via API
@@ -505,7 +536,7 @@ off by default, enable it in the config
'Scope': the `PreferenceableModel` Model
'Group': the `PreferenceGroup` enum
-routes then get transformed to:
+Routes then get transformed to:
| Action | URI | Description |
|-----------|---------------------------------------------------|-------------------------------------------------------------|
diff --git a/src/Contracts/PreferenceableModel.php b/src/Contracts/PreferenceableModel.php
index b02a3ed..c06cebc 100644
--- a/src/Contracts/PreferenceableModel.php
+++ b/src/Contracts/PreferenceableModel.php
@@ -31,6 +31,17 @@ public function getPreferences(string $group = null): Collection;
*/
public function removePreference(PreferenceGroup $name): int;
+
+ /**
+ * Reset the model to its original state
+ * Remove all user's preferences.
+ *
+ *
+ * @return int Number of deleted records.
+ * @throws AuthorizationException
+ */
+ public function removeAllPreferences(): int;
+
/**
* Set a preference value, handling validation and persistence.
*
@@ -54,7 +65,7 @@ public function setPreference(PreferenceGroup $name, mixed $value): void;
public function getPreference(PreferenceGroup $name, mixed $default = null): mixed;
/**
- * Get a user's preference value or default if not set with no casting
+ * Get a user's preference value or default if not set, transformed for data transfer
*
* @param PreferenceGroup|Preference $preference
* @param string|null $default Default value if preference not set.
diff --git a/src/Enums/PolicyAction.php b/src/Enums/PolicyAction.php
index ffa2fc7..8c06964 100644
--- a/src/Enums/PolicyAction.php
+++ b/src/Enums/PolicyAction.php
@@ -4,6 +4,7 @@
enum PolicyAction
{
+ case DELETE_ALL;
case INDEX;
case GET;
case UPDATE;
diff --git a/src/Exceptions/InvalidStateException.php b/src/Exceptions/InvalidStateException.php
new file mode 100644
index 0000000..dbbc707
--- /dev/null
+++ b/src/Exceptions/InvalidStateException.php
@@ -0,0 +1,26 @@
+state = $state;
+ parent::__construct($message, 0, $previous);
+ }
+
+ /**
+ * @return int
+ */
+ public function getState(): int
+ {
+ return $this->state;
+ }
+}
\ No newline at end of file
diff --git a/src/Factory/PreferenceBuilder.php b/src/Factory/PreferenceBuilder.php
index 66d3927..30c4f63 100644
--- a/src/Factory/PreferenceBuilder.php
+++ b/src/Factory/PreferenceBuilder.php
@@ -9,6 +9,8 @@
use Matteoc99\LaravelPreference\Contracts\CastableEnum;
use Matteoc99\LaravelPreference\Contracts\PreferenceGroup;
use Matteoc99\LaravelPreference\Enums\Cast;
+use Matteoc99\LaravelPreference\Exceptions\InvalidStateException;
+use Matteoc99\LaravelPreference\Factory\builders\BaseBuilder;
use Matteoc99\LaravelPreference\Factory\builders\ObjectPreferenceBuilder;
use Matteoc99\LaravelPreference\Factory\builders\PrimitivePreferenceBuilder;
use Matteoc99\LaravelPreference\Models\Preference;
@@ -28,6 +30,7 @@ class PreferenceBuilder
*
* @return void Creates the preference and commits it to the database.
*
+ * @throws InvalidStateException
* @example
* ```
* PreferenceBuilder::buildString(UserPreference::TEST, "default");
@@ -36,7 +39,7 @@ class PreferenceBuilder
*/
public static function buildString(PreferenceGroup $name, string $default = null): void
{
- self::init($name)->nullable()->withDefaultValue(null)->create();
+ self::init($name)->nullable()->withDefaultValue($default)->create();
}
/**
@@ -49,6 +52,7 @@ public static function buildString(PreferenceGroup $name, string $default = null
*
* @return void Creates the preference and commits it to the database.
*
+ * @throws InvalidStateException
* @example
* ```
* PreferenceBuilder::buildArray(new SettingsPreferenceGroup(), ["item1", "item2"]);
@@ -58,7 +62,7 @@ public static function buildString(PreferenceGroup $name, string $default = null
*/
public static function buildArray(PreferenceGroup $name, array $default = null): void
{
- self::init($name, Cast::ARRAY)->nullable()->withDefaultValue(null)->create();
+ self::init($name, Cast::ARRAY)->nullable()->withDefaultValue($default)->create();
}
@@ -99,7 +103,7 @@ public static function init(PreferenceGroup $name, CastableEnum $cast = Cast::ST
*
* @param PreferenceGroup $name
*
- * @return int,
+ * @return int
*/
public static function delete(PreferenceGroup $name): int
{
@@ -148,7 +152,23 @@ public static function initBulk(array $preferences, bool $nullable = false): voi
throw new InvalidArgumentException("no preferences provided");
}
- foreach ($preferences as $index => &$preferenceData) {
+ $cleanPreferences = [];
+
+ foreach ($preferences as $index => $preferenceData) {
+
+ if ($preferenceData instanceof BaseBuilder) {
+ if ($preferenceData->isStateSet(BaseBuilder::STATE_CREATED)) {
+ throw new InvalidStateException($preferenceData->getState()
+ , "The State should not be Created at this point, as its initBulk responsibility");
+ }
+ if (!$preferenceData->isStateSet(BaseBuilder::STATE_NULLABLE_SET)) {
+ $preferenceData->nullable($nullable);
+ }
+
+ $preferenceData->updateOrCreate();
+ continue;
+ }
+
if (empty($preferenceData['cast'])) {
$preferenceData['cast'] = Cast::STRING;
}
@@ -177,7 +197,7 @@ public static function initBulk(array $preferences, bool $nullable = false): voi
$preferenceData['cast'] = serialize($preferenceData['cast']);
// Ensure Defaults
- $preferenceData = array_merge([
+ $preferenceData = array_merge([
'group' => 'general',
'default_value' => null,
'allowed_values' => null,
@@ -186,9 +206,10 @@ public static function initBulk(array $preferences, bool $nullable = false): voi
'rule' => null,
'nullable' => false,
], $preferenceData);
+ $cleanPreferences[] = $preferenceData;
}
- Preference::upsert($preferences, ['name', 'group']);
+ Preference::upsert($cleanPreferences, ['name', 'group']);
}
/**
@@ -203,6 +224,7 @@ public static function initBulk(array $preferences, bool $nullable = false): voi
* @return int Returns the number of deleted preferences.
*
* @throws InvalidArgumentException if the preferences array is empty or if any preference lacks a required
+ * @throws InvalidStateException
* 'name' field, or if the 'name' field does not implement PreferenceGroup.
*/
public static function deleteBulk(array $preferences): int
@@ -213,6 +235,18 @@ public static function deleteBulk(array $preferences): int
$query = Preference::query();
foreach ($preferences as $index => $preferenceData) {
+
+
+ if ($preferenceData instanceof BaseBuilder) {
+ if ($preferenceData->isStateSet(BaseBuilder::STATE_DELETED)) {
+ throw new InvalidStateException($preferenceData->getState()
+ , "The State should not be Deleted at this point, as its deleteBulk's responsibility");
+ }
+
+ $preferenceData->delete();
+ continue;
+ }
+
if (empty($preferenceData['name']) || !($preferenceData['name'] instanceof PreferenceGroup)) {
throw new InvalidArgumentException(
sprintf("index: #%s name is required and must implement PreferenceGroup", $index)
diff --git a/src/Factory/builders/BaseBuilder.php b/src/Factory/builders/BaseBuilder.php
index 1c6e3cb..7b28363 100644
--- a/src/Factory/builders/BaseBuilder.php
+++ b/src/Factory/builders/BaseBuilder.php
@@ -3,32 +3,63 @@
namespace Matteoc99\LaravelPreference\Factory\builders;
use Illuminate\Contracts\Validation\ValidationRule;
+use Illuminate\Validation\ValidationException;
use Matteoc99\LaravelPreference\Contracts\CastableEnum;
use Matteoc99\LaravelPreference\Contracts\PreferenceGroup;
use Matteoc99\LaravelPreference\Contracts\PreferencePolicy;
+use Matteoc99\LaravelPreference\Exceptions\InvalidStateException;
use Matteoc99\LaravelPreference\Models\Preference;
+use Matteoc99\LaravelPreference\Traits\HasState;
use Matteoc99\LaravelPreference\Utils\SerializeHelper;
use Matteoc99\LaravelPreference\Utils\ValidationHelper;
abstract class BaseBuilder
{
+ use HasState;
+
protected Preference $preference;
+ const STATE_INITIALIZED = 1;
+ const STATE_CREATED = 2;
+ const STATE_DELETED = 4;
+ const STATE_NAME_SET = 8;
+ const STATE_CAST_SET = 16;
+ const STATE_POLICY_SET = 32;
+ const STATE_RULE_SET = 64;
+ const STATE_DEFAULT_SET = 128;
+ const STATE_DESCRIPTION_SET = 256;
+ const STATE_NULLABLE_SET = 512;
+ const STATE_ALLOWED_VALUES_SET = 1024;
+
+ /**
+ * @throws InvalidStateException
+ */
public function __construct(PreferenceGroup $name, CastableEnum $cast)
{
$this->preference = new Preference();
- $this->withName($name)->withCast($cast)->nullable(false);
+ $this->addState(self::STATE_INITIALIZED);
+
+ $this->withName($name)->withCast($cast);
}
+ /**
+ * @throws InvalidStateException
+ */
private function withCast(CastableEnum $cast): static
{
+ $this->addState(self::STATE_CAST_SET);
$this->preference->cast = $cast;
return $this;
}
+ /**
+ * @throws InvalidStateException
+ */
private function withName(PreferenceGroup $name): static
{
+ $this->addState(self::STATE_NAME_SET);
+
SerializeHelper::conformNameAndGroup($name, $group);
$this->preference->name = $name;
@@ -37,44 +68,100 @@ private function withName(PreferenceGroup $name): static
}
+ /**
+ * @throws InvalidStateException
+ */
public function withPolicy(PreferencePolicy $policy): static
{
+ $this->addState(self::STATE_POLICY_SET);
$this->preference->policy = $policy;
return $this;
}
+ /**
+ * @throws InvalidStateException
+ */
public function withDefaultValue(mixed $value): static
{
+ $this->addState(self::STATE_DEFAULT_SET);
+
$this->preference->default_value = $value;
return $this;
}
+ /**
+ * @throws InvalidStateException
+ */
public function withDescription(string $description): static
{
+ $this->addState(self::STATE_DESCRIPTION_SET);
+
$this->preference->description = $description;
return $this;
}
+ /**
+ * @throws InvalidStateException
+ */
public function withRule(ValidationRule $rule): static
{
+ $this->addState(self::STATE_RULE_SET);
+
$this->preference->rule = $rule;
return $this;
}
+ /**
+ * @throws InvalidStateException
+ */
public function nullable(bool $nullable = true)
{
+ $this->addState(self::STATE_NULLABLE_SET);
+
$this->preference->nullable = $nullable;
return $this;
}
+ /**
+ * @deprecated no reason to use this over updateOrCreate, will be removed in v3.x
+ */
public function create(): Preference
{
return $this->updateOrCreate();
+
+ }
+
+ /**
+ * @throws InvalidStateException
+ */
+ public function delete(): int
+ {
+
+ if (!$this->isStateSet(self::STATE_INITIALIZED)
+ || !$this->isStateSet(self::STATE_NAME_SET)) {
+ throw new InvalidStateException($this->getState(), "Initialize the builder before deleting the preference");
+ }
+
+ $this->addState(self::STATE_DELETED);
+
+
+
+ return Preference::query()
+ ->where('group', $this->preference->group)
+ ->where('name', $this->preference->name)
+ ->delete();
+
}
+ /**
+ * @throws InvalidStateException
+ * @throws ValidationException
+ */
public function updateOrCreate(): Preference
{
+ $this->addState(self::STATE_CREATED);
+
ValidationHelper::validatePreference($this->preference);
diff --git a/src/Factory/builders/ObjectPreferenceBuilder.php b/src/Factory/builders/ObjectPreferenceBuilder.php
index 96f873e..0fff541 100644
--- a/src/Factory/builders/ObjectPreferenceBuilder.php
+++ b/src/Factory/builders/ObjectPreferenceBuilder.php
@@ -2,14 +2,20 @@
namespace Matteoc99\LaravelPreference\Factory\builders;
+use Matteoc99\LaravelPreference\Exceptions\InvalidStateException;
use Matteoc99\LaravelPreference\Utils\ValidationHelper;
class ObjectPreferenceBuilder extends BaseBuilder
{
+ /**
+ * @throws InvalidStateException
+ */
public function setAllowedClasses(...$classes): static
{
+ $this->addState(self::STATE_ALLOWED_VALUES_SET);
+
ValidationHelper::validateAllowedClasses($this->preference->cast, $classes);
diff --git a/src/Models/Preference.php b/src/Models/Preference.php
index 37b7bf1..5319cf7 100644
--- a/src/Models/Preference.php
+++ b/src/Models/Preference.php
@@ -44,6 +44,10 @@ class Preference extends BaseModel
'allowed_values',
];
+ protected $attributes = [
+ 'nullable' => false,
+ ];
+
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
diff --git a/src/Rules/LaravelRule.php b/src/Rules/LaravelRule.php
new file mode 100644
index 0000000..a91ee7c
--- /dev/null
+++ b/src/Rules/LaravelRule.php
@@ -0,0 +1,26 @@
+ $value], [$attribute => $this->rule]);
+
+ if ($validator->fails()) {
+ $fail($validator->messages());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Traits/HasPreferences.php b/src/Traits/HasPreferences.php
index 00a886c..f2759a4 100644
--- a/src/Traits/HasPreferences.php
+++ b/src/Traits/HasPreferences.php
@@ -120,6 +120,22 @@ public function removePreference(PreferenceGroup|Preference $preference): int
return $this->userPreferences()->where('preference_id', $preference->id)->delete();
}
+ /**
+ * Reset the model to its original state
+ * Remove all user's preferences.
+ *
+ *
+ * @return int Number of deleted records.
+ * @throws AuthorizationException
+ */
+ public function removeAllPreferences(): int
+ {
+ $this->authorize(PolicyAction::DELETE_ALL);
+
+ return $this->userPreferences()->delete();
+ }
+
+
/**
* Get all preferences for a user, optionally filtered by group.
*
@@ -141,6 +157,7 @@ public function getPreferences(string $group = null): Collection
return $query->get();
}
+
/**
* Validate existence of a preference and retrieve it.
*
diff --git a/src/Traits/HasState.php b/src/Traits/HasState.php
new file mode 100644
index 0000000..b146ce7
--- /dev/null
+++ b/src/Traits/HasState.php
@@ -0,0 +1,62 @@
+isStateSet($state)) {
+ throw new InvalidStateException($this->getState(), "The model is already in the state: " . $this->getStateName($state));
+ }
+
+ $this->state |= $state;
+ }
+
+ public function isStateSet($state): bool
+ {
+ return ($this->state & $state) === $state;
+ }
+
+ /**
+ * @throws InvalidStateException
+ */
+ protected function removeState($state): void
+ {
+ if (!$this->isStateSet($state)) {
+ throw new InvalidStateException($this->getState(), "The model is not in the state: " . $this->getStateName($state) . " and can not be removed. ");
+ }
+ $this->state &= ~$state;
+ }
+
+ protected function getStateName($state): string
+ {
+ $reflection = new ReflectionClass($this);
+ $constants = $reflection->getConstants();
+
+ foreach ($constants as $name => $value) {
+ if ($state === $value) {
+ return $name;
+ }
+ }
+
+ return 'Unknown State';
+ }
+
+ /**
+ * @return int
+ */
+ public function getState(): int
+ {
+ return $this->state;
+ }
+
+}
\ No newline at end of file
diff --git a/tests/BuilderTest/BuilderStateTest.php b/tests/BuilderTest/BuilderStateTest.php
new file mode 100644
index 0000000..5d5e363
--- /dev/null
+++ b/tests/BuilderTest/BuilderStateTest.php
@@ -0,0 +1,103 @@
+assertTrue($builder->isStateSet(BaseBuilder::STATE_INITIALIZED));
+
+ $this->assertTrue($builder->isStateSet(BaseBuilder::STATE_NAME_SET));
+ $this->assertTrue($builder->isStateSet(BaseBuilder::STATE_CAST_SET));
+ }
+
+ /** @test */
+ public function it_throws_exception_when_adding_same_state_twice()
+ {
+ $this->expectException(InvalidStateException::class);
+ try {
+
+ PreferenceBuilder::init(General::LANGUAGE)->withDefaultValue("de")->withDefaultValue("en");
+ } catch (InvalidStateException $e) {
+ $this->assertEquals(BaseBuilder::STATE_INITIALIZED
+ + BaseBuilder::STATE_NAME_SET
+ + BaseBuilder::STATE_CAST_SET
+ + BaseBuilder::STATE_DEFAULT_SET, $e->getState());
+ throw $e;
+ }
+ }
+
+
+ /** @test */
+ public function it_properly_adds_name_and_cast_state()
+ {
+ $builder = PreferenceBuilder::init(General::LANGUAGE);
+ $builder->withDefaultValue("en");
+ $builder->withRule(new InRule("en", "it", "de"));
+
+ $this->assertTrue($builder->isStateSet(BaseBuilder::STATE_DEFAULT_SET));
+ $this->assertTrue($builder->isStateSet(BaseBuilder::STATE_RULE_SET));
+ }
+
+ /** @test */
+ public function it_handles_nullable_state_correctly()
+ {
+ $builder = PreferenceBuilder::init(General::LANGUAGE);
+ $builder->nullable();
+
+ $this->assertTrue($builder->isStateSet(BaseBuilder::STATE_NULLABLE_SET));
+ }
+
+ /** @test */
+ public function it_allows_adding_description_after_default_value()
+ {
+ $builder = PreferenceBuilder::init(General::LANGUAGE);
+ $builder->withDefaultValue("en")->withDescription("Default language setting");
+
+ $this->assertTrue($builder->isStateSet(BaseBuilder::STATE_DESCRIPTION_SET));
+ }
+
+ /** @test */
+ public function it_creates_preference_properly_with_all_required_states()
+ {
+ $builder = PreferenceBuilder::init(General::LANGUAGE)
+ ->withDefaultValue("en")
+ ->withRule(new InRule("en", "it", "de"))
+ ->withDescription("Language settings");
+
+ $preference = $builder->updateOrCreate();
+ $this->assertInstanceOf(Preference::class, $preference);
+ }
+
+ /** @test */
+ public function init_bulk_successfully_creates_preferences_with_mixed_input_types()
+ {
+ $preferences = [
+ ['name' => General::CONFIG, 'cast' => Cast::ARRAY],
+ PreferenceBuilder::init(General::LANGUAGE)->withRule(new InRule("en", "it", "de")),
+ PreferenceBuilder::init(General::THEME)->withRule(new InRule("light", "dark")),
+ ];
+
+ PreferenceBuilder::initBulk($preferences);
+
+ $this->assertDatabaseCount((new Preference())->getTable(), 3);
+ $this->assertDatabaseHas((new Preference())->getTable(), ['name' => General::LANGUAGE]);
+
+ PreferenceBuilder::deleteBulk($preferences);
+ $this->assertDatabaseCount((new Preference())->getTable(), 0);
+
+ }
+}
\ No newline at end of file
diff --git a/tests/PreferenceBuilderBulkTest.php b/tests/BuilderTest/PreferenceBuilderBulkTest.php
similarity index 95%
rename from tests/PreferenceBuilderBulkTest.php
rename to tests/BuilderTest/PreferenceBuilderBulkTest.php
index c4361ac..dd4f4c9 100644
--- a/tests/PreferenceBuilderBulkTest.php
+++ b/tests/BuilderTest/PreferenceBuilderBulkTest.php
@@ -1,12 +1,13 @@
assertDatabaseCount((new Preference())->getTable(), 1);
- $this->assertDatabaseHas((new Preference())->getTable(), ['name' => VideoPreferences::LANGUAGE]);
+ $this->assertDatabaseHas((new Preference())->getTable(), ['name' => VideoPreferences::LANGUAGE]);
}
/** @test */
@@ -127,13 +128,14 @@ public function init_bulk_handles_mixed_valid_and_invalid_preferences()
$this->expectException(\InvalidArgumentException::class);
PreferenceBuilder::initBulk($preferences);
}
+
/** @test */
public function init_bulk_handles_deprecated_group()
{
$preferences = [
- ['name' => General::LANGUAGE, 'cast' => Cast::STRING,'group'=>"hi"],
- ];
+ ['name' => General::LANGUAGE, 'cast' => Cast::STRING, 'group' => "hi"],
+ ];
$this->expectException(\InvalidArgumentException::class);
PreferenceBuilder::initBulk($preferences);
@@ -150,7 +152,7 @@ public function init_bulk_with_all_options()
PreferenceBuilder::initBulk($preferences);
$this->assertDatabaseCount((new Preference())->getTable(), 2);
- $this->assertEquals(true,$this->testUser->getPreference(General::LANGUAGE));
+ $this->assertEquals(true, $this->testUser->getPreference(General::LANGUAGE));
PreferenceBuilder::deleteBulk($preferences);
$this->assertDatabaseCount((new Preference())->getTable(), 0);
diff --git a/tests/PreferenceBasicTest.php b/tests/PreferenceBasicTest.php
index d54d408..f528407 100644
--- a/tests/PreferenceBasicTest.php
+++ b/tests/PreferenceBasicTest.php
@@ -7,6 +7,7 @@
use Matteoc99\LaravelPreference\Models\Preference;
use Matteoc99\LaravelPreference\Rules\InRule;
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
+use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\NumericPreferences;
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\OtherPreferences;
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences;
@@ -113,5 +114,28 @@ public function init_and_delete()
}
+ /** @test */
+ public function test_numeric_backed_preferences()
+ {
+ PreferenceBuilder::buildArray(NumericPreferences::ONE);
+
+ PreferenceBuilder::buildString(NumericPreferences::TWO);
+
+ PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
+ PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
+ PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
+ PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
+ PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
+
+ $this->assertDatabaseCount((new Preference())->getTable(), 3);
+
+ $this->testUser->setPreference(NumericPreferences::TWO, "14");
+ $this->testUser->setPreference(NumericPreferences::ONE, ["test" => "value"]);
+
+ $this->assertEquals('14', $this->testUser->getPreference(NumericPreferences::TWO));
+
+ $this->assertEquals(["test" => "value"], $this->testUser->getPreference(NumericPreferences::ONE));
+ }
+
}
\ No newline at end of file
diff --git a/tests/RulesTest/CombinedRulesTest.php b/tests/RulesTest/CombinedRulesTest.php
index 34f219f..4e22714 100644
--- a/tests/RulesTest/CombinedRulesTest.php
+++ b/tests/RulesTest/CombinedRulesTest.php
@@ -8,6 +8,7 @@
use Matteoc99\LaravelPreference\Rules\InRule;
use Matteoc99\LaravelPreference\Rules\InstanceOfRule;
use Matteoc99\LaravelPreference\Rules\IsRule;
+use Matteoc99\LaravelPreference\Rules\LaravelRule;
use Matteoc99\LaravelPreference\Rules\LowerThanRule;
use Matteoc99\LaravelPreference\Rules\OrRule;
use Matteoc99\LaravelPreference\Tests\TestCase;
@@ -43,7 +44,19 @@ public static function andRuleProvider(): array
];
}
+ public static function laravelRuleProvider(): array
+ {
+ return [
+ [new LaravelRule('required|email'), 'test@example.com', true, 'Expected LaravelRule to pass for valid email.'],
+ [new LaravelRule('required|numeric'), '123', true, 'Expected LaravelRule to pass for numeric value.'],
+ [new LaravelRule('required|numeric'), 'abc', false, 'Expected LaravelRule to fail for non-numeric value.'],
+ [new LaravelRule('required|in:foo,bar,baz'), 'foo', true, 'Expected LaravelRule to pass for value within given options.'],
+ [new LaravelRule('required|in:foo,bar,baz'), 'qux', false, 'Expected LaravelRule to fail for value not within given options.'],
+ ];
+ }
+
/**
+ * @dataProvider laravelRuleProvider
* @dataProvider orRuleProvider
* @dataProvider andRuleProvider
*/
diff --git a/tests/TestSubjects/Enums/NumericPreferences.php b/tests/TestSubjects/Enums/NumericPreferences.php
new file mode 100644
index 0000000..a32c01b
--- /dev/null
+++ b/tests/TestSubjects/Enums/NumericPreferences.php
@@ -0,0 +1,12 @@
+id == $this->id;
}
-
}
\ No newline at end of file