Skip to content

Commit 2c385a1

Browse files
committed
feat: nullable preferences
1 parent 46f7385 commit 2c385a1

12 files changed

+252
-37
lines changed

README.md

+20-14
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,17 @@ This Laravel package aims to store and manage user settings/preferences in a sim
3636

3737
## Features
3838

39-
- Type safe Casting,
40-
- for example, Cast::BACKED_ENUM expects and always returns an instantiated enum, same goes for all other casts
41-
- Validation
42-
- basic validation from casting and optionally additional rules
43-
- Extensible
44-
- (Create your own Validation Rules and Casts)
39+
- Type safe Casting
40+
- Validation & Authorization
41+
- Extensible (Create your own Validation Rules and Casts)
4542
- Enum support
46-
- store alle your preferences in one or more enums, to simplify the usage of this package
47-
- Can be used on any number of models
48-
- Api routes
43+
- Custom Api routes
4944
- work with preferences from a GUI or in addition to backend functionalities
50-
- Authorization checks
5145

5246
### Roadmap
5347

5448
- Additional inbuilt Custom Rules -> v2.x
55-
- Model / Object Casting -> v2.x
49+
- Policies: ~2.0.0-beta.5
5650
- Suggestions are welcome
5751

5852
## Installation
@@ -161,8 +155,15 @@ public function up(): void
161155

162156
// or with casting
163157
PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
164-
->withDefaultValue(Language::EN)
165-
->create()
158+
->withDefaultValue(Language::EN)
159+
->create()
160+
161+
// nullable support
162+
PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
163+
->withDefaultValue(null)
164+
->nullable()
165+
->create()
166+
166167

167168

168169
}
@@ -187,7 +188,9 @@ return new class extends Migration {
187188
public function up(): void
188189
{
189190

190-
PreferenceBuilder::initBulk($this->preferences());
191+
PreferenceBuilder::initBulk($this->preferences(),
192+
true/false // nullable for the whole Bulk
193+
);
191194
}
192195

193196
/**
@@ -207,6 +210,9 @@ return new class extends Migration {
207210
['name' => Preferences::LANGUAGE, 'cast' => Cast::STRING, 'default_value' => 'en', 'rule' => new InRule("en", "it", "de")],
208211
['name' => Preferences::THEME, 'cast' => Cast::STRING, 'default_value' => 'light'],
209212
['name' => Preferences::CONFIGURATION, 'cast' => Cast::ARRAY],
213+
['name' => Preferences::CONFIGURATION,
214+
'nullable' => true // or nullable for only one configuration
215+
],
210216
];
211217
}
212218
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
use Matteoc99\LaravelPreference\Models\Preference;
7+
8+
return new class extends Migration {
9+
10+
public function up()
11+
{
12+
$preferenceTable = (new Preference())->getTable();
13+
14+
Schema::table($preferenceTable, function (Blueprint $table) {
15+
$table->boolean('nullable')->default(false);
16+
});
17+
}
18+
19+
public function down()
20+
{
21+
$preferenceTable = (new Preference())->getTable();
22+
23+
Schema::table($preferenceTable, function (Blueprint $table) {
24+
$table->dropColumn('nullable');
25+
});
26+
}
27+
};

src/Enums/Cast.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,7 @@ private function ensureCarbon(mixed $value): Carbon
119119
try {
120120
$value = Carbon::parse($value);
121121
} catch (Exception $_) {
122-
throw ValidationException::withMessages([
123-
"Invalid format for cast to " . $this->name
124-
]);
122+
throw ValidationException::withMessages(["Invalid format for cast to " . $this->name]);
125123
}
126124
}
127125
return $value;

src/Factory/PreferenceBuilder.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private function __construct()
2727
public static function init(PreferenceGroup $name, CastableEnum $cast = Cast::STRING): static
2828
{
2929
$builder = new PreferenceBuilder();
30-
return $builder->withName($name)->withCast($cast);
30+
return $builder->withName($name)->withCast($cast)->nullable(false);
3131
}
3232

3333
public static function delete(PreferenceGroup $name): int
@@ -82,6 +82,13 @@ public function withRule(ValidationRule $rule): static
8282
return $this;
8383
}
8484

85+
public function nullable(bool $nullable = true)
86+
{
87+
$this->preference->nullable = $nullable;
88+
return $this;
89+
}
90+
91+
8592
public function create(): Preference
8693
{
8794
return $this->updateOrCreate();
@@ -103,7 +110,7 @@ public function updateOrCreate(): Preference
103110
/**
104111
* @throws ValidationException
105112
*/
106-
public static function initBulk(array $preferences): void
113+
public static function initBulk(array $preferences, bool $nullable = false): void
107114
{
108115
if (empty($preferences)) {
109116
throw new InvalidArgumentException("no preferences provided");
@@ -113,6 +120,10 @@ public static function initBulk(array $preferences): void
113120
if (empty($preferenceData['cast'])) {
114121
$preferenceData['cast'] = Cast::STRING;
115122
}
123+
if (!array_key_exists('nullable', $preferenceData)) {
124+
$preferenceData['nullable'] = $nullable;
125+
}
126+
116127
if (empty($preferenceData['name']) || !($preferenceData['name'] instanceof PreferenceGroup)) {
117128
throw new InvalidArgumentException(
118129
sprintf("index: #%s name is required and needs to be a PreferenceGroup", $key)
@@ -130,7 +141,12 @@ public static function initBulk(array $preferences): void
130141
}
131142

132143
if (!empty($preferenceData['default_value'])) {
133-
ValidationHelper::validateValue($preferenceData['default_value'], $preferenceData['cast'], $preferenceData['rule']);
144+
ValidationHelper::validateValue(
145+
$preferenceData['default_value'],
146+
$preferenceData['cast'] ?? null,
147+
$preferenceData['rule'] ?? null,
148+
$preferenceData['nullable'],
149+
);
134150
}
135151

136152

@@ -159,6 +175,7 @@ public static function initBulk(array $preferences): void
159175
'default_value' => null,
160176
'description' => '',
161177
'rule' => null,
178+
'nullable' => false,
162179
], $preferenceData);
163180
}
164181

src/Models/Preference.php

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* @property ValidationRule|null $rule
2323
* @property PreferencePolicy|null $policy
2424
* @property mixed $default_value
25+
* @property boolean $nullable
2526
* @property Carbon $created_at
2627
* @property Carbon $updated_at
2728
*/
@@ -36,6 +37,7 @@ class Preference extends BaseModel
3637
'description',
3738
'cast',
3839
'policy',
40+
'nullable',
3941
'rule',
4042
'default_value',
4143
];
@@ -47,6 +49,7 @@ class Preference extends BaseModel
4749
'rule' => SerializingCaster::class,
4850
'policy' => SerializingCaster::class,
4951
'default_value' => ValueCaster::class,
52+
'nullable' => 'boolean',
5053
];
5154

