Skip to content

Instantly share code, notes, and snippets.

@jdelisle
Last active September 29, 2015 10:21
Show Gist options
  • Save jdelisle/14a230f6a5a9a22de154 to your computer and use it in GitHub Desktop.
Save jdelisle/14a230f6a5a9a22de154 to your computer and use it in GitHub Desktop.
Query string parameters validation for Zend's Apigility resource fetchAll method
<?php
namespace Application\Rest;
use Zend\InputFilter\Input;
use Zend\InputFilter\InputFilter;
use Zend\Stdlib\Parameters;
use ZF\ApiProblem\ApiProblem;
use ZF\Rest\ResourceEvent;
abstract class AbstractResourceListener extends \ZF\Rest\AbstractResourceListener {
/**
* The query params validation config for the resource (must be set by the resource's constructor)
*
* @var array
*/
protected $queryParamsValidation;
/**
* An array of validation messages for invalid input
*
* @var array
*/
private $validationMessages = array();
/**
* Dispatch an incoming event to the appropriate method
*
* Marshals arguments from the event parameters.
*
* Also filters and validates query params prior to a fetchAll event. Returns an ApiProblem if validation fails.
*
* @param ResourceEvent $event
* @return mixed
*/
public function dispatch(ResourceEvent $event)
{
if ($event->getName() == 'fetchAll' && is_array($this->queryParamsValidation)) {
try {
$this->validateQueryParams($event->getQueryParams());
} catch (\InvalidArgumentException $e) {
return new ApiProblem(400, 'Failed Validation', null, null, array('validation_messages' => $this->validationMessages));
}
}
return parent::dispatch($event);
}
/**
* Validate the event's query params and retrieve validation messages on invalid input
*
* @param Parameters $params
* @throws \InvalidArgumentException
*/
private function validateQueryParams(Parameters $params)
{
$inputFilter = $this->buildInputFilter($params);
if ($inputFilter->isValid()) {
// overwrite parameters with valid values
$params->fromArray($inputFilter->getValues());
} else {
foreach ($inputFilter->getInvalidInput() as $error) {
$this->validationMessages[$error->getName()] = $error->getMessages();
}
throw new \InvalidArgumentException();
}
}
/**
* Build an input filter for query params validation
*
* @param Parameters $params
* @return InputFilter
*/
private function buildInputFilter(Parameters $params)
{
$inputFilter = new InputFilter();
$defaultValues = array();
foreach ($this->queryParamsValidation as $param) {
$this->assertKeys($param, array('name', 'default_value', 'filters', 'validators'));
$defaultValues[$param['name']] = $param['default_value'];
$input = new Input($param['name']);
$input->setRequired(false);
foreach ($param['filters'] as $filter) {
$this->assertKeys($filter, array('name', 'options'));
$input->getFilterChain()->attach(new $filter['name']($filter['options']));
}
foreach ($param['validators'] as $validator) {
$this->assertKeys($validator, array('name', 'options'));
$input->getValidatorChain()->attach(new $validator['name']($validator['options']));
}
$inputFilter->add($input);
}
$inputFilter->setData(array_merge($defaultValues, (array) $params));
return $inputFilter;
}
/**
* Assert that needed array config keys are set
*
* @param array $array
* @param array $keys
* @throws \Exception
*/
private function assertKeys(array $array, array $keys)
{
$missingKeys = array_diff($keys, array_keys($array));
if ($missingKeys) {
$missingKeys = join(', ', $missingKeys);
throw new \Exception("Missing config data [{$missingKeys}] for query params validation");
}
}
}
<?php
return array(
'input_filter_specs' => array(
'Application\\V1\\Rest\\User\\QueryParamsValidator' => array(
0 => array(
'name' => 'sort_key',
'default_value' => null,
'filters' => array(),
'validators' => array(
0 => array(
'name' => 'Zend\\Validator\\InArray',
'options' => array(
'haystack' => array(
0 => 'first_name',
1 => 'last_name',
2 => 'email',
),
),
),
),
),
1 => array(
'name' => 'sort_order',
'default_value' => null,
'filters' => array(),
'validators' => array(
0 => array(
'name' => 'Zend\\Validator\\InArray',
'options' => array(
'haystack' => array(
0 => 'asc',
1 => 'desc',
),
),
),
),
),
2 => array(
'name' => 'active',
'default_value' => false,
'filters' => array(
0 => array(
'name' => 'Zend\\Filter\\Boolean',
'options' => array(
'type' => 511,
),
),
),
'validators' => array(),
),
),
),
);
<?php
namespace Application\V1\Rest\User;
use Application\Rest\AbstractResourceListener;
class UserResource extends AbstractResourceListener
{
/**
* @param array $queryParamsValidation
*/
public function __construct(array $queryParamsValidation){
$this->queryParamsValidation = $queryParamsValidation;
}
/**
* Fetch all or a subset of resources
*
* @param array $params
* @return ApiProblem|mixed
*/
public function fetchAll($params = array())
{
// Use your validated/filtered params here
// TODO: Implement fetchAll() method.
}
}
<?php
namespace Application\V1\Rest\User;
class UserResourceFactory
{
public function __invoke($services)
{
if (!isset($services->get('config')['input_filter_specs']['Application\V1\Rest\User\QueryParamsValidator'])) {
throw new \Exception("Expected query params validation config for UserResource couldn't be found");
}
return new UserResource(
$services->get('config')['input_filter_specs']['Application\V1\Rest\User\QueryParamsValidator']
);
}
}
@metanav
Copy link

metanav commented Sep 29, 2015

Can we do it without injecting in the UserResource class. I mean right after route event?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment