Fix first set of API tests
[dokuwiki.git] / inc / Remote / ApiCall.php
blob3d5f192a606358bd4924ab4cbe8f0ba391ad5f98
1 <?php
3 namespace dokuwiki\Remote;
6 class ApiCall
8 /** @var callable The method to be called for this endpoint */
9 protected $method;
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 = [
19 'type' => 'string',
20 'description' => '',
23 /** @var string The summary of the method */
24 protected string $summary = '';
26 /** @var string The description of the method */
27 protected string $description = '';
29 /**
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;
42 $this->parseData();
45 /**
46 * Call the method
48 * Important: access/authentication checks need to be done before calling this!
50 * @param array $args
51 * @return mixed
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);
61 /**
62 * @return bool
64 public function isPublic(): bool
66 return $this->isPublic;
69 /**
70 * @param bool $isPublic
71 * @return $this
73 public function setPublic(bool $isPublic = true): self
75 $this->isPublic = $isPublic;
76 return $this;
80 /**
81 * @return array
83 public function getArgs(): array
85 return $this->args;
88 /**
89 * Limit the arguments to the given ones
91 * @param string[] $args
92 * @return $this
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));
103 return $this;
107 * Set the description for an argument
109 * @param string $arg
110 * @param string $description
111 * @return $this
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;
119 return $this;
123 * @return array
125 public function getReturn(): array
127 return $this->return;
131 * Set the description for the return value
133 * @param string $description
134 * @return $this
136 public function setReturnDescription(string $description): self
138 $this->return['description'] = $description;
139 return $this;
143 * @return string
145 public function getSummary(): string
147 return $this->summary;
151 * @param string $summary
152 * @return $this
154 public function setSummary(string $summary): self
156 $this->summary = $summary;
157 return $this;
161 * @return string
163 public function getDescription(): string
165 return $this->description;
169 * @param string $description
170 * @return $this
172 public function setDescription(string $description): self
174 $this->description = $description;
175 return $this;
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]);
189 } else {
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();
200 if ($realType) {
201 $type = $realType->getName();
202 } elseif (isset($docInfo['args'][$name]['type'])) {
203 $type = $docInfo['args'][$name]['type'];
204 } else {
205 $type = 'string';
208 if (isset($docInfo['args'][$name]['description'])) {
209 $description = $docInfo['args'][$name]['description'];
210 } else {
211 $description = '';
214 $this->args[$name] = [
215 'type' => $type,
216 'description' => trim($description),
220 $returnType = $reflect->getReturnType();
221 if ($returnType) {
222 $this->return['type'] = $returnType->getName();
223 } elseif (isset($docInfo['return']['type'])) {
224 $this->return['type'] = $docInfo['return']['type'];
225 } else {
226 $this->return['type'] = 'string';
229 if (isset($docInfo['return']['description'])) {
230 $this->return['description'] = $docInfo['return']['description'];
235 * Parse a doc block
237 * @param string $doc
238 * @return array
240 protected function parseDocBlock($doc)
242 // strip asterisks and leading spaces
243 $doc = preg_replace(
244 ['/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'],
245 ['', '', '', ''],
246 $doc
249 $doc = trim($doc);
251 // get all tags
252 $tags = [];
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, '');
264 return array_merge(
266 'summary' => trim($summary),
267 'description' => trim($description),
268 'tags' => $tags,
270 $params
275 * Process the param and return tags
277 * @param array $tags
278 * @return array
280 protected function extractDocTags(&$tags)
282 $result = [];
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']);
308 return $result;
312 * Matches the given type hint against the valid options for the remote API
314 * @param string $hint
315 * @return string
317 protected function cleanTypeHint($hint)
319 $types = explode('|', $hint);
320 foreach ($types as $t) {
321 if (str_ends_with($t, '[]')) {
322 return 'array';
324 if ($t === 'boolean' || $t === 'true' || $t === 'false') {
325 return 'bool';
327 if (in_array($t, ['array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'])) {
328 return $t;
331 return 'string';
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
339 * @return array
341 protected function namedArgsToPositional($params)
343 $args = [];
345 foreach (array_keys($this->args) as $arg) {
346 if (isset($params[$arg])) {
347 $args[] = $params[$arg];
348 } else {
349 $args[] = null;
353 return $args;