Skip to content

Commit 8576ba1

Browse files
author
Guy Elsmore-Paddock
committed
[#169] Support Filters Being Bound to Different Stages of Processing
Makes it possible for a filter definition to include a new `stage` key that can be set to `before_validation`, `after_validation`, `request_wire`, or `response_wire`; which binds the filter to fire off either before validation (the default), after validation, before being written to the wire in a request, or after being read from the wire from a response, respectively. Still needs tests.
1 parent d14ba44 commit 8576ba1

13 files changed

+307
-65
lines changed

src/Handler/ValidatedDescriptionHandler.php

+24-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use GuzzleHttp\Command\CommandInterface;
44
use GuzzleHttp\Command\Exception\CommandException;
55
use GuzzleHttp\Command\Guzzle\DescriptionInterface;
6+
use GuzzleHttp\Command\Guzzle\Parameter;
67
use GuzzleHttp\Command\Guzzle\SchemaValidator;
78

89
/**
@@ -43,17 +44,30 @@ public function __invoke(callable $handler)
4344
foreach ($operation->getParams() as $name => $schema) {
4445
$value = $command[$name];
4546

46-
if ($value) {
47-
$value = $schema->filter($value);
48-
}
47+
$preValidationValue = $schema->filter(
48+
$value,
49+
Parameter::FILTER_STAGE_BEFORE_VALIDATION
50+
);
51+
52+
if (!$this->validator->validate($schema, $preValidationValue)) {
53+
$errors =
54+
array_merge($errors, $this->validator->getErrors());
55+
} else {
56+
$postValidationValue = $schema->filter(
57+
$preValidationValue,
58+
Parameter::FILTER_STAGE_AFTER_VALIDATION
59+
);
4960

50-
if (! $this->validator->validate($schema, $value)) {
51-
$errors = array_merge($errors, $this->validator->getErrors());
52-
} elseif ($value !== $command[$name]) {
53-
// Update the config value if it changed and no validation errors were encountered.
54-
// This happen when the user extending an operation
55-
// See https://github.com/guzzle/guzzle-services/issues/145
56-
$command[$name] = $value;
61+
if ($postValidationValue !== $command[$name]) {
62+
// Update the parameter value if it has changed and no
63+
// validation errors were encountered. This ensures the
64+
// parameter has a value even when the user is extending
65+
// an operation.
66+
//
67+
// See:
68+
// https://github.com/guzzle/guzzle-services/issues/145
69+
$command[$name] = $postValidationValue;
70+
}
5771
}
5872
}
5973

src/Parameter.php

+208-29
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,43 @@
88
*/
99
class Parameter implements ToArrayInterface
1010
{
11+
/**
12+
* The name of the filter stage that happens before a parameter is
13+
* validated, for filtering raw data (e.g. clean-up before validation).
14+
*/
15+
const FILTER_STAGE_BEFORE_VALIDATION = 'before_validation';
16+
17+
/**
18+
* The name of the filter stage that happens immediately after a parameter
19+
* has been validated but before it is evaluated by location handlers to be
20+
* written out on the wire.
21+
*/
22+
const FILTER_STAGE_AFTER_VALIDATION = 'after_validation';
23+
24+
/**
25+
* The name of the filter stage that happens right before a validated value
26+
* is being written out "on the wire" (e.g. for adjusting the structure or
27+
* format of the data before sending it to the server).
28+
*/
29+
const FILTER_STAGE_REQUEST_WIRE = 'request_wire';
30+
31+
/**
32+
* The name of the filter stage that happens right after a value has been
33+
* read out of a response "on the wire" (e.g. for adjusting the structure or
34+
* format of the data after receiving it back from the server).
35+
*/
36+
const FILTER_STAGE_RESPONSE_WIRE = 'response_wire';
37+
38+
/**
39+
* A list of all allowed filter stages.
40+
*/
41+
const FILTER_STAGES = [
42+
self::FILTER_STAGE_BEFORE_VALIDATION,
43+
self::FILTER_STAGE_AFTER_VALIDATION,
44+
self::FILTER_STAGE_REQUEST_WIRE,
45+
self::FILTER_STAGE_RESPONSE_WIRE
46+
];
47+
1148
private $originalData;
1249

1350
/** @var string $name */
@@ -117,14 +154,23 @@ class Parameter implements ToArrayInterface
117154
* full class path to a static method or an array of complex filter
118155
* information. You can specify static methods of classes using the full
119156
* namespace class name followed by '::' (e.g. Foo\Bar::baz). Some
120-
* filters require arguments in order to properly filter a value. For
121-
* complex filters, use a hash containing a 'method' key pointing to a
122-
* static method, and an 'args' key containing an array of positional
123-
* arguments to pass to the method. Arguments can contain keywords that
124-
* are replaced when filtering a value: '@value' is replaced with the
125-
* value being validated, '@api' is replaced with the Parameter object.
157+
* filters require arguments in order to properly filter a value.
126158
*
127-
* - properties: When the type is an object, you can specify nested parameters
159+
* For complex filters, use a hash containing a 'method' key pointing to a
160+
* static method, an 'args' key containing an array of positional
161+
* arguments to pass to the method, and an optional 'stage' key. Arguments
162+
* can contain keywords that are replaced when filtering a value: '@value'
163+
* is replaced with the value being validated, '@api' is replaced with the
164+
* Parameter object, and '@stage' is replaced with the current filter
165+
* stage (if any was provided).
166+
*
167+
* The optional 'stage' key can be provided to control when the filter is
168+
* invoked. The key can indicate that a filter should only be invoked
169+
* 'before_validation', 'after_validation', when being written out to the
170+
* 'request_wire' or being read from the 'response_wire'.
171+
*
172+
* - properties: When the type is an object, you can specify nested
173+
* parameters
128174
*
129175
* - additionalProperties: (array) This attribute defines a schema for all
130176
* properties that are not explicitly defined in an object type
@@ -250,14 +296,30 @@ public function getValue($value)
250296
* parameter.
251297
*
252298
* @param mixed $value Value to filter
299+
* @param string $stage An optional specifier of what filter stage to
300+
* invoke. If null, then all filters are invoked no matter what stage
301+
* they apply to. Otherwise, only filters for the specified stage are
302+
* invoked.
253303
*
254304
* @return mixed Returns the filtered value
255305
* @throws \RuntimeException when trying to format when no service
256306
* description is available.
257-
*/
258-
public function filter($value)
259-
{
260-
// Formats are applied exclusively and supersed filters
307+
* @throws \InvalidArgumentException if an invalid validation stage is
308+
* provided.
309+
*/
310+
public function filter($value, $stage = null)
311+
{
312+
if (($stage !== null) && !in_array($stage, self::FILTER_STAGES)) {
313+
throw new \InvalidArgumentException(
314+
sprintf(
315+
'$stage must be one of [%s], but was given "%s"',
316+
implode(', ', self::FILTER_STAGES),
317+
$stage
318+
)
319+
);
320+
}
321+
322+
// Formats are applied exclusively and supercede filters
261323
if ($this->format) {
262324
if (!$this->serviceDescription) {
263325
throw new \RuntimeException('No service description was set so '
@@ -273,24 +335,7 @@ public function filter($value)
273335

274336
// Apply filters to the value
275337
if ($this->filters) {
276-
foreach ($this->filters as $filter) {
277-
if (is_array($filter)) {
278-
// Convert complex filters that hold value place holders
279-
foreach ($filter['args'] as &$data) {
280-
if ($data == '@value') {
281-
$data = $value;
282-
} elseif ($data == '@api') {
283-
$data = $this;
284-
}
285-
}
286-
$value = call_user_func_array(
287-
$filter['method'],
288-
$filter['args']
289-
);
290-
} else {
291-
$value = call_user_func($filter, $value);
292-
}
293-
}
338+
$value = $this->invokeCustomFilters($value, $stage);
294339
}
295340

296341
return $value;
@@ -628,6 +673,17 @@ private function addFilter($filter)
628673
'A [method] value must be specified for each complex filter'
629674
);
630675
}
676+
677+
if (isset($filter['stage'])
678+
&& !in_array($filter['stage'], self::FILTER_STAGES)) {
679+
throw new \InvalidArgumentException(
680+
sprintf(
681+
'[stage] value must be one of [%s], but was given "%s"',
682+
implode(', ', self::FILTER_STAGES),
683+
$filter['stage']
684+
)
685+
);
686+
}
631687
}
632688

633689
if (!$this->filters) {
@@ -652,4 +708,127 @@ public function has($var)
652708
}
653709
return isset($this->{$var}) && !empty($this->{$var});
654710
}
711+
712+
/**
713+
* Filters the given data using filter methods specified in the config.
714+
*
715+
* If $stage is provided, only filters that apply to the provided filter
716+
* stage will be invoked. To preserve legacy behavior, filters that do not
717+
* specify a stage are implicitly invoked only in the pre-validation stage.
718+
*
719+
* @param mixed $value The value to filter.
720+
* @param string $stage An optional specifier of what filter stage to
721+
* invoke. If null, then all filters are invoked no matter what stage
722+
* they apply to. Otherwise, only filters for the specified stage are
723+
* invoked.
724+
*
725+
* @return mixed The filtered value.
726+
*/
727+
private function invokeCustomFilters($value, $stage) {
728+
$filteredValue = $value;
729+
730+
foreach ($this->filters as $filter) {
731+
if (is_array($filter)) {
732+
$filteredValue =
733+
$this->invokeComplexFilter($filter, $value, $stage);
734+
} else {
735+
$filteredValue =
736+
$this->invokeSimpleFilter($filter, $value, $stage);
737+
}
738+
}
739+
740+
return $filteredValue;
741+
}
742+
743+
/**
744+
* Invokes a filter that uses value substitution and/or should only be
745+
* invoked for a particular filter stage.
746+
*
747+
* If $stage is provided, and the filter specifies a stage, it is not
748+
* invoked unless $stage matches the stage the filter indicates it applies
749+
* to. If the filter is not invoked, $value is returned exactly as it was
750+
* provided to this method.
751+
*
752+
* To preserve legacy behavior, if the filter does not specify a stage, it
753+
* is implicitly invoked only in the pre-validation stage.
754+
*
755+
* @param array $filter Information about the filter to invoke.
756+
* @param mixed $value The value to filter.
757+
* @param string $stage An optional specifier of what filter stage to
758+
* invoke. If null, then the filter is invoked no matter what stage it
759+
* indicates it applies to. Otherwise, the filter is only invoked if it
760+
* matches the specified stage.
761+
*
762+
* @return mixed The filtered value.
763+
*/
764+
private function invokeComplexFilter(array $filter, $value, $stage) {
765+
if (isset($filter['stage'])) {
766+
$filterStage = $filter['stage'];
767+
} else {
768+
$filterStage = self::FILTER_STAGE_AFTER_VALIDATION;
769+
}
770+
771+
if (($stage === null) || ($filterStage == $stage)) {
772+
// Convert complex filters that hold value place holders
773+
$filterArgs =
774+
$this->expandFilterArgs($filter['args'], $value, $stage);
775+
776+
$filteredValue =
777+
call_user_func_array($filter['method'], $filterArgs);
778+
} else {
779+
$filteredValue = $value;
780+
}
781+
782+
return $filteredValue;
783+
}
784+
785+
/**
786+
* Replaces any placeholders in filter arguments with values from the
787+
* current context.
788+
*
789+
* @param array $filterArgs The array of arguments to pass to the filter
790+
* function. Some of the elements of this array are expected to be
791+
* placeholders that will be replaced by this function.
792+
*
793+
* @return array The array of arguments, with all placeholders replaced.
794+
*/
795+
function expandFilterArgs(array $filterArgs, $value, $stage) {
796+
$replacements = [
797+
'@value' => $value,
798+
'@api' => $this,
799+
'@stage' => $stage,
800+
];
801+
802+
foreach ($filterArgs as &$argValue) {
803+
if (isset($replacements[$argValue])) {
804+
$argValue = $replacements[$argValue];
805+
}
806+
}
807+
808+
return $filterArgs;
809+
}
810+
811+
/**
812+
* Invokes a filter only provides a function or method name to invoke,
813+
* without additional parameters.
814+
*
815+
* If $stage is provided, the filter is not invoked unless we are in the
816+
* pre-validation stage, to preserve legacy behavior.
817+
*
818+
* @param array $filter Information about the filter to invoke.
819+
* @param mixed $value The value to filter.
820+
* @param string $stage An optional specifier of what filter stage to
821+
* invoke. If null, then the filter is invoked no matter what.
822+
* Otherwise, the filter is only invoked if the value is
823+
* FILTER_STAGE_AFTER_VALIDATION.
824+
*
825+
* @return mixed The filtered value.
826+
*/
827+
private function invokeSimpleFilter($filter, $value, $stage) {
828+
if ($stage === self::FILTER_STAGE_AFTER_VALIDATION) {
829+
return $value;
830+
} else {
831+
return call_user_func($filter, $value);
832+
}
833+
}
655834
}

src/RequestLocation/AbstractLocation.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ protected function prepareValue($value, Parameter $param)
6262
{
6363
return is_array($value)
6464
? $this->resolveRecursively($value, $param)
65-
: $param->filter($value);
65+
: $param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);
6666
}
6767

6868
/**
@@ -96,6 +96,6 @@ protected function resolveRecursively(array $value, Parameter $param)
9696
}
9797
}
9898

99-
return $param->filter($value);
99+
return $param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);
100100
}
101101
}

src/RequestLocation/BodyLocation.php

+10-5
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,20 @@ public function visit(
3535
RequestInterface $request,
3636
Parameter $param
3737
) {
38-
$oldValue = $request->getBody()->getContents();
38+
$existingResponse = $request->getBody()->getContents();
3939

4040
$value = $command[$param->getName()];
41-
$value = $param->getName() . '=' . $param->filter($value);
41+
$filteredValue =
42+
$param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);
4243

43-
if ($oldValue !== '') {
44-
$value = $oldValue . '&' . $value;
44+
$valueForResponse = sprintf('%s=%s', $param->getName(), $filteredValue);
45+
46+
if ($existingResponse == '') {
47+
$response = $valueForResponse;
48+
} else {
49+
$response = $existingResponse . '&' . $valueForResponse;
4550
}
4651

47-
return $request->withBody(Psr7\stream_for($value));
52+
return $request->withBody(Psr7\stream_for($response));
4853
}
4954
}

src/RequestLocation/HeaderLocation.php

+9-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ public function visit(
3636
Parameter $param
3737
) {
3838
$value = $command[$param->getName()];
39+
$filteredValue =
40+
$param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);
3941

40-
return $request->withHeader($param->getWireName(), $param->filter($value));
42+
return $request->withHeader($param->getWireName(), $filteredValue);
4143
}
4244

4345
/**
@@ -57,7 +59,12 @@ public function after(
5759
if ($additional && ($additional->getLocation() === $this->locationName)) {
5860
foreach ($command->toArray() as $key => $value) {
5961
if (!$operation->hasParam($key)) {
60-
$request = $request->withHeader($key, $additional->filter($value));
62+
$filteredValue = $additional->filter(
63+
$value,
64+
Parameter::FILTER_STAGE_REQUEST_WIRE
65+
);
66+
67+
$request = $request->withHeader($key, $filteredValue);
6168
}
6269
}
6370
}

0 commit comments

Comments
 (0)