Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
360 changes: 318 additions & 42 deletions docs/sanitization.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions src/Sanitization/Defaults/BooleanSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Kettasoft\Filterable\Sanitization\Defaults;

use Kettasoft\Filterable\Sanitization\Contracts\Sanitizable;

/**
* Converts truthy/falsy string and integer representations to native PHP booleans.
*
* Truthy: "true", "1", "yes", "on" → true
* Falsy: "false", "0", "no", "off", "" → false
*
* Alias: 'boolean'
*
* @example
* protected $sanitizers = ['is_active' => 'boolean'];
* // "yes" → true | "off" → false
*/
class BooleanSanitizer implements Sanitizable
{
/**
* String values treated as true.
* @var array<string>
*/
protected array $truthy = ['true', '1', 'yes', 'on'];

/**
* String values treated as false.
* @var array<string>
*/
protected array $falsy = ['false', '0', 'no', 'off', ''];

public function sanitize($value): mixed
{
if (is_bool($value)) {
return $value;
}

if (is_string($value)) {
$lower = mb_strtolower(trim($value));

if (in_array($lower, $this->truthy, true)) return true;
if (in_array($lower, $this->falsy, true)) return false;
}

if (is_int($value)) {
return $value !== 0;
}

return (bool) $value;
}
}
67 changes: 67 additions & 0 deletions src/Sanitization/Defaults/ClampSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Kettasoft\Filterable\Sanitization\Defaults;

use Kettasoft\Filterable\Sanitization\Contracts\Sanitizable;

/**
* Clamps a numeric value between an optional minimum and/or maximum bound.
* Non-numeric values are returned unchanged.
*
* Alias: 'clamp'
*
* @example
* protected $sanitizers = ['per_page' => ClampSanitizer::class];
* // new ClampSanitizer(1, 100): 150 → 100 | 0 → 1
*/
class ClampSanitizer implements Sanitizable
{
/**
* Minimum allowed value (inclusive). Null = no lower bound.
* @var int|float|null
*/
protected int|float|null $min;

/**
* Maximum allowed value (inclusive). Null = no upper bound.
* @var int|float|null
*/
protected int|float|null $max;

public function __construct(int|float|null $min = null, int|float|null $max = null)
{
$this->min = $min;
$this->max = $max;
}

public function sanitize($value): mixed
{
if (is_array($value)) {
return array_map(fn($v) => $this->clamp($v), $value);
}

return $this->clamp($value);
}

protected function clamp(mixed $value): mixed
{
if (! is_numeric($value)) {
return $value;
}

// Preserve float type when either bound is a float
$value = (is_float($this->min) || is_float($this->max))
? (float) $value
: (int) $value;

if ($this->min !== null) {
$value = max($this->min, $value);
}

if ($this->max !== null) {
$value = min($this->max, $value);
}

return $value;
}
}
48 changes: 48 additions & 0 deletions src/Sanitization/Defaults/EscapeHtmlSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Kettasoft\Filterable\Sanitization\Defaults;

use Kettasoft\Filterable\Sanitization\Contracts\Sanitizable;

/**
* Converts HTML special characters to their entity equivalents for use in
* HTML text contexts. Applications must still escape values at their final
* output boundary according to that output context.
*
* Alias: 'escape_html'
*
* @example
* protected $sanitizers = ['title' => 'escape_html'];
* // "<script>alert(1)</script>" → "&lt;script&gt;alert(1)&lt;/script&gt;"
*/
class EscapeHtmlSanitizer implements Sanitizable
{
/**
* Character encoding.
* @var string
*/
protected string $encoding;

public function __construct(string $encoding = 'UTF-8')
{
$this->encoding = $encoding;
}

public function sanitize($value): mixed
{
if (is_string($value)) {
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, $this->encoding);
}

if (is_array($value)) {
return array_map(
fn($v) => is_string($v)
? htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, $this->encoding)
: $v,
$value
);
}

return $value;
}
}
45 changes: 45 additions & 0 deletions src/Sanitization/Defaults/FloatSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Kettasoft\Filterable\Sanitization\Defaults;

