All Projects → mpscholten → request-parser

mpscholten / request-parser

Licence: MIT license
Small PHP Library for type-safe input handling

Programming Languages

PHP
23972 projects - #3 most used programming language

Projects that are alternatives of or similar to request-parser

Iio Sensor Proxy
IIO accelerometer sensor to input device proxy
Stars: ✭ 192 (+262.26%)
Mutual labels:  input
Wickedengine
3D engine focusing on modern rendering techniques and performance.
Stars: ✭ 3,148 (+5839.62%)
Mutual labels:  input
liu
💫 Boshiamy Input Method in macOS 嘸蝦米輸入法
Stars: ✭ 39 (-26.42%)
Mutual labels:  input
Vue Cool Select
Select with autocomplete, slots, bootstrap and material design themes.
Stars: ✭ 195 (+267.92%)
Mutual labels:  input
React Intl Tel Input
Rewrite International Telephone Input in React.js. (Looking for maintainers, and PRs & contributors are also welcomed!)
Stars: ✭ 212 (+300%)
Mutual labels:  input
Decoders
Elegant validation library for type-safe input data (for TypeScript and Flow)
Stars: ✭ 246 (+364.15%)
Mutual labels:  input
Selectize.js
Selectize is the hybrid of a textbox and <select> box. It's jQuery based, and it has autocomplete and native-feeling keyboard navigation; useful for tagging, contact lists, etc.
Stars: ✭ 12,744 (+23945.28%)
Mutual labels:  input
slim-skeleton
Slim Framework skeleton application following MVC construction
Stars: ✭ 18 (-66.04%)
Mutual labels:  psr
Rangetouch
A super tiny library to make `<input type='range'>` sliders work better on touch devices
Stars: ✭ 224 (+322.64%)
Mutual labels:  input
rapp
Cross-platform entry point library
Stars: ✭ 57 (+7.55%)
Mutual labels:  input
React Code Input
React component for entering and validating PIN code.
Stars: ✭ 207 (+290.57%)
Mutual labels:  input
Ant Plus
🔺 Ant Design 表单简化版
Stars: ✭ 212 (+300%)
Mutual labels:  input
Enigo
Cross platform input simulation in Rust
Stars: ✭ 254 (+379.25%)
Mutual labels:  input
Ngx Material File Input
File input for Angular Material form-field
Stars: ✭ 193 (+264.15%)
Mutual labels:  input
angular-datetime-inputs
📅 Angular directives for datetime inputs
Stars: ✭ 20 (-62.26%)
Mutual labels:  input
Voice Overlay Android
🗣 An overlay that gets your user’s voice permission and input as text in a customizable UI
Stars: ✭ 189 (+256.6%)
Mutual labels:  input
Pygame Menu
Menu for pygame. Simple, lightweight and easy to use
Stars: ✭ 244 (+360.38%)
Mutual labels:  input
input-mask
🎭 @ngneat/input-mask is an angular library that creates an input mask
Stars: ✭ 174 (+228.3%)
Mutual labels:  input
Truth
A Domain Representation Language
Stars: ✭ 23 (-56.6%)
Mutual labels:  type-safety
InputSimulatorStandard
Input Simulator Standard
Stars: ✭ 54 (+1.89%)
Mutual labels:  input

request parser

Latest Stable Version License Circle CI Total Downloads

Small PHP Library for type-safe input handling.

The Problem

Let's say you have an action which lists some entities. This includes paging, ascending or descending ordering and optional filtering by the time the entity was created. This action will have some kind of input parsing, which can look like this:

public function index()
{
    $page = $this->request->query->get('page');
    if ($page === null || !is_integer($page)) {
        throw new Exception("Parameter page not found");
    }
    
    $order = $this->request->query->get('order');
    if ($order === null || !in_array($order, ['asc', 'desc'])) {
        throw new Exception("Parameter order not found");
    }
    
    // Optional parameter
    $createdAt = $this->query->query->get('createdAt');
    if (is_string($createdAt)) {
        $createdAt = new DateTime($createdAt);
    } else {
        $createdAt = null;
    }
}

Obviously this code is not very nice to read because it is not very descriptive. It's also pretty verbose for what it's doing. And when you don't pay close attention you will probably miss a null check or a type check.

Now compare the above code to this version:

public function index()
{
    $page = $this->queryParameter('page')->int()->required();
    $order = $this->queryParameter('order')->oneOf(['asc', 'desc'])->required();
    $createdAt = $this->queryParameter('createdAt')->dateTime()->defaultsTo(null);
}

That's what this library offers. It allows you to express "this action requires a page parameter of type int" or "this action has an optional parameter createdAt of type DateTime, and will be set to a default value if absent".

Examples

If you'd like to go straight to the code now, you can just play around with the examples.

  1. cd /tmp
  2. git clone [email protected]:mpscholten/request-parser.git
  3. cd request-parser
  4. composer install
  5. cd examples
  6. php -S localhost:8080
  7. Open it: http://localhost:8080/symfony.php?action=hello

