1<?php
2
3namespace dokuwiki\Remote;
4
5use dokuwiki\Remote\OpenApiDoc\DocBlockMethod;
6use InvalidArgumentException;
7use ReflectionException;
8use ReflectionFunction;
9use ReflectionMethod;
10use RuntimeException;
11
12class ApiCall
13{
14    /** @var callable The method to be called for this endpoint */
15    protected $method;
16
17    /** @var bool Whether this call can be called without authentication */
18    protected bool $isPublic = false;
19
20    /** @var string The category this call belongs to */
21    protected string $category;
22
23    /** @var DocBlockMethod The meta data of this call as parsed from its doc block */
24    protected $docs;
25
26    /**
27     * Make the given method available as an API call
28     *
29     * @param string|array $method Either [object,'method'] or 'function'
30     * @param string $category The category this call belongs to
31     */
32    public function __construct($method, $category = '')
33    {
34        if (!is_callable($method)) {
35            throw new InvalidArgumentException('Method is not callable');
36        }
37
38        $this->method = $method;
39        $this->category = $category;
40    }
41
42    /**
43     * Call the method
44     *
45     * Important: access/authentication checks need to be done before calling this!
46     *
47     * @param array $args
48     * @return mixed
49     */
50    public function __invoke($args)
51    {
52        if (!array_is_list($args)) {
53            $args = $this->namedArgsToPositional($args);
54        }
55        return call_user_func_array($this->method, $args);
56    }
57
58    /**
59     * Access the method documentation
60     *
61     * This lazy loads the docs only when needed
62     *
63     * @return DocBlockMethod
64     */
65    public function getDocs()
66    {
67        if ($this->docs === null) {
68            try {
69                if (is_array($this->method)) {
70                    $reflect = new ReflectionMethod($this->method[0], $this->method[1]);
71                } else {
72                    $reflect = new ReflectionFunction($this->method);
73                }
74                $this->docs = new DocBlockMethod($reflect);
75            } catch (ReflectionException $e) {
76                throw new RuntimeException('Failed to parse API method documentation', 0, $e);
77            }
78        }
79        return $this->docs;
80    }
81
82    /**
83     * Is this a public method?
84     *
85     * Public methods can be called without authentication
86     *
87     * @return bool
88     */
89    public function isPublic()
90    {
91        return $this->isPublic;
92    }
93
94    /**
95     * Set the public flag
96     *
97     * @param bool $isPublic
98     * @return $this
99     */
100    public function setPublic(bool $isPublic = true)
101    {
102        $this->isPublic = $isPublic;
103        return $this;
104    }
105
106    /**
107     * Get information about the argument of this call
108     *
109     * @return array
110     */
111    public function getArgs()
112    {
113        return $this->getDocs()->getParameters();
114    }
115
116    /**
117     * Get information about the return value of this call
118     *
119     * @return array
120     */
121    public function getReturn()
122    {
123        return $this->getDocs()->getReturn();
124    }
125
126    /**
127     * Get the summary of this call
128     *
129     * @return string
130     */
131    public function getSummary()
132    {
133        return $this->getDocs()->getSummary();
134    }
135
136    /**
137     * Get the description of this call
138     *
139     * @return string
140     */
141    public function getDescription()
142    {
143        return $this->getDocs()->getDescription();
144    }
145
146    /**
147     * Get the category of this call
148     *
149     * @return string
150     */
151    public function getCategory()
152    {
153        return $this->category;
154    }
155
156    /**
157     * Converts named arguments to positional arguments
158     *
159     * @fixme with PHP 8 we can use named arguments directly using the spread operator
160     * @param array $params
161     * @return array
162     */
163    protected function namedArgsToPositional($params)
164    {
165        $args = [];
166
167        foreach ($this->getDocs()->getParameters() as $arg => $arginfo) {
168            if (isset($params[$arg])) {
169                $args[] = $params[$arg];
170            } elseif ($arginfo['optional'] && array_key_exists('default', $arginfo)) {
171                $args[] = $arginfo['default'];
172            } else {
173                throw new InvalidArgumentException("Missing argument $arg");
174            }
175        }
176
177        return $args;
178    }
179}
180