Skip to content

Commit 86005d8

Browse files
committed
feat:
LaravelRule Testing Numeric Preferences readme
1 parent b7c0814 commit 86005d8

File tree

6 files changed

+141
-41
lines changed

6 files changed

+141
-41
lines changed

README.md

+64-39
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This Laravel package aims to store and manage user settings/preferences in a sim
1616
* [Concepts](#concepts)
1717
* [Define your preferences](#define-your-preferences)
1818
* [Create a Preference](#create-a-preference)
19+
* [Preference Building](#preference-building)
1920
* [Working with preferences](#working-with-preferences)
2021
* [Examples](#examples)
2122
* [Casting](#casting)
@@ -25,7 +26,6 @@ This Laravel package aims to store and manage user settings/preferences in a sim
2526
* [Available Rules](#available-rules)
2627
* [Custom Rules](#custom-rules)
2728
* [Policies](#policies)
28-
* [Preference Building](#preference-building)
2929
* [Routing](#routing)
3030
* [Anantomy](#anantomy)
3131
* [Example](#config-example)
@@ -118,15 +118,20 @@ php artisan migrate
118118

119119
### Concepts
120120

121-
Each preference has at least a name and a caster. For additional validation you can add you custom Rule object
122-
> [!TIP]
123-
> The default caster supports all major primitives, enums, objects, as well as time/datetime/date and timestamp which
124-
> get converted with `Carbon/Carbon`
121+
Each preference has at least a name and a caster.
122+
Names are stored in one or more enums and are the unique identifier for that preference
123+
124+
For additional validation you can add you custom Rule object.
125+
126+
For additional security you can add Policies
125127

126128
### Define your preferences
127129

128130
Organize them in one or more **string backed** enum.
129131

132+
> [!NOTE]
133+
> while it does not need to be string backed, its way more developer friendly. Especially when interacting over the APi
134+
130135
Each enum gets scoped and does not conflict with other enums with the same case
131136

132137
e.g.
@@ -143,7 +148,7 @@ enum Preferences :string implements PreferenceGroup
143148

144149
enum General :string implements PreferenceGroup
145150
{
146-
case LANGUAGE="language";
151+
case LANGUAGE="language";
147152
case THEME="theme";
148153
}
149154
```
@@ -166,7 +171,7 @@ public function up(): void
166171
// Or
167172
PreferenceBuilder::init(Preferences::LANGUAGE)->create()
168173
// different enums with the same value do not conflict
169-
PreferenceBuilder::init(OtherPreferences::LANGUAGE)->create()
174+
PreferenceBuilder::init(General::LANGUAGE)->create()
170175

171176
// update
172177
PreferenceBuilder::init(Preferences::LANGUAGE)
@@ -184,8 +189,6 @@ public function up(): void
184189
->nullable()
185190
->create()
186191

187-
188-
189192
}
190193

191194
public function down(): void
@@ -239,17 +242,51 @@ return new class extends Migration {
239242

240243
```
241244

245+
## Preference Building
246+
247+
<details>
248+
<summary>Check all methods available to build a Preference</summary>
249+
250+
### Available Methods
251+
252+
This table includes a complete list of all features available,
253+
when building a preference.
254+
255+
| Single-Mode | Bulk-Mode (array-keys) | Constrains | Description |
256+
|-------------------------------------|---------------------------------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
257+
| init(>name<,>cast<) | ```["name"=> >name<]``` | \>name< = instanceof PreferenceGroup | Unique identifier for the preference |
258+
| 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 |
259+
| nullable(>nullable<) | ```["nullable"=> >nullable<]``` | \>nullable< = bool | Whether the default value can be null and if the preference can be set to null |
260+
| withDefaultValue(>default_value<) | ```["default_value"=> >default_value<]``` | \>default_value< = mixed, but must comply with the cast & validationRule | Initial value for this preference |
261+
| withDescription(>description<) | ```["description"=> >description<]``` | \>description< = string | Legacy code from v1.x has no actual use as of now |
262+
| withPolicy(>policy<) | ```["policy"=> >policy<]``` | \>policy< = instanceof PreferencePolicy | Authorize actions such as update/delete etc. on certain preferences. |
263+
| withRule(>rule<) | ```["rule"=> >rule<]``` | \>rule< = instanceof ValidationRule | Additional validation Rule, to validate values before setting them |
264+
| setAllowedClasses(>allowed_values<) | ```["allowed_values"=> >allowed_values<]``` | \>allowed_values< = array of string classes. For non Primitive Casts only | Current use-cases: <br/> - restrict classes of enum or object that can be set to this preference<br/> - reconstruct the original class when sending data via api. |
265+
266+
### Available helper functions
267+
268+
Optionally, pass the default value as a second parameter
269+
270+
```php
271+
// quickly build a nullable Array preference
272+
PreferenceBuilder::buildArray(VideoPreferences::CONFIG);
273+
274+
PreferenceBuilder::buildString(VideoPreferences::LANGUAGE);
275+
```
276+
277+
</details>
278+
242279
## Working with preferences
243280

244-
two things are needed:
281+
Two things are needed:
245282

246283
- `HasPreferences` trait to access the helper functions
247284
- `PreferenceableModel` Interface to have access to the implementation
248285
- in particular to `isUserAuthorized`
249286

250287
#### isUserAuthorized
251288

252-
guard function to validate if the currently logged in (if any) user has access to this model
289+
Guard function to validate if the currently logged in (if any) user has access to this model
253290
Signature:
254291

255292
- $user the logged in user
@@ -303,7 +340,7 @@ class User extends \Illuminate\Foundation\Auth\User implements PreferenceableMod
303340

304341
## Casting
305342

306-
set the cast when creating a Preference
343+
Set the cast when creating a Preference
307344

308345
> [!NOTE]
309346
> a cast has 3 main jobs
@@ -341,7 +378,7 @@ PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
341378

342379
### Custom Caster
343380

344-
implement `CastableEnum`
381+
Implement `CastableEnum`
345382

346383
> [!IMPORTANT]
347384
> The custom caster needs to be a **string backed** enum
@@ -407,19 +444,20 @@ Additional validation, which can be way more complex than provided by the Cast
407444

408445
### Available Rules
409446

410-
| Rule | Example | Description |
411-
|----------------|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
447+
| Rule | Example | Description |
448+
|----------------|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
412449
| AndRule | `new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures all pass |
413-
| OrRule | `new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures at least one passes |
414-
| BetweenRule | `new BetweenRule(2.4, 5.5)` | For INT and FLOAT, check that the value is between min and max |
415-
| InRule | `new InRule("it","en","de")` | Expects the value to be validated to be in that equal to one of the `n` params |
416-
| 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` |
417-
| IsRule | `new IsRule(Type::ITERABLE)` | Expects a `Matteoc99\LaravelPreference\Enums\Type` Enum. Checks e.g. if the value is iterable |
418-
| LowerThanRule | `new LowerThanRule(5)` | For INT and FLOAT, check that the value to be validated is less than the one passed in the constructor |
450+
| OrRule | `new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures at least one passes |
451+
| LaravelRule | `new LaravelRule("required\|numeric")` | Expects a string, containing a Laravel Validation Rule |
452+
| BetweenRule | `new BetweenRule(2.4, 5.5)` | For INT and FLOAT, check that the value is between min and max |
453+
| InRule | `new InRule("it","en","de")` | Expects the value to be validated to be in that equal to one of the `n` params |
454+
| 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` |
455+
| IsRule | `new IsRule(Type::ITERABLE)` | Expects a `Matteoc99\LaravelPreference\Enums\Type` Enum. Checks e.g. if the value is iterable |
456+
| LowerThanRule | `new LowerThanRule(5)` | For INT and FLOAT, check that the value to be validated is less than the one passed in the constructor |
419457

420458
### Custom Rules
421459

422-
implement `ValidationRule`
460+
Implement Laravel's `ValidationRule`
423461

424462
#### Example:
425463

@@ -453,11 +491,11 @@ class MyRule implements ValidationRule
453491

454492
## Policies
455493

456-
each preference can have a Policy, should [isUserAuthorized](#isuserauthorized) not be enough for your usecase
494+
Each preference can have a Policy, should [isUserAuthorized](#isuserauthorized) not be enough for your usecase
457495

458496
### Creating policies
459497

460-
implement `PreferencePolicy` and the 4 methods defined by the contract
498+
Implement `PreferencePolicy` and the 4 methods defined by the contract
461499

462500
| parameter | description |
463501
|-----------------------------|------------------------------------------------------------|
@@ -480,22 +518,9 @@ implement `PreferencePolicy` and the 4 methods defined by the contract
480518

481519
````
482520

483-
## Preference Building
484-
485-
| Single-Mode | Bulk-Mode (array-keys) | Constrains | Description |
486-
|-------------------------------------|---------------------------------------------|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
487-
| init(>name<,>cast<) | ```["name"=> >name<]``` | \>name< = instanceof PreferenceGroup | Unique identifier for the preference |
488-
| 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 |
489-
| nullable(>nullable<) | ```["nullable"=> >nullable<]``` | \>nullable< = bool | Whether the default value can be null and if the preference can be set to null |
490-
| withDefaultValue(>default_value<) | ```["default_value"=> >default_value<]``` | \>default_value< = mixed, but must comply with the cast & validationRule | Initial value for this preference |
491-
| withDescription(>description<) | ```["description"=> >description<]``` | \>description< = string | Legacy code from v1.x has no actual use as of now |
492-
| withPolicy(>policy<) | ```["policy"=> >policy<]``` | \>policy< = instanceof PreferencePolicy | Authorize actions such as update/delete etc. on certain preferences. |
493-
| withRule(>rule<) | ```["rule"=> >rule<]``` | \>rule< = instanceof ValidationRule | Additional validation Rule, to validate values before setting them |
494-
| setAllowedClasses(>allowed_values<) | ```["allowed_values"=> >allowed_values<]``` | \>allowed_values< = array of string classes. For non Primitive Casts only | Current use-cases: <br/> - restrict classes of enum or object that can be set to this preference<br/> - reconstruct the original class when sending data via api. |
495-
496521
## Routing
497522

498-
off by default, enable it in the config
523+
Off by default, enable it in the config
499524

500525
> [!WARNING]
501526
> **(Current) limitation**: it's not possible to set object casts via API
@@ -505,7 +530,7 @@ off by default, enable it in the config
505530
'Scope': the `PreferenceableModel` Model
506531
'Group': the `PreferenceGroup` enum
507532

508-
routes then get transformed to:
533+
Routes then get transformed to:
509534

510535
| Action | URI | Description |
511536
|-----------|---------------------------------------------------|-------------------------------------------------------------|

src/Factory/PreferenceBuilder.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class PreferenceBuilder
3636
*/
3737
public static function buildString(PreferenceGroup $name, string $default = null): void
3838
{
39-
self::init($name)->nullable()->withDefaultValue(null)->create();
39+
self::init($name)->nullable()->withDefaultValue($default)->create();
4040
}
4141

4242
/**
@@ -58,7 +58,7 @@ public static function buildString(PreferenceGroup $name, string $default = null
5858
*/
5959
public static function buildArray(PreferenceGroup $name, array $default = null): void
6060
{
61-
self::init($name, Cast::ARRAY)->nullable()->withDefaultValue(null)->create();
61+
self::init($name, Cast::ARRAY)->nullable()->withDefaultValue($default)->create();
6262
}
6363

6464

src/Rules/LaravelRule.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Matteoc99\LaravelPreference\Rules;
4+
5+
6+
use Closure;
7+
use Illuminate\Contracts\Validation\ValidationRule;
8+
use Illuminate\Support\Facades\Validator;
9+
10+
class LaravelRule implements ValidationRule
11+
{
12+
public function __construct(protected string $rule)
13+
{
14+
}
15+
16+
17+
public function validate(string $attribute, mixed $value, Closure $fail): void
18+
{
19+
20+
$validator = Validator::make([$attribute => $value], [$attribute => $this->rule]);
21+
22+
if ($validator->fails()) {
23+
$fail($validator->messages());
24+
}
25+
}
26+
}

tests/PreferenceBasicTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Matteoc99\LaravelPreference\Models\Preference;
88
use Matteoc99\LaravelPreference\Rules\InRule;
99
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
10+
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\NumericPreferences;
1011
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\OtherPreferences;
1112
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences;
1213

@@ -113,5 +114,28 @@ public function init_and_delete()
113114

114115
}
115116

117+
/** @test */
118+
public function test_numeric_backed_preferences()
119+
{
120+
PreferenceBuilder::buildArray(NumericPreferences::ONE);
121+
122+
PreferenceBuilder::buildString(NumericPreferences::TWO);
123+
124+
PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
125+
PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
126+
PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
127+
PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
128+
PreferenceBuilder::init(NumericPreferences::TWO)->updateOrCreate();
129+
130+
$this->assertDatabaseCount((new Preference())->getTable(), 3);
131+
132+
$this->testUser->setPreference(NumericPreferences::TWO, "14");
133+
$this->testUser->setPreference(NumericPreferences::ONE, ["test" => "value"]);
134+
135+
$this->assertEquals('14', $this->testUser->getPreference(NumericPreferences::TWO));
136+
137+
$this->assertEquals(["test" => "value"], $this->testUser->getPreference(NumericPreferences::ONE));
138+
}
139+
116140

117141
}

tests/RulesTest/CombinedRulesTest.php

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Matteoc99\LaravelPreference\Rules\InRule;
99
use Matteoc99\LaravelPreference\Rules\InstanceOfRule;
1010
use Matteoc99\LaravelPreference\Rules\IsRule;
11+
use Matteoc99\LaravelPreference\Rules\LaravelRule;
1112
use Matteoc99\LaravelPreference\Rules\LowerThanRule;
1213
use Matteoc99\LaravelPreference\Rules\OrRule;
1314
use Matteoc99\LaravelPreference\Tests\TestCase;
@@ -43,7 +44,19 @@ public static function andRuleProvider(): array
4344
];
4445
}
4546

47+
public static function laravelRuleProvider(): array
48+
{
49+
return [
50+
[new LaravelRule('required|email'), '[email protected]', true, 'Expected LaravelRule to pass for valid email.'],
51+
[new LaravelRule('required|numeric'), '123', true, 'Expected LaravelRule to pass for numeric value.'],
52+
[new LaravelRule('required|numeric'), 'abc', false, 'Expected LaravelRule to fail for non-numeric value.'],
53+
[new LaravelRule('required|in:foo,bar,baz'), 'foo', true, 'Expected LaravelRule to pass for value within given options.'],
54+
[new LaravelRule('required|in:foo,bar,baz'), 'qux', false, 'Expected LaravelRule to fail for value not within given options.'],
55+
];
56+
}
57+
4658
/**
59+
* @dataProvider laravelRuleProvider
4760
* @dataProvider orRuleProvider
4861
* @dataProvider andRuleProvider
4962
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Matteoc99\LaravelPreference\Tests\TestSubjects\Enums;
4+
5+
use Matteoc99\LaravelPreference\Contracts\PreferenceGroup;
6+
7+
enum NumericPreferences: int implements PreferenceGroup
8+
{
9+
case ONE = 1;
10+
case TWO = 2;
11+
case THREE = 3;
12+
}

0 commit comments

Comments
 (0)