There are also several other php files inside the examples directory. To get your hands dirty, I suggest just modifying the examples a bit.

Getting Started

Install via composer

composer require mpscholten/request-parser

Integrations

  • If you're using symfony/http-foundation, click here.
  • If you're using a Psr7 ServerRequestInterface implementation, click here.
  • If you're using some other Request abstraction (or maybe just plain old $_GET and friends), check out this example.

Symfony HttpFoundation

The following example assumes you're using the symfony Request:

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $this->initRequestParser($request);
    }
}

Then you can use the library like this:

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $this->initRequestParser($request);
    }
    
    public function myAction()
    {
        $someParameter = $this->queryParameter('someParameter')->string()->required();
    }
}

When doing GET /MyController/myAction?someParameter=example, the $someParameter variable will contain the string "example".

You might wonder what happens when we leave out the ?someParameter part, like GET /MyController/myAction. In this case the $this->queryParameter('someParameter')->string()->required() will throw a NotFoundException. This exception can be handled by your application to show an error message.

Take a look at the examples.

Psr7

The following example assumes you're using the Psr7 ServerRequestInterface:

class MyController
{
    use \MPScholten\RequestParser\Psr7\ControllerHelperTrait;
    
    public function __construct(ServerRequestInterface $request)
    {
        $this->initRequestParser($request);
    }
}

Then you can use the library like this:

class MyController
{
    use \MPScholten\RequestParser\Psr7\ControllerHelperTrait;
    
    public function __construct(ServerRequestInterface $request)
    {
        $this->initRequestParser($request);
    }
    
    public function myAction()
    {
        $someParameter = $this->queryParameter('someParameter')->string()->required();
    }
}

When doing GET /MyController/myAction?someParameter=example, the $someParameter variable will contain the string "example".

You might wonder what happens when we leave out the ?someParameter part, like GET /MyController/myAction. In this case the $this->queryParameter('someParameter')->string()->required() will throw a NotFoundException. This exception can be handled by your application to show an error message.

Take a look at the examples.

Optional Parameters

To make the someParameter optional, we can just replace required() with defaultsTo($someDefaultValue):

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $this->initRequestParser($request);
    }
    
    public function myAction()
    {
        $someParameter = $this->queryParameter('someParameter')->string()->defaultsTo('no value given');
    }
}

When doing GET /MyController/myAction, the $someParameter variable will now contain the string "no value given". No exception will be thrown because we specified a default value.

In general you first specify the parameter name, followed by the type and then specify whether the parameter is required or is optional with a default value.

For more examples, check out the examples/ directory of this repository. It contains several runnable examples.

Integers, Enums, DateTimes and Json Payloads

Often we need more than just strings. RequestParser also provides methods for other data types:

class DashboardController
{
    public function show()
    {
        $dashboardId = $this->queryParameter('id')->int()->required();
        
        // GET /dashboard?name=Hello   =>   $dashboardName == "Hello"
        $dashboardName = $this->queryParameter('name')->string()->required();
        
        // Get /dashboard?name=   => $dashboardName == "default value"
        $dashboardName = $this->queryParameter('name')->string()->defaultsToIfEmpty("default value");
        
        // GET /dashboard?status=private  =>   $dashboardStatus == "private"
        // GET /dashboard?status=public   =>   $dashboardStatus == "public"
        // GET /dashboard?status=invalid  =>   A NotFoundException will be thrown
        $dashboardStatus = $this->queryParameter('status')->oneOf(['private', 'public'])->required();
        
        // GET /dashboard?createdAt=01.01.2016     =>   $dateTime == new DateTime("01.01.2016")
        // GET /dashboard?createdAt=invalid_date   =>   A NotFoundException will be thrown
        $dateTime = $this->queryParameter('createdAt')->dateTime()->required();
        
        // GET /dashboard?config={"a":true}     =>   $json == ['a' => true]
        $json = $this->queryParameter('config')->json()->required();
        
        // GET /dashboard?includeWidgets=true    =>   $includeWidgets == true
        // GET /dashboard?includeWidgets=false   =>   $includeWidgets == false
        // GET /dashboard?includeWidgets=0       =>   $includeWidgets == false
        // GET /dashboard?includeWidgets=abcde   =>   A NotFoundException will be thrown
        $includeWidgets = $this->queryParameter('includeWidgets')->boolean()->required();
        
        // GET /dashboard?includeWidgets=yes   =>   $includeWidgets == true
        // GET /dashboard?includeWidgets=no    =>   $includeWidgets == false
        $includeWidgets = $this->queryParameter('includeWidgets')->yesNoBoolean()->required();
        
        // GET /image?scale=2.5   =>   $scale == 2.5
        $scale = $this->queryParameter('scale')->float()->required();
    }
}

All of these types also provide a defaultsTo variant.

