Skip to content

Commit bdbd1e9

Browse files
authored
Merge pull request #37 from kettasoft/feat/exception-handling
Improved exception handling engine with new configuration options
2 parents 8b86b4d + f5fbcd7 commit bdbd1e9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1632
-138
lines changed

config/filterable.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,4 +861,44 @@
861861
'log_channel' => env('FILTERABLE_CACHE_LOG_CHANNEL', 'daily'),
862862
],
863863
],
864+
865+
/*
866+
|--------------------------------------------------------------------------
867+
| Exception Handling
868+
|--------------------------------------------------------------------------
869+
|
870+
| Define how Filterable handles exceptions thrown during filtering.
871+
| You can choose from built-in handlers or implement your own.
872+
|
873+
| Supported options:
874+
| - handler: The class responsible for handling exceptions.
875+
| - strict: When true, exceptions will always be thrown instead of skipped.
876+
| - log_exceptions: Whether to log unhandled or skipped exceptions.
877+
| - report: A closure or class name to customize how exceptions are reported.
878+
|
879+
*/
880+
'exceptions' => [
881+
/*
882+
|--------------------------------------------------------------------------
883+
| Exception Handler
884+
|--------------------------------------------------------------------------
885+
|
886+
| The class responsible for handling exceptions during filtering.
887+
| You can implement your own handler by adhering to the
888+
| Filterable\Contracts\ExceptionHandler interface.
889+
|
890+
*/
891+
'handler' => Kettasoft\Filterable\Exceptions\Handlers\DefaultHandler::class,
892+
893+
/*
894+
|--------------------------------------------------------------------------
895+
| Strict Mode
896+
|--------------------------------------------------------------------------
897+
|
898+
| When enabled, exceptions will always be thrown instead of skipped.
899+
| This overrides per-engine strict settings.
900+
|
901+
*/
902+
'strict' => env('FILTERABLE_EXCEPTION_STRICT', false),
903+
]
864904
];

