I'm writing an API class, and my general goal is for it to be easy to make any class's methods accessible via the API, without having to make any serious changes to the class itself. Essentially, I should be able to instantiate an API class instance on any class that I want to use (within my little framework), and have it just work.
For example, In my API class, I have a method call
, that I want to use $_GET
to call the correct function from the class that I want to make accessible (let's call it Beep
). So I specify an action
parameter in my API, so that the action
is the method of Beep
to call, with the remaining arguments in $_GET
being, presumably, the arguments开发者_StackOverflow for the method. In API->call
, I can do $BeepInstance->$_GET['action']()
, but I have no way of determining which arguments from $_GET to send, and in what order to send them.
func_get_args
will only return the list of given arguments for the function in which it is called, and I don't necessarily know the correct order in which to pass them with call_user_func_array
.
Has anyone tried to do something similar to this?
Here's a solution + example that uses reflection to map your input arguments to method parameters. I also added a way to control which methods are exposed to make it more secure.
class Dispatch {
private $apis;
public function registerAPI($api, $name, $exposedActions) {
$this->apis[$name] = array(
'api' => $api,
'exposedActions' => $exposedActions
);
}
public function handleRequest($apiName, $action, $arguments) {
if (isset($this->apis[$apiName])) {
$api = $this->apis[$apiName]['api'];
// check that the action is exposed
if (in_array($action, $this->apis[$apiName]['exposedActions'])) {
// execute action
// get method reflection & parameters
$reflection = new ReflectionClass($api);
$method = $reflection->getMethod($action);
// map $arguments to $orderedArguments for the function
$orderedArguments = array();
foreach ($method->getParameters() as $parameter) {
if (array_key_exists($parameter->name, $arguments)) {
$orderedArguments[] = $arguments[$parameter->name];
} else if ($parameter->isOptional()) {
$orderedArguments[] = $parameter->getDefaultValue();
} else {
throw new InvalidArgumentException("Parameter {$parameter->name} is required");
}
}
// call method with ordered arguments
return call_user_func_array(array($api, $action), $orderedArguments);
} else {
throw new InvalidArgumentException("Action {$action} is not exposed");
}
} else {
throw new InvalidArgumentException("API {$apiName} is not registered");
}
}
}
class Beep {
public function doBeep($tone = 15000)
{
echo 'beep at ' . $tone;
}
public function notExposedInAPI()
{
// do secret stuff
}
}
Example:
// dispatch.php?api=beep&action=doBeep&tone=20000
$beep = new Beep();
$dispatch = new Dispatch();
$dispatch->registerAPI($beep, 'beep', array('doBeep'));
$dispatch->handleRequest($_GET['api'], $_GET['action'], $_GET);
We did something similar in our API. We used a proxy method _methodName($p) and passed in the $_GET or $_REQUEST array. The proxy method knows the order of the parameters required for the real method, so it invokes the real method correctly. Using call_user_func_array() worked pretty well with that.
Not sure if that's the best way to go about it, but it works well for us.
The controller looks something like this:
if (method_exists($server, "_$method"))
$resp = call_user_func_array("{$server}::_$method", array($_REQUEST));
And then the model is setup like:
public function test($arg1, $arg2) { ... }
public function _test($p) {
return $this->test($p['arg1'], $p['arg2']);
}
I'd propose to pass an associative array the the respective method. Since the assoc. array provides a name to value mapping.
Moreover, never do something like this:
$BeepInstance->$_GET['action']()
This is highly insecure.
Probably define another associate array, which maps actions passed as GET 'action' parameters to actual method names.
精彩评论