Supported Data Types
Type Code example Input example
String $this->queryParameter('name')->string()->required(); 'John Doe'
Comma-Separated String $this->queryParameter('names')->commaSeparated()->string()->required(); 'John Doe,John Oliver'
Integer $this->queryParameter('id')->int()->required(); '5'
Comma-Separated Integer $this->queryParameter('groupIds')->commaSeparated()->int()->required(); '5,6,7,8'
Float $this->queryParameter('ratio')->float()->required(); '0.98'
Comma-Separated Float $this->queryParameter('precipitation')->commaSeparated()->float()->required(); '0.98,1.24,5.21'
DateTime $this->queryParameter('timestamp')->dateTime()->required(); '2016-07-20'
Comma-Separated DateTime $this->queryParameter('eventTimes')->commaSeparated()->dateTime()->required(); '2016-07-20 13:10:50,2016-07-21 12:01:07'
Boolean $this->queryParameter('success')->boolean()->required(); 'true'
Comma-Separated Boolean $this->queryParameter('answers')->commaSeparated()->boolean()->required(); '1,0,0,1'
Yes/No Boolean $this->queryParameter('success')->yesNoBoolean()->required(); 'yes'
Comma-Separated Yes/No Boolean $this->queryParameter('answers')->commaSeparated()->yesNoBoolean()->required(); 'y,n,n,y,n'
JSON $this->queryParameter('payload')->json()->required(); '{"event":"click","timestamp":"2016-07-20 13:10:50"}'
Comma-Separated JSON $this->queryParameter('events')->commaSeparated()->json()->required(); '{"event":"click","timestamp":"2016-07-20 13:10:50"},{"event":"add_to_basket","timestamp":"2016-07-20 13:11:01"}'
GET Requests:

$this->queryParameter($name) tells the controller that we want a query parameter (everything after the "?" is called the query string). This is usually what we want when dealing with GET requests

POST Requests:

When we're dealing with a POST request, we need to use $this->bodyParameter($name) to access form fields or the ajax payload.

Autocompletion

The library allows you to take extensive use of autocompletion features of your IDE. E.g. after typing $this->queryParameter('someParameter)-> your IDE will offer you all the possible input types, e.g. string() or int(). After picking a type, e.g. string(), your IDE will offer required() or defaultsTo(defaultValue) to specify the behavior when the parameter is not set.

Static Analysis

The library supports static analysis by your IDE. E.g. when having a parameter like $createdAt = $this->queryParameter('createdAt')->dateTime()->required();, your IDE will know that $createdAt is a DateTime object. This allows you to detect type errors while editing and also decreases the maintenance cost of an action because the types improve legibility.

The library also decreases the risk of unexpected null values because parameters always have an explicit default value or are required.

Error Handling

When a parameter is required but not found or when validation fails, the library will throw an exception. The default exceptions are \MPScholten\RequestParser\NotFoundException and \MPScholten\RequestParser\InvalidValueException. The suggested way to handle the errors thrown by the library is to catch them inside your front controller:

try {
    $controller->$action();
} catch (NotFoundException $e) {
    echo $e->getMessage();
} catch (InvalidValueException $e) {
    echo $e->getMessage();
}

Using Custom Exception Classes

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $exceptionFactory = new ExceptionFactory(CustomNotFoundException::class, CustomInvalidValueException::class));

        $config = new \MPScholten\RequestParser\Config();
        $config->setExceptionFactory($exceptionFactory);

        $this->initRequestParser($request, $config);
    }
}

Using Custom Exception Messages

Overriding Single Messages

If you need to override the exception message thrown by the library just once or twice, you can do this by passing the exception messages as the first and second argument to ->required():

class DashboardController
{
    public function show()
    {
        $dashboardId = $this->queryParameter('id')->int()->required("The dashboard id has to be a valid number", "No dashboard id given");
    }
}
Overriding All Messages

If you don't want to specify a custom exception message for all your actions, but still don't want to use the built-in exception messages, you can provide your own exception message generator:

class FriendlyExceptionMessageFactory extends \MPScholten\RequestParser\ExceptionMessageFactory
{
    protected function createNotFoundMessage($parameterName)
    {
        return "Looks like $parameterName is missing :)";
    }

    protected function createInvalidValueMessage($parameterName, $parameterValue, $expected)
    {
        return "Whoops :) $parameterName seems to be invalid. We're looking for $expected but you provided '$parameterValue'";
    }
}

class MyController
{
    use \MPScholten\RequestParser\Symfony\ControllerHelperTrait;
    
    public function __construct(Request $request)
    {
        $config = new \MPScholten\RequestParser\Config();
        $config->setExceptionMessageFactory(new FriendlyExceptionMessageFactory());

        $this->initRequestParser($request, $config);
    }
}

Check it out this example about custom exceptions.

Is It Production Ready?

Absolutely. This library was initially developed at quintly and is extensively used in production since april 2015. Using it at scale in production means there's a a big focus on backwards compatibility and not breaking stuff.

Tests

composer tests
composer tests-coverage

Contributing

Feel free to send pull requests!

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].