docs/.vuepress/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ export default defineUserConfig({
233233
},
234234
],
235235
},
236+
{
237+
text: "Exceptions",
238+
link: "exceptions",
239+
},
236240
{
237241
text: "Event System",
238242
link: "events",

docs/exceptions.md

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
title: Exception Handling
3+
sidebarDepth: 2
4+
---
5+
6+
# Exception Handling
7+
8+
Filterable provides a structured and predictable exception-handling system that
9+
allows engines to decide whether filtering should stop, skip the current filter,
10+
or continue normally.
11+
This mechanism was redesigned to offer clearer behavior, improved safety, and
12+
better extensibility.
13+
14+
The system is built around three main components:
15+
16+
- **Exception types** (how engines signal different situations)
17+
- **Handlers** (how exceptions are processed)
18+
- **Configuration** (how strict or lenient the system should behave)
19+
20+
---
21+
22+
## Exception Flow Overview
23+
24+
During filtering, an engine may encounter invalid, empty, or malformed input.
25+
Instead of halting the entire process, the engine throws a specific exception
26+
to indicate what happened.
27+
28+
The handler then decides—based on the exception type and strict configuration—
29+
whether the exception should be:
30+
31+
- **thrown** (stop filtering),
32+
- **or skipped** (ignore this filter and continue with the next one).
33+
34+
If a handler returns `false`, the current filter is skipped.
35+
36+
---
37+
38+
## Exception Types
39+
40+
Filterable defines two fundamental exception categories.
41+
Each one represents a different kind of failure and implies different behavior.
42+
43+
### **SkipExecution**
44+
45+
`SkipExecution` is used when the engine cannot apply the filter, but the situation
46+
is not considered critical.
47+
48+
Typical scenarios include:
49+
50+
- empty values when the engine does not accept empty input,
51+
- unsupported operators,
52+
- incomplete data structures.
53+
54+
**Behavior:**
55+
56+
- If strict mode is enabled → **the exception is thrown**
57+
- If strict mode is disabled → **the filter is skipped**
58+
59+
This allows engines to ignore irrelevant or incomplete input without failing the
60+
whole filtering pipeline.
61+
62+
---
63+
64+
### **StrictnessException**
65+
66+
`StrictnessException` represents invalid or unsafe input.
67+
This type signals that the engine cannot proceed safely with the given data.
68+
69+
Examples include:
70+
71+
- corrupted or malformed values,
72+
- invalid structure or types,
73+
- contradictory or logically impossible conditions.
74+
75+
**Behavior:**
76+
77+
- strict mode enabled → **always thrown**
78+
- strict mode disabled → handler may return `false` to skip, but the exception
79+
indicates a more serious issue
80+
81+
This class of exceptions enforces higher input correctness.
82+
83+
---
84+
85+
## Exception Handlers
86+
87+
Handlers determine what happens when an exception is thrown.
88+
They receive both the exception and the engine instance.
89+
90+
Returning `false` means:
91+
**"Skip this filter and continue."**
92+
93+
Throwing the exception stops filtering immediately.
94+
95+
### **ExceptionHandlerInterface**
96+
97+
Every handler must implement:
98+
99+
```php
100+
interface ExceptionHandlerInterface
101+
{
102+
public function handle(\Throwable $exception, Engine $engine): bool;
103+
}
104+
```
105+
106+
This gives full control to define how exceptions are processed.
107+
108+
---
109+
110+
## Helper Base Class: FilterableExceptionHandler
111+
112+
`FilterableExceptionHandler` provides shared logic that custom handlers
113+
can use to simplify implementation.
114+
115+
Key helper methods:
116+
117+
- `isStrictThrowing()`
118+
Checks whether global strict mode is enabled via config.
119+
120+
- `hasSkipping($exception)`
121+
Detects `SkipExecution`.
122+
123+
- `isStrictness($exception)`
124+
Detects strictness-related exceptions.
125+
126+
Custom handlers may extend this abstract class to avoid duplicating logic.
127+
128+
```php
129+
abstract class FilterableExceptionHandler implements ExceptionHandlerInterface
130+
{
131+
abstract public function handle(\Throwable $exception, Engine $engine): bool;
132+
133+
protected function isStrictThrowing(): bool
134+
{
135+
return config('filterable.exception.strict', false);
136+
}
137+
138+
protected function hasSkipping($exception): bool
139+
{
140+
return $exception instanceof SkipExecution;
141+
}
142+
143+
protected function isStrictness($exception): bool
144+
{
145+
return $exception instanceof StrictnessException;
146+
}
147+
}
148+
```
149+
150+
---
151+
152+
## DefaultHandler Behavior
153+
154+
The default handler implements the standard strategy for both exception types:
155+
156+
```php
157+
class DefaultHandler extends FilterableExceptionHandler
158+
{
159+
public function handle(\Throwable|SkipExecution $exception, Engine $engine): bool
160+
{
161+
// SkipExecution: skip if not strict
162+
if ($this->hasSkipping($exception)) {
163+
if ($engine->isStrict() || $this->isStrictThrowing()) {
164+
throw $exception;
165+
}
166+
return false; // skip current filter
167+
}
168+
169+
// StrictnessException: throw when strict
170+
if ($this->isStrictness($exception) || $this->isStrictThrowing()) {
171+
throw $exception;
172+
}
173+
174+
return false; // default: skip non-critical cases
175+
}
176+
}
177+
```
178+
179+
### Summary of Behavior
180+
181+
| Exception Type | Strict Mode | Behavior |
182+
| ------------------- | ----------- | --------------- |
183+
| SkipExecution | Enabled | Throw exception |
184+
| SkipExecution | Disabled | Skip filter |
185+
| StrictnessException | Enabled | Throw exception |
186+
| StrictnessException | Disabled | Skip filter |
187+
188+
---
189+
190+
## Configuration
191+
192+
Exception handling is defined in the `filterable.exceptions` config section:
193+
194+
```php
195+
'exceptions' => [
196+
197+
'handler' => Kettasoft\Filterable\Exceptions\Handlers\DefaultHandler::class,
198+
199+
'strict' => env('FILTERABLE_EXCEPTION_STRICT', false),
200+
]
201+
```
202+
203+
### `handler`
204+
205+
Defines the class responsible for handling exceptions.
206+
207+
Must implement:
208+
`ExceptionHandlerInterface`.
209+
210+
### `strict`
211+
212+
When enabled:
213+
214+
- exceptions are always thrown,
215+
- skipping behavior is disabled,
216+
- engine-level strict settings are overridden.
217+
218+
---
219+
220+
## How Filter Skipping Works
221+
222+
If the handler returns `false`, the current filter is skipped and the next filter
223+
is processed.
224+
225+
Example:
226+
227+
Filters: **status**, **name**, **is_active**
228+
Suppose:
229+
230+
- `status` receives empty data,
231+
- engine does not accept empty values → throws `SkipExecution`.
232+
233+
If strict mode is disabled:
234+
235+
- `status` is skipped,
236+
- filtering continues with `name` then `is_active`.
237+
238+
This allows the filtering pipeline to continue gracefully without failing
239+
because of optional or incomplete input.
240+
241+
---
242+
243+
## Creating Custom Handlers
244+
245+
To implement your own rules:
246+
247+
```php
248+
class MyCustomHandler extends FilterableExceptionHandler
249+
{
250+
public function handle(\Throwable $exception, Engine $engine): bool
251+
{
252+
// custom logic
253+
}
254+
}
255+
```
256+
257+
Register it in the config:
258+
259+
```php
260+
'exceptions' => [
261+
'handler' => App\Filters\Handlers\MyCustomHandler::class,
262+
]
263+
```
264+
265+
---
266+
267+
## Conclusion
268+
269+
This unified exception-handling pipeline provides:
270+
271+
- clear distinction between skip-level and failure-level issues,
272+
- configurable strictness,
273+
- customizable handlers,
274+
- consistent engine behavior,
275+
- predictable filter skipping.
276+
277+
It enables robust and flexible filtering without breaking existing APIs.

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" colors="true">
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" colors="true" stopOnFailure="true">
33
<testsuites>
44
<testsuite name="Unit">
55
<directory suffix="Test.php">./tests</directory>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Kettasoft\Filterable\Engines\Contracts;
4+
5+
use Kettasoft\Filterable\Engines\Exceptions\SkipExecution;
6+
7+
interface Skippable
8+
{
9+
/**
10+
* Skip the current execution with a message and optional clause.
11+
* @param string $message
12+
* @param mixed $clause
13+
* @throws SkipExecution
14+
* @return never
15+
*/
16+
public function skip(string $message, mixed $clause = null): never;
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Kettasoft\Filterable\Engines\Exceptions;
4+
5+
use Kettasoft\Filterable\Exceptions\StrictnessException;
6+
7+
class InvalidDataFormatException extends StrictnessException
8+
{
9+
public function __construct()
10+
{
11+
parent::__construct("The provided data is either incommpatible or incorrectly formatted.");
12+
}
13+
}

src/Exceptions/InvalidOperatorException.php renamed to src/Engines/Exceptions/InvalidOperatorException.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?php
22

3-
namespace Kettasoft\Filterable\Exceptions;
3+
namespace Kettasoft\Filterable\Engines\Exceptions;
44

5-
class InvalidOperatorException extends \InvalidArgumentException
5+
class InvalidOperatorException extends SkipExecution
66
{
77
/**
88
* InvalidOperatorException constructor.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Kettasoft\Filterable\Engines\Exceptions;
4+
5+
class NotAllowedEmptyValueException extends SkipExecution
6+
{
7+
/**
8+
* NotAllowedEmptyValueException constructor.
9+
* @param mixed $message
10+
*/
11+
public function __construct($message = "")
12+
{
13+
parent::__construct($message);
14+
}
15+
}

0 commit comments

Comments
 (0)