'string', 'description' => '', ]; /** @var string The summary of the method */ protected string $summary = ''; /** @var string The description of the method */ protected string $description = ''; /** * Make the given method available as an API call * * @param string|array $method Either [object,'method'] or 'function' * @throws \ReflectionException */ public function __construct($method) { if (!is_callable($method)) { throw new \InvalidArgumentException('Method is not callable'); } $this->method = $method; $this->parseData(); } /** * Call the method * * Important: access/authentication checks need to be done before calling this! * * @param array $args * @return mixed */ public function __invoke($args) { if (!array_is_list($args)) { $args = $this->namedArgsToPositional($args); } return call_user_func_array($this->method, $args); } /** * @return bool */ public function isPublic(): bool { return $this->isPublic; } /** * @param bool $isPublic * @return $this */ public function setPublic(bool $isPublic = true): self { $this->isPublic = $isPublic; return $this; } /** * @return array */ public function getArgs(): array { return $this->args; } /** * Limit the arguments to the given ones * * @param string[] $args * @return $this */ public function limitArgs($args): self { foreach ($args as $arg) { if (!isset($this->args[$arg])) { throw new \InvalidArgumentException("Unknown argument $arg"); } } $this->args = array_intersect_key($this->args, array_flip($args)); return $this; } /** * Set the description for an argument * * @param string $arg * @param string $description * @return $this */ public function setArgDescription(string $arg, string $description): self { if (!isset($this->args[$arg])) { throw new \InvalidArgumentException('Unknown argument'); } $this->args[$arg]['description'] = $description; return $this; } /** * @return array */ public function getReturn(): array { return $this->return; } /** * Set the description for the return value * * @param string $description * @return $this */ public function setReturnDescription(string $description): self { $this->return['description'] = $description; return $this; } /** * @return string */ public function getSummary(): string { return $this->summary; } /** * @param string $summary * @return $this */ public function setSummary(string $summary): self { $this->summary = $summary; return $this; } /** * @return string */ public function getDescription(): string { return $this->description; } /** * @param string $description * @return $this */ public function setDescription(string $description): self { $this->description = $description; return $this; } /** * Fill in the metadata * * This uses Reflection to inspect the method signature and doc block * * @throws \ReflectionException */ protected function parseData() { if (is_array($this->method)) { $reflect = new \ReflectionMethod($this->method[0], $this->method[1]); } else { $reflect = new \ReflectionFunction($this->method); } $docInfo = $this->parseDocBlock($reflect->getDocComment()); $this->summary = $docInfo['summary']; $this->description = $docInfo['description']; foreach ($reflect->getParameters() as $parameter) { $name = $parameter->name; $realType = $parameter->getType(); if ($realType) { $type = $realType->getName(); } elseif (isset($docInfo['args'][$name]['type'])) { $type = $docInfo['args'][$name]['type']; } else { $type = 'string'; } if (isset($docInfo['args'][$name]['description'])) { $description = $docInfo['args'][$name]['description']; } else { $description = ''; } $this->args[$name] = [ 'type' => $type, 'description' => trim($description), ]; } $returnType = $reflect->getReturnType(); if ($returnType) { $this->return['type'] = $returnType->getName(); } elseif (isset($docInfo['return']['type'])) { $this->return['type'] = $docInfo['return']['type']; } else { $this->return['type'] = 'string'; } if (isset($docInfo['return']['description'])) { $this->return['description'] = $docInfo['return']['description']; } } /** * Parse a doc block * * @param string $doc * @return array */ protected function parseDocBlock($doc) { // strip asterisks and leading spaces $doc = preg_replace( ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'], ['', '', '', ''], $doc ); $doc = trim($doc); // get all tags $tags = []; if (preg_match_all('/^@(\w+)\s+(.*)$/m', $doc, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $tags[$match[1]][] = trim($match[2]); } } $params = $this->extractDocTags($tags); // strip the tags from the doc $doc = preg_replace('/^@(\w+)\s+(.*)$/m', '', $doc); [$summary, $description] = sexplode("\n\n", $doc, 2, ''); return array_merge( [ 'summary' => trim($summary), 'description' => trim($description), 'tags' => $tags, ], $params ); } /** * Process the param and return tags * * @param array $tags * @return array */ protected function extractDocTags(&$tags) { $result = []; if (isset($tags['param'])) { foreach ($tags['param'] as $param) { if (preg_match('/^(\w+)\s+\$(\w+)(\s+(.*))?$/m', $param, $m)) { $result['args'][$m[2]] = [ 'type' => $this->cleanTypeHint($m[1]), 'description' => trim($m[3] ?? ''), ]; } } unset($tags['param']); } if (isset($tags['return'])) { $return = $tags['return'][0]; if (preg_match('/^(\w+)(\s+(.*))$/m', $return, $m)) { $result['return'] = [ 'type' => $this->cleanTypeHint($m[1]), 'description' => trim($m[2] ?? '') ]; } unset($tags['return']); } return $result; } /** * Matches the given type hint against the valid options for the remote API * * @param string $hint * @return string */ protected function cleanTypeHint($hint) { $types = explode('|', $hint); foreach ($types as $t) { if (str_ends_with($t, '[]')) { return 'array'; } if ($t === 'boolean' || $t === 'true' || $t === 'false') { return 'bool'; } if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) { return $t; } } return 'string'; } /** * Converts named arguments to positional arguments * * @fixme with PHP 8 we can use named arguments directly using the spread operator * @param array $params * @return array */ protected function namedArgsToPositional($params) { $args = []; foreach (array_keys($this->args) as $arg) { if (isset($params[$arg])) { $args[] = $params[$arg]; } else { $args[] = null; } } return $args; } }