Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.56% covered (success)
97.56%
40 / 41
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dispatcher
97.56% covered (success)
97.56%
40 / 41
85.71% covered (warning)
85.71%
6 / 7
25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dispatch
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
 dispatchString
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
8.02
 dispatchArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 dispatchClosure
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getController
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Dispatcher
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Dispatcher;
16
17use phpOMS\Application\ApplicationAbstract;
18use phpOMS\Autoloader;
19use phpOMS\System\File\PathException;
20
21/**
22 * Dispatcher class.
23 *
24 * @package phpOMS\Dispatcher
25 * @license OMS License 2.0
26 * @link    https://jingga.app
27 * @since   1.0.0
28 */
29final class Dispatcher implements DispatcherInterface
30{
31    /**
32     * Application.
33     *
34     * @var null|ApplicationAbstract
35     * @since 1.0.0
36     */
37    private ?ApplicationAbstract $app;
38
39    /**
40     * Controller.
41     *
42     * Set in the module manager on module initialization.
43     *
44     * @var array
45     * @since 1.0.0
46     */
47    private array $controllers = [];
48
49    /**
50     * Constructor.
51     *
52     * @param ApplicationAbstract $app Appliaction
53     *
54     * @since 1.0.0
55     */
56    public function __construct(ApplicationAbstract $app = null)
57    {
58        $this->app = $app;
59    }
60
61    /**
62     * {@inheritdoc}
63     */
64    public function dispatch(array | string | callable $controller, mixed ...$data) : array
65    {
66        $views = [];
67        $data  = \array_values($data);
68
69        if (\is_array($controller) && isset($controller['dest'])) {
70            if (!empty($controller['data'])) {
71                $data = \array_merge(
72                    empty($data) ? [] : $data,
73                    \is_array($controller['data']) ? $controller['data'] : [$controller['data']]
74                );
75            }
76
77            $controller = $controller['dest'];
78        }
79
80        // Php void functions always return null.
81        // In a different language the Api functions would reguire a return type
82        // If null is returned (i.e. void functions) these get ignored later in the response renderer as null is not "rendered"
83        if (\is_string($controller)) {
84            $views += $this->dispatchString($controller, $data);
85        } elseif (\is_array($controller)) {
86            $views += $this->dispatchArray($controller, $data);
87        } else {
88            $views[] = $this->dispatchClosure($controller, $data);
89        }
90
91        return $views;
92    }
93
94    /**
95     * Dispatch string.
96     *
97     * The dispatcher can dispatch static functions.
98     * String: `some/namespace/path::myStaticFunction`
99     *
100     * Additionally it's also possible to dispatch functions of modules.
101     * Modules are classes which can get instantiated with `new Class(ApplicationAbstract $app)`
102     * String: `some/namespace/path:myMethod`
103     *
104     * @param string     $controller Controller string
105     * @param null|array $data       Data
106     *
107     * @return array
108     *
109     * @throws PathException             this exception is thrown if the function cannot be autoloaded
110     * @throws \Exception                this exception is thrown if the function is not callable
111     * @throws \UnexpectedValueException this exception is thrown if the controller string is malformed
112     *
113     * @since 1.0.0
114     */
115    private function dispatchString(string $controller, array $data = null) : array
116    {
117        $views    = [];
118        $dispatch = \explode(':', $controller);
119
120        if (!Autoloader::exists($dispatch[0]) && !isset($this->controllers[$dispatch[0]])) {
121            throw new PathException($dispatch[0]);
122        }
123
124        if (($c = \count($dispatch)) === 3) {
125            /* Handling static functions */
126            $function = $dispatch[0] . '::' . $dispatch[2];
127
128            if (!\is_callable($function)) {
129                throw new \Exception('Endpoint "'. $function .'" is not callable!');
130            }
131
132            $views[$controller] = $data === null ? $function() : $function(...$data);
133        } elseif ($c === 2) {
134            $obj                = $this->getController($dispatch[0]);
135            $views[$controller] = $data === null
136                ? $obj->{$dispatch[1]}()
137                : $obj->{$dispatch[1]}(...$data);
138        } else {
139            throw new \UnexpectedValueException('Unexpected function.');
140        }
141
142        return $views;
143    }
144
145    /**
146     * Dispatch array.
147     *
148     * @param array      $controller Controller string
149     * @param null|array $data       Data
150     *
151     * @return array
152     *
153     * @since 1.0.0
154     */
155    private function dispatchArray(array $controller, array $data = null) : array
156    {
157        $views = [];
158        foreach ($controller as $controllerSingle) {
159            $views += $data === null ? $this->dispatch($controllerSingle) : $this->dispatch($controllerSingle, ...$data);
160        }
161
162        return $views;
163    }
164
165    /**
166     * Dispatch closure.
167     *
168     * @param Callable   $controller Controller string
169     * @param null|array $data       Data
170     *
171     * @return mixed
172     *
173     * @since 1.0.0
174     */
175    private function dispatchClosure(callable $controller, array $data = null) : mixed
176    {
177        return $data === null ? $controller($this->app) : $controller($this->app, ...$data);
178    }
179
180    /**
181     * Dispatch controller.
182     *
183     * @param string $controller Controller
184     *
185     * @return object
186     *
187     * @throws PathException this exception is thrown in case the controller couldn't be found
188     *
189     * @since 1.0.0
190     */
191    private function getController(string $controller) : object
192    {
193        if (!isset($this->controllers[$controller])) {
194            $this->controllers[$controller] = new $controller($this->app);
195        }
196
197        return $this->controllers[$controller];
198    }
199
200    /**
201     * Set controller by alias.
202     *
203     * @param object $controller Controller
204     * @param string $name       Controller string
205     *
206     * @return void
207     *
208     * @since 1.0.0
209     */
210    public function set(object $controller, string $name) : void
211    {
212        $this->controllers[$name] = $controller;
213    }
214}