3 namespace dokuwiki\Remote
;
8 /** @var callable The method to be called for this endpoint */
11 /** @var bool Whether this call can be called without authentication */
12 protected bool $isPublic = false;
14 /** @var array Metadata on the accepted parameters */
15 protected array $args = [];
17 /** @var array Metadata on the return value */
18 protected array $return = [
23 /** @var string The summary of the method */
24 protected string $summary = '';
26 /** @var string The description of the method */
27 protected string $description = '';
30 * Make the given method available as an API call
32 * @param string|array $method Either [object,'method'] or 'function'
33 * @throws \ReflectionException
35 public function __construct($method)
37 if (!is_callable($method)) {
38 throw new \
InvalidArgumentException('Method is not callable');
41 $this->method
= $method;
48 * Important: access/authentication checks need to be done before calling this!
53 public function __invoke($args)
55 if (!array_is_list($args)) {
56 $args = $this->namedArgsToPositional($args);
58 return call_user_func_array($this->method
, $args);
64 public function isPublic(): bool
66 return $this->isPublic
;
70 * @param bool $isPublic
73 public function setPublic(bool $isPublic = true): self
75 $this->isPublic
= $isPublic;
83 public function getArgs(): array
89 * Limit the arguments to the given ones
91 * @param string[] $args
94 public function limitArgs($args): self
96 foreach ($args as $arg) {
97 if (!isset($this->args
[$arg])) {
98 throw new \
InvalidArgumentException("Unknown argument $arg");
101 $this->args
= array_intersect_key($this->args
, array_flip($args));
107 * Set the description for an argument
110 * @param string $description
113 public function setArgDescription(string $arg, string $description): self
115 if (!isset($this->args
[$arg])) {
116 throw new \
InvalidArgumentException('Unknown argument');
118 $this->args
[$arg]['description'] = $description;
125 public function getReturn(): array
127 return $this->return;
131 * Set the description for the return value
133 * @param string $description
136 public function setReturnDescription(string $description): self
138 $this->return['description'] = $description;
145 public function getSummary(): string
147 return $this->summary
;
151 * @param string $summary
154 public function setSummary(string $summary): self
156 $this->summary
= $summary;
163 public function getDescription(): string
165 return $this->description
;
169 * @param string $description
172 public function setDescription(string $description): self
174 $this->description
= $description;
179 * Fill in the metadata
181 * This uses Reflection to inspect the method signature and doc block
183 * @throws \ReflectionException
185 protected function parseData()
187 if (is_array($this->method
)) {
188 $reflect = new \
ReflectionMethod($this->method
[0], $this->method
[1]);
190 $reflect = new \
ReflectionFunction($this->method
);
193 $docInfo = $this->parseDocBlock($reflect->getDocComment());
194 $this->summary
= $docInfo['summary'];
195 $this->description
= $docInfo['description'];
197 foreach ($reflect->getParameters() as $parameter) {
198 $name = $parameter->name
;
199 $realType = $parameter->getType();
201 $type = $realType->getName();
202 } elseif (isset($docInfo['args'][$name]['type'])) {
203 $type = $docInfo['args'][$name]['type'];
208 if (isset($docInfo['args'][$name]['description'])) {
209 $description = $docInfo['args'][$name]['description'];
214 $this->args
[$name] = [
216 'description' => trim($description),
220 $returnType = $reflect->getReturnType();
222 $this->return['type'] = $returnType->getName();
223 } elseif (isset($docInfo['return']['type'])) {
224 $this->return['type'] = $docInfo['return']['type'];
226 $this->return['type'] = 'string';
229 if (isset($docInfo['return']['description'])) {
230 $this->return['description'] = $docInfo['return']['description'];
240 protected function parseDocBlock($doc)
242 // strip asterisks and leading spaces
244 ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'],
253 if (preg_match_all('/^@(\w+)\s+(.*)$/m', $doc, $matches, PREG_SET_ORDER
)) {
254 foreach ($matches as $match) {
255 $tags[$match[1]][] = trim($match[2]);
258 $params = $this->extractDocTags($tags);
260 // strip the tags from the doc
261 $doc = preg_replace('/^@(\w+)\s+(.*)$/m', '', $doc);
263 [$summary, $description] = sexplode("\n\n", $doc, 2, '');
266 'summary' => trim($summary),
267 'description' => trim($description),
275 * Process the param and return tags
280 protected function extractDocTags(&$tags)
284 if (isset($tags['param'])) {
285 foreach ($tags['param'] as $param) {
286 if (preg_match('/^(\w+)\s+\$(\w+)(\s+(.*))?$/m', $param, $m)) {
287 $result['args'][$m[2]] = [
288 'type' => $this->cleanTypeHint($m[1]),
289 'description' => trim($m[3] ??
''),
293 unset($tags['param']);
297 if (isset($tags['return'])) {
298 $return = $tags['return'][0];
299 if (preg_match('/^(\w+)(\s+(.*))?$/m', $return, $m)) {
300 $result['return'] = [
301 'type' => $this->cleanTypeHint($m[1]),
302 'description' => trim($m[2] ??
'')
305 unset($tags['return']);
312 * Matches the given type hint against the valid options for the remote API
314 * @param string $hint
317 protected function cleanTypeHint($hint)
319 $types = explode('|', $hint);
320 foreach ($types as $t) {
321 if (str_ends_with($t, '[]')) {
324 if ($t === 'boolean' ||
$t === 'true' ||
$t === 'false') {
327 if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) {
335 * Converts named arguments to positional arguments
337 * @fixme with PHP 8 we can use named arguments directly using the spread operator
338 * @param array $params
341 protected function namedArgsToPositional($params)
345 foreach (array_keys($this->args
) as $arg) {
346 if (isset($params[$arg])) {
347 $args[] = $params[$arg];