use Kettasoft\Filterable\Sanitization\Contracts\Sanitizable;

/**
* Casts the value to a float.
* Optionally rounds to a given number of decimal places.
*
* Alias: 'float'
*
* @example
* protected $sanitizers = ['price' => FloatSanitizer::class];
* // "19.999abc" → 19.999 | new FloatSanitizer(2) → 20.0
*/
class FloatSanitizer implements Sanitizable
{
/**
* Number of decimal places to round to (null = no rounding).
* @var int|null
*/
protected ?int $decimals;

public function __construct(?int $decimals = null)
{
$this->decimals = $decimals;
}

public function sanitize($value): mixed
{
if (is_array($value)) {
return array_map(fn($v) => $this->cast($v), $value);
}

return $this->cast($value);
}

protected function cast(mixed $value): float
{
$float = (float) filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);

return $this->decimals !== null ? round($float, $this->decimals) : $float;
}
}
47 changes: 47 additions & 0 deletions src/Sanitization/Defaults/IntegerSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Kettasoft\Filterable\Sanitization\Defaults;

use Kettasoft\Filterable\Sanitization\Contracts\Sanitizable;

/**
* Casts the value to an integer.
* Non-numeric values return 0, or null when $nullOnFail is true.
*
* Alias: 'integer'
*
* @example
* protected $sanitizers = ['page' => 'integer'];
* // "42abc" → 42 | "xyz" → 0
*/
class IntegerSanitizer implements Sanitizable
{
/**
* Return null instead of 0 for non-numeric values.
* @var bool
*/
protected bool $nullOnFail;

public function __construct(bool $nullOnFail = false)
{
$this->nullOnFail = $nullOnFail;
}

public function sanitize($value): mixed
{
if (is_array($value)) {
return array_map(fn($v) => $this->cast($v), $value);
}

return $this->cast($value);
}

protected function cast(mixed $value): ?int
{
if (is_numeric($value)) {
return (int) $value;
}

return $this->nullOnFail ? null : (int) $value;
}
}
33 changes: 33 additions & 0 deletions src/Sanitization/Defaults/LowercaseSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Kettasoft\Filterable\Sanitization\Defaults;

use Kettasoft\Filterable\Sanitization\Contracts\Sanitizable;

/**
* Converts string values to lowercase using multibyte-safe function.
*
* Alias: 'lowercase'
*
* @example
* protected $sanitizers = ['email' => 'lowercase'];
* // "Hello@Example.COM" → "hello@example.com"
*/
class LowercaseSanitizer implements Sanitizable
{
public function sanitize($value): mixed
{
if (is_string($value)) {
return mb_strtolower($value);
}

if (is_array($value)) {
return array_map(
fn($v) => is_string($v) ? mb_strtolower($v) : $v,
$value
);
}

return $value;
}
}
51 changes: 51 additions & 0 deletions src/Sanitization/Defaults/NullIfEmptySanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Kettasoft\Filterable\Sanitization\Defaults;

use Kettasoft\Filterable\Sanitization\Contracts\Sanitizable;

/**
* Returns null when the value is an empty string or matches a list of "empty" representations.
* Useful for normalising optional request inputs that arrive as empty strings.
*
* Alias: 'null_if_empty'
*
* @example
* protected $sanitizers = ['search' => 'null_if_empty'];
* // "" → null | "0" → "0" | "hello" → "hello"
*/
class NullIfEmptySanitizer implements Sanitizable
{
/**
* String representations that should be treated as empty.
* @var array<string>
*/
protected array $emptyValues;

public function __construct(array $emptyValues = ['', 'null', 'undefined', 'none'])
{
$this->emptyValues = $emptyValues;
}

public function sanitize($value): mixed
{
if (is_array($value)) {
return array_map(fn($v) => $this->nullify($v), $value);
}

return $this->nullify($value);
}

protected function nullify(mixed $value): mixed
{
if (is_null($value)) {
return null;
}

if (is_string($value) && in_array(trim($value), $this->emptyValues, true)) {
return null;
}

return $value;
}
}
Loading
Loading