Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
de2f0cc
refactor: update exception handling by moving exceptions to Engines n…
kettasoft Nov 14, 2025
d094249
refactor: replace built-in exceptions with SkipExecution for custom e…
kettasoft Nov 15, 2025
0bbffc2
refactor: remove unused FilterableContext import from Engine class
kettasoft Nov 15, 2025
08a9875
refactor: change SkipExecution from abstract to concrete class
kettasoft Nov 15, 2025
35095bf
feat: add Skippable interface for execution skipping functionality
kettasoft Nov 15, 2025
be7e156
refactor: rename execute method to handle in engine classes for consi…
kettasoft Nov 15, 2025
f1bff7b
feat: add StrictnessException class for custom runtime exception hand…
kettasoft Nov 15, 2025
2a6b11d
refactor: replace InvalidArgumentException with StrictnessException i…
kettasoft Nov 15, 2025
9592863
refactor: change InvalidDataFormatException to extend StrictnessExcep…
kettasoft Nov 15, 2025
e9f440a
fix: add stopOnFailure attribute to phpunit configuration for improve…
kettasoft Nov 15, 2025
fec8328
refactor: simplify exception usage in Operators enum by importing Inv…
kettasoft Nov 15, 2025
098c09f
refactor: remove validated property and setStatus method from Clause …
kettasoft Nov 15, 2025
1521a43
refactor: streamline validation methods in ClauseFactory and improve …
kettasoft Nov 15, 2025
c0593f5
test: add User model, factory, and migration for user management
kettasoft Nov 15, 2025
2a2f22a
Merge remote-tracking branch 'origin/master' into feat/exception-hand…
kettasoft Nov 18, 2025
68e6166
fix(EngineManagerTest): rename handle method to execute in custom eng…
kettasoft Nov 18, 2025
8885f12
fix(Clause): change value type from string|null to mixed for better f…
kettasoft Nov 18, 2025
88c6268
test(InvokableEngineTest): add comprehensive filtering tests for vari…
kettasoft Nov 18, 2025
94dbc66
fix(ClauseFactory): remove unnecessary blank line in validateField me…
kettasoft Nov 19, 2025
fe26061
feat(PostFactory, CreatePostsTable, Post): enhance post model with ad…
kettasoft Nov 19, 2025
d6c6d07
feat(ExceptionHandlerInterface): add interface for handling exception…
kettasoft Nov 19, 2025
e79ef0d
feat(filterable.php): add configuration for exception handling during…
kettasoft Nov 19, 2025
514a61a
feat(FilterableExceptionHandler): implement abstract class for handli…
kettasoft Nov 19, 2025
b38b5be
feat(DefaultHandler): add default exception handler for Filterable
kettasoft Nov 19, 2025
9288a5d
feat(filterable.php): add exception handler configuration for filtering
kettasoft Nov 19, 2025
399565a
feat(Filterable): add method to retrieve exception handler instance
kettasoft Nov 19, 2025
c47c2a4
feat(NotAllowedEmptyValueException): create exception for handling em…
kettasoft Nov 19, 2025
9d188db
refactor(Engine): rename handle method to execute for consistency acr…
kettasoft Nov 19, 2025
706d0fb
refactor(Invokable): rename initializeFilters method to applyFilterMe…
kettasoft Nov 19, 2025
4822eed
feat(Engine): enhance attempt method to handle exceptions with contex…
kettasoft Nov 19, 2025
c7de75d
feat(Engines): refactor filtering logic to use attempt method for imp…
kettasoft Nov 19, 2025
f5fbcd7
feat(Exceptions): add structured exception handling documentation
kettasoft Nov 19, 2025
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
40 changes: 40 additions & 0 deletions config/filterable.php
Original file line number Diff line number Diff line change
Expand Up @@ -861,4 +861,44 @@
'log_channel' => env('FILTERABLE_CACHE_LOG_CHANNEL', 'daily'),
],
],