5255
}

src/Rules/InstanceOfRule.php

+9-3
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ public function message(): string
1919

2020
public function validate(string $attribute, mixed $value, Closure $fail): void
2121
{
22-
if (!is_string($value)) $value = $value::class;
23-
if (!class_exists($value)) {
22+
if (!is_object($value) && !is_string($value)) {
2423
$fail($this->message());
2524
return;
2625
}
2726

28-
if (!in_array($this->instance, class_implements($value))) {
27+
$className = is_object($value) ? get_class($value) : $value;
28+
29+
if (!class_exists($className)) {
30+
$fail($this->message());
31+
return;
32+
}
33+
34+
if (!in_array($this->instance, class_implements($className))) {
2935
$fail($this->message());
3036
}
3137
}

src/Traits/HasPreferences.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Illuminate\Database\Eloquent\Relations\MorphMany;
77
use Illuminate\Support\Collection;
88
use Illuminate\Support\Facades\Auth;
9-
use Illuminate\Support\Facades\Validator;
109
use Illuminate\Validation\ValidationException;
1110
use Matteoc99\LaravelPreference\Contracts\PreferenceGroup;
1211
use Matteoc99\LaravelPreference\Enums\PolicyAction;
@@ -47,6 +46,10 @@ public function getPreference(PreferenceGroup|Preference $preference, mixed $def
4746

4847
$userPreference = $this->userPreferences()->where('preference_id', $preference->id)->first();
4948

49+
if (!empty($userPreference) && $preference->nullable) {
50+
return $userPreference->value;
51+
}
52+
5053
return $userPreference?->value ?? $preference->default_value ?? $default;
5154
}
5255

@@ -66,7 +69,12 @@ public function setPreference(PreferenceGroup|Preference $preference, mixed $val
6669

6770
$preference = $this->validateAndRetrievePreference($preference);
6871

69-
ValidationHelper::validateValue($value, $preference->cast, $preference->rule);
72+
ValidationHelper::validateValue(
73+
$value,
74+
$preference->cast,
75+
$preference->rule,
76+
$preference->nullable
77+
);
7078

7179
$this->userPreferences()->updateOrCreate(['preference_id' => $preference->id], ['value' => $value]);
7280
}

src/Utils/ValidationHelper.php

+8-4
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ class ValidationHelper
1818
*
1919
* @throws ValidationException
2020
*/
21-
public static function validateValue(mixed $value, ?CastableEnum $cast, ?ValidationRule $rule): void
21+
public static function validateValue(mixed $value, ?CastableEnum $cast, ?ValidationRule $rule, bool $nullable = false): void
2222
{
23-
$validator = Validator::make(['value' => $value], ['value' => self::getValidationRules($cast, $rule)]);
23+
$validator = Validator::make(['value' => $value], ['value' => self::getValidationRules($cast, $rule, $nullable)]);
2424

2525
if ($validator->fails()) {
2626
throw new ValidationException($validator);
@@ -34,12 +34,16 @@ public static function validateValue(mixed $value, ?CastableEnum $cast, ?Validat
3434
*/
3535
public static function validatePreference(Preference $preference)
3636
{
37-
self::validateValue($preference->default_value, $preference->cast, $preference->rule);
37+
self::validateValue($preference->default_value, $preference->cast, $preference->rule, $preference->nullable);
3838
}
3939

40-
private static function getValidationRules(?CastableEnum $cast, ?ValidationRule $rule): array
40+
private static function getValidationRules(?CastableEnum $cast, ?ValidationRule $rule, bool $nullable = false): array
4141
{
4242
$rules = [];
43+
if ($nullable) {
44+
$rules[] = "nullable";
45+
}
46+
4347
if ($cast) {
4448
$castValidation = $cast->validation();
4549
if ($castValidation) {

tests/ApiTest/WorkflowTest.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ public function test_workflow()
3030
/** @test */
3131
public function test_int_workflow()
3232
{
33-
PreferenceBuilder::init(VideoPreferences::QUALITY, Cast::INT)->withDefaultValue(2)->withRule(new LowerThanRule(5))->create();
33+
PreferenceBuilder::init(VideoPreferences::QUALITY, Cast::INT)
34+
->withDefaultValue(2)
35+
->withRule(new LowerThanRule(5))
36+
->create();
3437

3538
$video = $this->get(route('preferences.user.video.get', ['scope_id' => 1, 'preference' => 'quality']));
3639
$video->assertSuccessful();

tests/CastsTest/ValueCasterTest.php

+19-7
Original file line numberDiff line numberDiff line change
@@ -131,23 +131,35 @@ public function test_set()
131131
public static function castProvider(): array
132132
{
133133
return [
134-
'Bool False' => [
134+
'bool_false' => [
135135
Cast::BOOL, false, false, 'assertFalse'
136136
],
137-
'Int' => [
137+
'int_string' => [
138138
Cast::INT, '123', 123, 'assertEquals'
139139
],
140-
'Float' => [
140+
'int' => [
141+
Cast::INT, 2, 2, 'assertEquals'
142+
],
143+
'int_null' => [
144+
Cast::INT, null, null, 'assertEquals'
145+
],
146+
'float' => [
141147
Cast::FLOAT, '3.14', 3.14, 'assertEquals'
142148
],
143-
'String' => [
149+
'string' => [
144150
Cast::STRING, 'hello', 'hello', 'assertEquals'
145151
],
146-
'Array' => [
152+
'array' => [
147153
Cast::ARRAY, json_encode([1, "hello"]), [1, "hello"], 'assertEquals'
148154
],
149-
'Null' => [
150-
null, '12345', '12345', 'assertEquals'
155+
'null' => [
156+
null, null, null, 'assertEquals'
157+
],
158+
'none' => [
159+
Cast::NONE, null, null, 'assertEquals'
160+
],
161+
'date_null' => [
162+
Cast::DATE, null, null, 'assertEquals'
151163
],
152164
];
153165
}

tests/PreferenceCastTest.php

+19
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,25 @@ public function user_can_set_and_get_date_preference()
8181
$this->assertEquals($birthday->toDateString(), $preference->toDateString());
8282
}
8383

84+
/** @test */
85+
public function user_can_set_and_get_null_preference()
86+
{
87+
PreferenceBuilder::init(General::BIRTHDAY, Cast::DATE)->nullable()->create();
88+
PreferenceBuilder::init(General::OPTIONS, Cast::BACKED_ENUM)->nullable()->create();
89+
90+
$this->testUser->setPreference(General::OPTIONS, null);
91+
$this->testUser->setPreference(General::BIRTHDAY, null);
92+
93+
94+
95+
$preference = $this->testUser->getPreference(General::OPTIONS);
96+
$this->assertEquals(null, $preference);
97+
98+
$preference = $this->testUser->getPreference(General::BIRTHDAY);
99+
$this->assertEquals(null, $preference);
100+
101+
}
102+
84103
/** @test */
85104
public function user_can_set_and_get_preference_with_custom_cast()
86105
{

0 commit comments

Comments
 (0)