krisawzm's avatar

Filtering request input

Hi, all! This is my first post on here. I'm hoping you'll be gentle. Otherwise, bring lube, 'cus I didn't.

What I want to achieve is simple. As an example, on my app, the user should be able to submit a form with a number in any (well, almost any) format he or she would like. "100 000" should be valid. "100 000 kr" should be valid (Norwegian currency.) What I want to do, is filter (or normalize) the request inputs in such a way that both values in the previous example should become 100000, which I could then validate using the standard validation stuff in a controller or a FormRequest.

Anyway, I made a trait which can be used on a FormRequest, which looks like this:

<?php

namespace App\Http\Requests\Traits;

use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\ParameterBag;

trait FiltersRequestInput
{
    /** @var bool */
    private $hasFiltered = false;

    /**
     * Override `Illuminate\Http\Request::getInputSource()` to perform the
     * filtering.
     *
     * @return \Symfony\Component\HttpFoundation\ParameterBag
     */
    protected function getInputSource(): ParameterBag
    {
        if ($this->hasFiltered) {
            return parent::getInputSource();
        }

        $input = parent::getInputSource();

        foreach ($this->filters() as $key => $filter) {
            if (!$input->has($key)) {
                continue;
            }

            $fn = $this->getCallable($filter, $key);

            $input->set($key, $fn($input->get($key)));
        }

        $this->hasFiltered = true;

        return $input;
    }

    /**
     * Get the filters for this request.
     *
     * @return array
     */
    abstract protected function filters(): array;

    /**
     * Get the callable from a filter.
     *
     * @param  callable|string  $filter
     * @param  string           $key
     *
     * @return callable
     *
     * @throws \InvalidArgumentException
     */
    private function getCallable($filter, string $key): callable
    {
        if (is_callable($filter)) {
            return $filter;
        }

        if (substr_count($filter, '@', 1) === 1) {
            $fn = explode('@', $filter);

            if (is_callable($fn)) {
                return $fn;
            }
        }

        if (method_exists($this, $filter)) {
            return [$this, $filter];
        }

        throw new InvalidArgumentException(sprintf(
            'Invalid filter [%s] defined for input key [%s] on request [%s]',
            $filter, $key, static::class
        ));
    }
}

Next, on a FormRequest:

<?php

class SomeRequest extends \Illuminate\Foundation\Http\FormRequest
{
    use FiltersRequestInput;

    // validation rules...

    protected function filters(): array
    {
        return [
            'number' => 'intval', // <- will "filter" through php's intval. could be any callable
        ];
    }
}

Then, in a controller somewhere:

<?php

class Controller
{
    public function store(SomeRequest $request)
    {
        // will be validated already
        $request->input('number'); // will have run through filter
    }
}

Is there a better way to achieve this? Is the trait implementation too sketchy? I would appreciate some thoughts.

Thanks.

0 likes
5 replies
krisawzm's avatar

@carlcassar: Is it possible to integrate the package with a FormRequest (or a controller) in such a way that the data is sanitized before hitting the validation, and also in such a way that it actually changes input values in the request object?

krisawzm's avatar

If anybody is interested, I made a package to help achieve the initial goal of this thread.

Among other things, it will allow you to define sanitation rules in a sanitize method on any form request (e.g. the stuff in app/Http/Requests), similar to how you specify validation rules in the rules method.

Quick example:

// app/Http/Requests/Request.php

namespace App\Http\Requests;

use Alfheim\Sanitizer\Laravel\FormRequest;
// Instead of `Illuminate\Foundation\Http\FormRequest`

abstract class Request extends FormRequest
{
    //
}

That's it! Now it's trivial to define sanitation rules on your form requests...

// app/Http/Requests/FooRequest.php

namespace App\Http\Requests;

class FooRequest extends Request
{
    // Sanitation rules...
    public function sanitize()
    {
        return [
            'name'  => 'trim|ucwords',
            'email' => 'trim|mb_strtolower',
        ];
    }

    // And of course, validation is defined as per usual...
    public function rules()
    {
        return [
            'name'  => 'required',
            'email' => 'required|email',
        ];
    }
}

For completeness, I'll show you the controller...

namespace App\Http\Controllers;

use App\Http\Requests\FooRequest;

class FooController extends Controller
{
    public function create(FooRequest $request)
    {
        // At this point, the $request will be both sanitized and validated.
        // You may go ahead and access the input as usual:

        $request->all();
        $request->input('name');
        $request->only(['name', 'email']);
        // etc...
    }
}

It is very much inspired by the Sanitizer package by Dayle Rees (referenced by @carlcassar), but with some extra sugar! I also plan on adding support for array inputs (e.g. "person.*.email"). Maybe.. one day.

GitHub: https://github.com/kalfheim/sanitizer Packagist: https://packagist.org/packages/kalfheim/sanitizer

4 likes

Please or to participate in this conversation.