/*
|--------------------------------------------------------------------------
| Exception Handling
|--------------------------------------------------------------------------
|
| Define how Filterable handles exceptions thrown during filtering.
| You can choose from built-in handlers or implement your own.
|
| Supported options:
| - handler: The class responsible for handling exceptions.
| - strict: When true, exceptions will always be thrown instead of skipped.
| - log_exceptions: Whether to log unhandled or skipped exceptions.
| - report: A closure or class name to customize how exceptions are reported.
|
*/
'exceptions' => [
/*
|--------------------------------------------------------------------------
| Exception Handler
|--------------------------------------------------------------------------
|
| The class responsible for handling exceptions during filtering.
| You can implement your own handler by adhering to the
| Filterable\Contracts\ExceptionHandler interface.
|
*/
'handler' => Kettasoft\Filterable\Exceptions\Handlers\DefaultHandler::class,

/*
|--------------------------------------------------------------------------
| Strict Mode
|--------------------------------------------------------------------------
|
| When enabled, exceptions will always be thrown instead of skipped.
| This overrides per-engine strict settings.
|
*/
'strict' => env('FILTERABLE_EXCEPTION_STRICT', false),
]
];
4 changes: 4 additions & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ export default defineUserConfig({
},
],
},
{
text: "Exceptions",
link: "exceptions",
},
{
text: "Event System",
link: "events",
Expand Down
277 changes: 277 additions & 0 deletions docs/exceptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
---
title: Exception Handling
sidebarDepth: 2
---

# Exception Handling

Filterable provides a structured and predictable exception-handling system that
allows engines to decide whether filtering should stop, skip the current filter,
or continue normally.
This mechanism was redesigned to offer clearer behavior, improved safety, and
better extensibility.

The system is built around three main components:

- **Exception types** (how engines signal different situations)
- **Handlers** (how exceptions are processed)
- **Configuration** (how strict or lenient the system should behave)

---

## Exception Flow Overview

During filtering, an engine may encounter invalid, empty, or malformed input.
Instead of halting the entire process, the engine throws a specific exception
to indicate what happened.

The handler then decides—based on the exception type and strict configuration—
whether the exception should be:

- **thrown** (stop filtering),
- **or skipped** (ignore this filter and continue with the next one).

If a handler returns `false`, the current filter is skipped.

---

## Exception Types

Filterable defines two fundamental exception categories.
Each one represents a different kind of failure and implies different behavior.

### **SkipExecution**

`SkipExecution` is used when the engine cannot apply the filter, but the situation
is not considered critical.

Typical scenarios include:

- empty values when the engine does not accept empty input,
- unsupported operators,
- incomplete data structures.

**Behavior:**

- If strict mode is enabled → **the exception is thrown**
- If strict mode is disabled → **the filter is skipped**

This allows engines to ignore irrelevant or incomplete input without failing the
whole filtering pipeline.

---

### **StrictnessException**

`StrictnessException` represents invalid or unsafe input.
This type signals that the engine cannot proceed safely with the given data.

Examples include:

- corrupted or malformed values,
- invalid structure or types,
- contradictory or logically impossible conditions.

**Behavior:**

- strict mode enabled → **always thrown**
- strict mode disabled → handler may return `false` to skip, but the exception
indicates a more serious issue

This class of exceptions enforces higher input correctness.

---

## Exception Handlers

Handlers determine what happens when an exception is thrown.
They receive both the exception and the engine instance.

Returning `false` means:
**"Skip this filter and continue."**

Throwing the exception stops filtering immediately.

### **ExceptionHandlerInterface**

Every handler must implement:

```php
interface ExceptionHandlerInterface
{
public function handle(\Throwable $exception, Engine $engine): bool;
}
```

This gives full control to define how exceptions are processed.

---

## Helper Base Class: FilterableExceptionHandler

`FilterableExceptionHandler` provides shared logic that custom handlers
can use to simplify implementation.

Key helper methods:

- `isStrictThrowing()`
Checks whether global strict mode is enabled via config.

- `hasSkipping($exception)`
Detects `SkipExecution`.

- `isStrictness($exception)`
Detects strictness-related exceptions.

Custom handlers may extend this abstract class to avoid duplicating logic.

```php
abstract class FilterableExceptionHandler implements ExceptionHandlerInterface
{
abstract public function handle(\Throwable $exception, Engine $engine): bool;

protected function isStrictThrowing(): bool
{
return config('filterable.exception.strict', false);
}

protected function hasSkipping($exception): bool
{
return $exception instanceof SkipExecution;
}

protected function isStrictness($exception): bool
{
return $exception instanceof StrictnessException;
}
}
```

---

## DefaultHandler Behavior

The default handler implements the standard strategy for both exception types:

```php
class DefaultHandler extends FilterableExceptionHandler
{
public function handle(\Throwable|SkipExecution $exception, Engine $engine): bool
{
// SkipExecution: skip if not strict
if ($this->hasSkipping($exception)) {
if ($engine->isStrict() || $this->isStrictThrowing()) {
throw $exception;
}
return false; // skip current filter
}

// StrictnessException: throw when strict
if ($this->isStrictness($exception) || $this->isStrictThrowing()) {
throw $exception;
}

return false; // default: skip non-critical cases
}
}
```

### Summary of Behavior

| Exception Type | Strict Mode | Behavior |
| ------------------- | ----------- | --------------- |
| SkipExecution | Enabled | Throw exception |
| SkipExecution | Disabled | Skip filter |
| StrictnessException | Enabled | Throw exception |
| StrictnessException | Disabled | Skip filter |

---

## Configuration

Exception handling is defined in the `filterable.exceptions` config section:

```php
'exceptions' => [

'handler' => Kettasoft\Filterable\Exceptions\Handlers\DefaultHandler::class,

'strict' => env('FILTERABLE_EXCEPTION_STRICT', false),
]
```

### `handler`

Defines the class responsible for handling exceptions.

Must implement:
`ExceptionHandlerInterface`.

### `strict`

When enabled:

- exceptions are always thrown,
- skipping behavior is disabled,
- engine-level strict settings are overridden.

---

## How Filter Skipping Works

If the handler returns `false`, the current filter is skipped and the next filter
is processed.

Example:

Filters: **status**, **name**, **is_active**
Suppose:

- `status` receives empty data,
- engine does not accept empty values → throws `SkipExecution`.

If strict mode is disabled:

- `status` is skipped,
- filtering continues with `name` then `is_active`.

This allows the filtering pipeline to continue gracefully without failing
because of optional or incomplete input.

---

## Creating Custom Handlers

To implement your own rules:

```php
class MyCustomHandler extends FilterableExceptionHandler
{
public function handle(\Throwable $exception, Engine $engine): bool
{
// custom logic
}
}
```

Register it in the config:

```php
'exceptions' => [
'handler' => App\Filters\Handlers\MyCustomHandler::class,
]
```

---

## Conclusion

This unified exception-handling pipeline provides:

- clear distinction between skip-level and failure-level issues,
- configurable strictness,
- customizable handlers,
- consistent engine behavior,
- predictable filter skipping.

It enables robust and flexible filtering without breaking existing APIs.
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" colors="true">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd" colors="true" stopOnFailure="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests</directory>
Expand Down
17 changes: 17 additions & 0 deletions src/Engines/Contracts/Skippable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Kettasoft\Filterable\Engines\Contracts;

use Kettasoft\Filterable\Engines\Exceptions\SkipExecution;

interface Skippable
{
/**
* Skip the current execution with a message and optional clause.
* @param string $message
* @param mixed $clause
* @throws SkipExecution
* @return never
*/
public function skip(string $message, mixed $clause = null): never;
}
13 changes: 13 additions & 0 deletions src/Engines/Exceptions/InvalidDataFormatException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Kettasoft\Filterable\Engines\Exceptions;

use Kettasoft\Filterable\Exceptions\StrictnessException;

class InvalidDataFormatException extends StrictnessException
{
public function __construct()
{
parent::__construct("The provided data is either incommpatible or incorrectly formatted.");
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?php

namespace Kettasoft\Filterable\Exceptions;
namespace Kettasoft\Filterable\Engines\Exceptions;

class InvalidOperatorException extends \InvalidArgumentException
class InvalidOperatorException extends SkipExecution
{
/**
* InvalidOperatorException constructor.
Expand Down
15 changes: 15 additions & 0 deletions src/Engines/Exceptions/NotAllowedEmptyValueException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Kettasoft\Filterable\Engines\Exceptions;

class NotAllowedEmptyValueException extends SkipExecution
{
/**
* NotAllowedEmptyValueException constructor.
* @param mixed $message
*/
public function __construct($message = "")
{
parent::__construct($message);
}
}
Loading