Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
db1a087
refactor(RequiredValueAttributeTest): remove redundant test for custo…
kettasoft Mar 1, 2026
661e7e4
feat(Payload): add cast and as methods for dynamic type casting
kettasoft Mar 1, 2026
c2a527d
feat(Attributes): implement MethodAttribute interface and enhance att…
kettasoft Mar 1, 2026
37d97b7
feat(Cast): implement Cast attribute for type casting and add tests
kettasoft Mar 1, 2026
102e5c4
feat(In): add In attribute for value validation and implement tests
kettasoft Mar 1, 2026
71ebd13
feat(Payload): enhance explode and split methods to support value ove…
kettasoft Mar 1, 2026
2687ccc
feat(Explode): add Explode attribute for string splitting functionality
kettasoft Mar 1, 2026
395ffaa
feat(Between): implement Between attribute for value range validation…
kettasoft Mar 4, 2026
75e90d1
feat(Regex): implement Regex attribute for regex pattern validation a…
kettasoft Mar 4, 2026
c8f1b84
feat(Trim): implement Trim attribute for string trimming functionalit…
kettasoft Mar 4, 2026
b6e0133
feat(Sanitize): implement Sanitize attribute for input sanitization w…
kettasoft Mar 4, 2026
3adcb16
feat(Scope): add Scope attribute for Eloquent scope application and i…
kettasoft Mar 4, 2026
11d0ef2
feat(SkipIf): implement SkipIf attribute for conditional filtering an…
kettasoft Mar 4, 2026
a3f438e
feat(MapValue): implement MapValue attribute for value mapping with s…
kettasoft Mar 4, 2026
2dcb48a
fix(Payload): update method to set value before returning it
kettasoft Mar 4, 2026
54672aa
refactor(CastAttributeTest): simplify value retrieval in cast attribu…
kettasoft Mar 4, 2026
d081281
feat(Authorize): implement Authorize attribute for method-level autho…
kettasoft Mar 4, 2026
20f950d
Add documentation for Invokable Engine annotations
kettasoft Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,75 @@ export default defineUserConfig({
children: [
{
text: "Invokable",
link: "engines/invokable",
collapsible: true,
children: [
{
text: "Overview",
link: "engines/invokable/",
},
{
text: "Annotations",
collapsible: true,
children: [
{
text: "Overview",
link: "engines/invokable/annotations/",
},
{
text: "Authorize",
link: "engines/invokable/annotations/authorize",
},
{
text: "SkipIf",
link: "engines/invokable/annotations/skip-if",
},
{
text: "Trim",
link: "engines/invokable/annotations/trim",
},
{
text: "Sanitize",
link: "engines/invokable/annotations/sanitize",
},
{
text: "Cast",
link: "engines/invokable/annotations/cast",
},
{
text: "DefaultValue",
link: "engines/invokable/annotations/default-value",
},
{
text: "MapValue",
link: "engines/invokable/annotations/map-value",
},
{
text: "Explode",
link: "engines/invokable/annotations/explode",
},
{
text: "Required",
link: "engines/invokable/annotations/required",
},
{
text: "In",
link: "engines/invokable/annotations/in",
},
{
text: "Between",
link: "engines/invokable/annotations/between",
},
{
text: "Regex",
link: "engines/invokable/annotations/regex",
},
{
text: "Scope",
link: "engines/invokable/annotations/scope",
},
],
},
],
},
{
text: "Tree",
Expand Down
84 changes: 84 additions & 0 deletions docs/engines/invokable/annotations/authorize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
sidebarDepth: 1
---

# #[Authorize]

**Stage:** `CONTROL` (1)

Requires authorization before the filter method executes. If authorization fails, the filter is skipped entirely.

---

## Parameters

| Parameter | Type | Required | Description |
| ------------ | -------- | -------- | ------------------------------------------------------------------- |
| `$authorize` | `string` | ✅ | Fully qualified class name implementing the `Authorizable` contract |

---

## Usage

```php
use Kettasoft\Filterable\Engines\Foundation\Attributes\Annotations\Authorize;

#[Authorize(AdminOnly::class)]
protected function secretField(Payload $payload)
{
return $this->builder->where('secret_field', $payload->value);
}
```

---

## Authorizable Contract

The class passed to `#[Authorize]` must implement `Kettasoft\Filterable\Contracts\Authorizable`:

```php
<?php

namespace App\Filters\Authorizations;

use Kettasoft\Filterable\Contracts\Authorizable;

class AdminOnly implements Authorizable
{
public function authorize(): bool
{
return auth()->user()?->is_admin ?? false;
}
}
```

---

## Behavior

| Scenario | Result |
| -------------------------------------- | ------------------------------------------------- |
| `authorize()` returns `true` | Filter method executes normally |
| `authorize()` returns `false` | Filter is **skipped** (SkipExecution is thrown) |
| Class doesn't implement `Authorizable` | `InvalidArgumentException` is thrown |

---

## Example: Role-Based Filter Access

```php
class RoleFilter implements Authorizable
{
public function authorize(): bool
{
return auth()->user()?->hasRole('manager');
}
}

// In your filter class:
#[Authorize(RoleFilter::class)]
protected function salary(Payload $payload)
{
return $this->builder->where('salary', '>=', $payload->value);
}
```
86 changes: 86 additions & 0 deletions docs/engines/invokable/annotations/between.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
sidebarDepth: 1
---

# #[Between]

**Stage:** `VALIDATE` (3)

Validates that the payload value falls within a specified numeric range. If the value is outside the range or not numeric, the filter is **skipped**.

---

## Parameters

| Parameter | Type | Required | Description |
| --------- | ------------- | -------- | --------------------- |
| `$min` | `float\|int` | ✅ | Minimum allowed value |
| `$max` | `float\|int` | ✅ | Maximum allowed value |

---

## Usage

### Integer Range

```php
use Kettasoft\Filterable\Engines\Foundation\Attributes\Annotations\Between;

#[Between(min: 1, max: 100)]
protected function views(Payload $payload)
{
return $this->builder->where('views', '>=', $payload->value);
}
```

### Float Range

```php
#[Between(min: 0.0, max: 5.0)]
protected function rating(Payload $payload)
{
return $this->builder->where('rating', '>=', $payload->value);
}
```

---

## Behavior

| Scenario | Result |
| ----------------------------------- | ------------------------------------ |
| Value is numeric and within range | Filter executes normally |
| Value is at the minimum boundary | Filter executes normally (**inclusive**) |
| Value is at the maximum boundary | Filter executes normally (**inclusive**) |
| Value is below the range | Filter is **skipped** |
| Value is above the range | Filter is **skipped** |
| Value is not numeric | Filter is **skipped** |

---

## Boundary Behavior

The check is **inclusive** on both ends:

```php
#[Between(min: 1, max: 100)]
// 1 → ✅ passes
// 50 → ✅ passes
// 100 → ✅ passes
// 0 → ❌ skipped
// 101 → ❌ skipped
```

---

## Combining with Other Attributes

```php
#[SkipIf('empty')]
#[Trim]
#[Between(min: 1, max: 1000)]
protected function price(Payload $payload)
{
return $this->builder->where('price', '>=', $payload->value);
}
```
80 changes: 80 additions & 0 deletions docs/engines/invokable/annotations/cast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
sidebarDepth: 1
---

# #[Cast]

**Stage:** `TRANSFORM` (2)

Casts the payload value to a specific type using the Payload's `as*` methods.

---

## Parameters

| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ---------------------------------------------------- |
| `$type` | `string` | ✅ | The target type name (maps to `Payload::as{Type}()`) |

---

## Supported Types

| Type | Maps To | Description |
| --------- | ----------------------- | ------------------------------------ |
| `int` | `$payload->asInt()` | Cast to integer |
| `boolean` | `$payload->asBoolean()` | Cast to boolean |
| `array` | `$payload->asArray()` | Decode JSON or return existing array |
| `carbon` | `$payload->asCarbon()` | Parse to Carbon date instance |
| `slug` | `$payload->asSlug()` | Convert to URL-friendly slug |
| `like` | `$payload->asLike()` | Wrap with `%` for LIKE queries |

---

## Usage

### Cast to Integer

```php
use Kettasoft\Filterable\Engines\Foundation\Attributes\Annotations\Cast;

#[Cast('int')]
protected function views(Payload $payload)
{
// "42" → 42
return $this->builder->where('views', '>=', $payload->value);
}
```

### Cast to Boolean

```php
#[Cast('boolean')]
protected function isFeatured(Payload $payload)
{
// "true" → true, "false" → false
return $this->builder->where('is_featured', $payload->value);
}
```

### Cast to Array (from JSON)

```php
#[Cast('array')]
protected function tags(Payload $payload)
{
// '["php","laravel"]' → ['php', 'laravel']
$tags = $payload->value;
return $this->builder->whereIn('tag', $tags);
}
```

---

## Behavior

| Scenario | Result |
| -------------------------- | ------------------------------- |
| Cast type is supported | Value is cast and returned |
| Cast type is not supported | `StrictnessException` is thrown |
| Cast fails (invalid value) | `StrictnessException` is thrown |
66 changes: 66 additions & 0 deletions docs/engines/invokable/annotations/default-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
sidebarDepth: 1
---

# #[DefaultValue]

**Stage:** `TRANSFORM` (2)

Sets a fallback value when the payload value is empty or null. The filter method still executes, but with the default value instead of the empty input.

---

## Parameters

| Parameter | Type | Required | Description |
| --------- | ------- | -------- | ------------------------------------ |
| `$value` | `mixed` | ✅ | The default value to use as fallback |

---

## Usage

```php
use Kettasoft\Filterable\Engines\Foundation\Attributes\Annotations\DefaultValue;

#[DefaultValue('active')]
protected function status(Payload $payload)
{
// If status is empty → uses "active"
return $this->builder->where('status', $payload->value);
}
```

### With Numeric Default

```php
#[DefaultValue(10)]
protected function perPage(Payload $payload)
{
// If perPage is empty → uses 10
return $this->builder->limit($payload->value);
}
```

---

## Behavior

| Scenario | Result |
| ------------------------------ | ----------------------------------------- |
| Value is empty or null | Payload value is set to the default |
| Value is provided (non-empty) | Default is **not** applied, original kept |

---

## Combining with Other Attributes

```php
#[DefaultValue('pending')]
#[In('active', 'pending', 'archived')]
protected function status(Payload $payload)
{
// Empty input → "pending" → passes In validation
return $this->builder->where('status', $payload->value);
}
```
Loading