├── CHANGES.md ├── phpunit.php ├── tests ├── FakeExtended.php ├── FakeClassWithMethod.php ├── FakeInvokableClass.php ├── FakeInvokable.php ├── FakeBase.php ├── InvokeClosureTraitTest.php ├── InvokeMethodTraitTest.php └── DispatcherTest.php ├── phpunit.xml.dist ├── .scrutinizer.yml ├── .travis.yml ├── CONTRIBUTING.md ├── src ├── Exception.php ├── Exception │ ├── MethodNotSpecified.php │ ├── ObjectNotDefined.php │ ├── MethodNotDefined.php │ ├── ObjectNotSpecified.php │ ├── ParamNotSpecified.php │ └── MethodNotAccessible.php ├── InvokeClosureTrait.php ├── InvokeMethodTrait.php ├── DispatcherInterface.php └── Dispatcher.php ├── composer.json ├── autoload.php ├── LICENSE └── README.md /CHANGES.md: -------------------------------------------------------------------------------- 1 | Hygiene release: update license year, and remove branch alias. 2 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/FakeClassWithMethod.php: -------------------------------------------------------------------------------- 1 | . 4 | 5 | The time between submitting a contribution and its review one may be extensive; do not be discouraged if there is not immediate feedback. 6 | 7 | Thanks! 8 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | invokeMethod($this, $method, $params); 11 | } 12 | 13 | public function publicMethod($foo, $bar, $baz = 'baz') 14 | { 15 | return "$foo $bar $baz"; 16 | } 17 | 18 | protected function protectedMethod($foo, $bar, $baz = 'baz') 19 | { 20 | return "$foo $bar $baz"; 21 | } 22 | 23 | private function privateMethod($foo, $bar, $baz = 'baz') 24 | { 25 | return "$foo $bar $baz"; 26 | } 27 | 28 | public function directParams(array $params) 29 | { 30 | return "{$params['foo']} {$params['bar']} {$params['baz']}"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aura/dispatcher", 3 | "type": "library", 4 | "description": "Creates objects from a factory and invokes methods using named parameters; also provides a trait for invoking closures and object methods with named parameters.", 5 | "keywords": [ 6 | "controller", 7 | "dispatcher", 8 | "dispatcher", 9 | "factory" 10 | ], 11 | "homepage": "https://github.com/auraphp/Aura.Dispatcher", 12 | "license": "BSD-2-Clause", 13 | "authors": [ 14 | { 15 | "name": "Aura.Dispatcher Contributors", 16 | "homepage": "https://github.com/auraphp/Aura.Dispatcher/contributors" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=5.4.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Aura\\Dispatcher\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "aura": { 29 | "type": "library" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | array( 10 | __DIR__ . '/src', 11 | __DIR__ . '/tests', 12 | ), 13 | ); 14 | 15 | // go through the prefixes 16 | foreach ($prefixes as $prefix => $dirs) { 17 | 18 | // does the requested class match the namespace prefix? 19 | $prefix_len = strlen($prefix); 20 | if (substr($class, 0, $prefix_len) !== $prefix) { 21 | continue; 22 | } 23 | 24 | // strip the prefix off the class 25 | $class = substr($class, $prefix_len); 26 | 27 | // a partial filename 28 | $part = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; 29 | 30 | // go through the directories to find classes 31 | foreach ($dirs as $dir) { 32 | $dir = str_replace('/', DIRECTORY_SEPARATOR, $dir); 33 | $file = $dir . DIRECTORY_SEPARATOR . $part; 34 | if (is_readable($file)) { 35 | require $file; 36 | return; 37 | } 38 | } 39 | } 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2016, Aura for PHP 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /tests/InvokeClosureTraitTest.php: -------------------------------------------------------------------------------- 1 | invokeClosure($closure, [ 15 | 'foo' => 'FOO', 16 | 'bar' => 'BAR', 17 | ]); 18 | $this->assertSame($expect, $actual); 19 | } 20 | 21 | public function testInvokeClosure_positionalParams() 22 | { 23 | $closure = function ($foo, $bar, $baz = 'baz') { 24 | return "$foo $bar $baz"; 25 | }; 26 | $expect = 'FOO BAR baz'; 27 | $actual = $this->invokeClosure($closure, [ 28 | 0 => 'FOO', 29 | 1 => 'BAR', 30 | ]); 31 | $this->assertSame($expect, $actual); 32 | } 33 | 34 | public function testInvokeClosure_directParams() 35 | { 36 | $closure = function (array $params) { 37 | return "{$params['foo']} {$params['bar']} {$params['baz']}"; 38 | }; 39 | 40 | $params = [ 41 | 'foo' => 'foo', 42 | 'bar' => 'bar', 43 | 'baz' => 'baz', 44 | ]; 45 | $params['params'] =& $params; 46 | 47 | $expect = 'foo bar baz'; 48 | $actual = $this->invokeClosure($closure, $params); 49 | $this->assertSame($expect, $actual); 50 | } 51 | 52 | public function testInvokeClosure_paramNotSpecified() 53 | { 54 | $closure = function ($foo, $bar, $baz = 'baz') { 55 | return "$foo $bar $baz"; 56 | }; 57 | 58 | $this->setExpectedException( 59 | 'Aura\Dispatcher\Exception\ParamNotSpecified', 60 | 'Closure(1 : $bar)' 61 | ); 62 | 63 | $this->invokeClosure($closure, [ 64 | 'foo' => 'foo', 65 | 'baz' => 'baz', 66 | ]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/InvokeClosureTrait.php: -------------------------------------------------------------------------------- 1 | getParameters() as $i => $param) { 47 | if (isset($params[$param->name])) { 48 | // a named param value is available 49 | $args[] = $params[$param->name]; 50 | } elseif (isset($params[$i])) { 51 | // a positional param value is available 52 | $args[] = $params[$i]; 53 | } elseif ($param->isDefaultValueAvailable()) { 54 | // use the default value 55 | $args[] = $param->getDefaultValue(); 56 | } else { 57 | // no default value and no matching param 58 | $message = "Closure($i : \${$param->name})"; 59 | throw new Exception\ParamNotSpecified($message); 60 | } 61 | } 62 | 63 | // invoke with the args, and done 64 | return $reflect->invokeArgs($args); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/InvokeMethodTrait.php: -------------------------------------------------------------------------------- 1 | isProtected()) { 54 | $access = 'protected'; 55 | $accessible = ($object instanceof $this); 56 | } elseif ($reflect->isPrivate()) { 57 | $access = 'private'; 58 | $declaring_class = $reflect->getDeclaringClass()->getName(); 59 | $accessible = ($declaring_class == get_class($this)); 60 | } 61 | 62 | // is the method accessible by $this? 63 | if (! $accessible) { 64 | $message = get_class($object) . "::$method is $access"; 65 | throw new Exception\MethodNotAccessible($message); 66 | } 67 | 68 | // the method is accessible by $this 69 | $reflect->setAccessible(true); 70 | 71 | // sequential arguments when invoking 72 | $args = []; 73 | 74 | // match params with arguments 75 | foreach ($reflect->getParameters() as $i => $param) { 76 | if (isset($params[$param->name])) { 77 | // a named param value is available 78 | $args[] = $params[$param->name]; 79 | } elseif (isset($params[$i])) { 80 | // a positional param value is available 81 | $args[] = $params[$i]; 82 | } elseif ($param->isDefaultValueAvailable()) { 83 | // use the default value 84 | $args[] = $param->getDefaultValue(); 85 | } else { 86 | // no default value and no matching param 87 | $message = get_class($object) . '::' . $method 88 | . "($i : \${$param->name})"; 89 | throw new Exception\ParamNotSpecified($message); 90 | } 91 | } 92 | 93 | // invoke with the args, and done 94 | return $reflect->invokeArgs($object, $args); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/DispatcherInterface.php: -------------------------------------------------------------------------------- 1 | setExpectedException('Aura\Dispatcher\Exception\MethodNotDefined'); 12 | $object->exec('noSuchMethod'); 13 | } 14 | 15 | public function testInvokeMethod_public() 16 | { 17 | // works on base object 18 | $object = new FakeBase; 19 | $expect = 'FOO BAR baz'; 20 | $actual = $object->exec( 21 | 'publicMethod', 22 | [ 23 | 'foo' => 'FOO', 24 | 'bar' => 'BAR', 25 | ] 26 | ); 27 | $this->assertSame($expect, $actual); 28 | 29 | // works on extended object 30 | $object = new FakeExtended; 31 | $expect = 'FOO BAR baz'; 32 | $actual = $object->exec( 33 | 'publicMethod', 34 | [ 35 | 'foo' => 'FOO', 36 | 'bar' => 'BAR', 37 | ] 38 | ); 39 | $this->assertSame($expect, $actual); 40 | } 41 | 42 | public function testInvokeMethod_protected() 43 | { 44 | // works on base object 45 | $object = new FakeBase; 46 | $expect = 'FOO BAR baz'; 47 | $actual = $object->exec( 48 | 'protectedMethod', 49 | [ 50 | 'foo' => 'FOO', 51 | 'bar' => 'BAR', 52 | ] 53 | ); 54 | $this->assertSame($expect, $actual); 55 | 56 | // works on extended object 57 | $object = new FakeExtended; 58 | $expect = 'FOO BAR baz'; 59 | $actual = $object->exec( 60 | 'protectedMethod', 61 | [ 62 | 'foo' => 'FOO', 63 | 'bar' => 'BAR', 64 | ] 65 | ); 66 | $this->assertSame($expect, $actual); 67 | 68 | // fails on external call 69 | $object = new FakeExtended; 70 | $expect = 'FOO BAR baz'; 71 | $this->setExpectedException('Aura\Dispatcher\Exception\MethodNotAccessible'); 72 | $actual = $this->invokeMethod( 73 | $object, 74 | 'protectedMethod', 75 | [ 76 | 'foo' => 'FOO', 77 | 'bar' => 'BAR', 78 | ] 79 | ); 80 | } 81 | 82 | public function testInvokeMethod_private() 83 | { 84 | // works on base object 85 | $object = new FakeBase; 86 | $expect = 'FOO BAR baz'; 87 | $actual = $object->exec( 88 | 'privateMethod', 89 | [ 90 | 'foo' => 'FOO', 91 | 'bar' => 'BAR', 92 | ] 93 | ); 94 | $this->assertSame($expect, $actual); 95 | 96 | // fails on extended object 97 | $object = new FakeExtended; 98 | $expect = 'FOO BAR baz'; 99 | $this->setExpectedException('Aura\Dispatcher\Exception\MethodNotAccessible'); 100 | $actual = $object->exec( 101 | 'privateMethod', 102 | [ 103 | 'foo' => 'FOO', 104 | 'bar' => 'BAR', 105 | ] 106 | ); 107 | } 108 | 109 | public function testInvokeMethod_positionalParams() 110 | { 111 | $object = new FakeBase; 112 | $expect = 'FOO BAR baz'; 113 | $actual = $object->exec( 114 | 'publicMethod', 115 | [ 116 | 0 => 'FOO', 117 | 1 => 'BAR', 118 | ] 119 | ); 120 | $this->assertSame($expect, $actual); 121 | } 122 | 123 | public function testInvokeMethod_directParams() 124 | { 125 | $object = new FakeBase; 126 | 127 | $params = [ 128 | 'foo' => 'foo', 129 | 'bar' => 'bar', 130 | 'baz' => 'baz', 131 | ]; 132 | $params['params'] =& $params; 133 | 134 | $expect = 'foo bar baz'; 135 | $actual = $object->exec('directParams', $params); 136 | $this->assertSame($expect, $actual); 137 | } 138 | 139 | public function testInvokeMethod_paramNotSpecified() 140 | { 141 | $object = new FakeBase; 142 | $this->setExpectedException( 143 | 'Aura\Dispatcher\Exception\ParamNotSpecified', 144 | 'Aura\Dispatcher\FakeBase::publicMethod(1 : $bar)' 145 | ); 146 | $object->exec( 147 | 'publicMethod', 148 | [ 149 | 'foo' => 'foo', 150 | 'baz' => 'baz', 151 | ] 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /tests/DispatcherTest.php: -------------------------------------------------------------------------------- 1 | objects = [ 13 | 'factory' => function () { 14 | return new FakeBase; 15 | }, 16 | 'closure' => function ($foo, $bar, $baz = 'baz') { 17 | return "$foo $bar $baz"; 18 | }, 19 | 'invokable' => function () { 20 | return new FakeInvokable; 21 | }, 22 | 'methodandinvoke' => function () { 23 | return new FakeClassWithMethod; 24 | } 25 | ]; 26 | 27 | $this->dispatcher = new Dispatcher( 28 | $this->objects, 29 | 'controller', 30 | 'action' 31 | ); 32 | } 33 | 34 | public function testGetSetHasEtc() 35 | { 36 | $foo = function () { 37 | return new FakeBase; 38 | }; 39 | 40 | $this->assertFalse($this->dispatcher->hasObject('foo')); 41 | 42 | $this->dispatcher->setObject('foo', $foo); 43 | $this->assertTrue($this->dispatcher->hasObject('foo')); 44 | 45 | $actual = $this->dispatcher->getObject('foo'); 46 | $this->assertInstanceOf('Closure', $actual); 47 | 48 | $actual = $this->dispatcher->getObjects(); 49 | $expect = array_merge($this->objects, ['foo' => $foo]); 50 | $this->assertSame($expect, $actual); 51 | 52 | $bar = function () { 53 | return new FakeExtended; 54 | }; 55 | 56 | $this->dispatcher->addObjects(['bar' => $bar]); 57 | $actual = $this->dispatcher->getObjects(); 58 | $expect = array_merge($this->objects, [ 59 | 'foo' => $foo, 60 | 'bar' => $bar, 61 | ]); 62 | $this->assertSame($expect, $actual); 63 | 64 | $this->setExpectedException('Aura\Dispatcher\Exception\ObjectNotDefined'); 65 | $this->dispatcher->getObject('NoSuchCallable'); 66 | } 67 | 68 | public function testParams() 69 | { 70 | $this->dispatcher->setObjectParam('foo'); 71 | $actual = $this->dispatcher->getObjectParam(); 72 | $this->assertSame('foo', $actual); 73 | 74 | $this->dispatcher->setMethodParam('bar'); 75 | $actual = $this->dispatcher->getMethodParam(); 76 | $this->assertSame('bar', $actual); 77 | } 78 | 79 | public function testDispatch_objectNotSpecified() 80 | { 81 | $params = []; 82 | $this->setExpectedException('Aura\Dispatcher\Exception\ObjectNotSpecified'); 83 | $this->dispatcher->__invoke($params); 84 | } 85 | 86 | public function testDispatch_objectNotDefined() 87 | { 88 | $params = ['controller' => 'undefined_object']; 89 | $this->setExpectedException('Aura\Dispatcher\Exception\ObjectNotDefined'); 90 | $this->dispatcher->__invoke($params); 91 | } 92 | 93 | public function testDispatch_factory() 94 | { 95 | $params = [ 96 | 'controller' => 'factory', 97 | 'action' => 'publicMethod', 98 | 'foo' => 'FOO', 99 | 'bar' => 'BAR', 100 | ]; 101 | $actual = $this->dispatcher->__invoke($params); 102 | $expect = 'FOO BAR baz'; 103 | $this->assertSame($expect, $actual); 104 | } 105 | 106 | 107 | public function testDispatch_factoryInParams() 108 | { 109 | $params = [ 110 | 'controller' => function () { 111 | return new FakeBase; 112 | }, 113 | 'action' => 'publicMethod', 114 | 'foo' => 'FOO', 115 | 'bar' => 'BAR', 116 | ]; 117 | $actual = $this->dispatcher->__invoke($params); 118 | $expect = 'FOO BAR baz'; 119 | $this->assertSame($expect, $actual); 120 | } 121 | 122 | public function testDispatch_closure() 123 | { 124 | $params = [ 125 | 'controller' => 'closure', 126 | 'foo' => 'FOO', 127 | 'bar' => 'BAR', 128 | ]; 129 | $actual = $this->dispatcher->__invoke($params); 130 | $expect = 'FOO BAR baz'; 131 | $this->assertSame($expect, $actual); 132 | } 133 | 134 | public function testDispatch_closureInParams() 135 | { 136 | $params = [ 137 | 'controller' => function ($foo, $bar, $baz = 'baz') { 138 | return "$foo $bar $baz"; 139 | }, 140 | 'foo' => 'FOO', 141 | 'bar' => 'BAR', 142 | ]; 143 | $actual = $this->dispatcher->__invoke($params); 144 | $expect = 'FOO BAR baz'; 145 | $this->assertSame($expect, $actual); 146 | } 147 | 148 | public function testDispatch_invokableObject() 149 | { 150 | $params = [ 151 | 'controller' => 'invokable', 152 | 'foo' => 'FOO', 153 | 'bar' => 'BAR', 154 | ]; 155 | $actual = $this->dispatcher->__invoke($params); 156 | $expect = 'FOO BAR baz'; 157 | $this->assertSame($expect, $actual); 158 | } 159 | 160 | public function testDispatch_namedObject() 161 | { 162 | $params = [ 163 | 'foo' => 'FOO', 164 | 'bar' => 'BAR', 165 | ]; 166 | $actual = $this->dispatcher->__invoke($params, 'invokable'); 167 | $expect = 'FOO BAR baz'; 168 | $this->assertSame($expect, $actual); 169 | } 170 | 171 | public function testMethodExistsInvokable() 172 | { 173 | $params = [ 174 | 'action' => 'someAction', 175 | 'controller' => 'methodandinvoke' 176 | ]; 177 | 178 | $actual = $this->dispatcher->__invoke($params); 179 | $expect = 'Hello World!'; 180 | $this->assertSame($expect, $actual); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | setObjects($objects); 71 | $this->setObjectParam($object_param); 72 | $this->setMethodParam($method_param); 73 | } 74 | 75 | /** 76 | * 77 | * Uses the params to get a dispatchable object, then dispatches it using 78 | * the params. 79 | * 80 | * @param array|ArrayAccess $params Params for the invocation. 81 | * 82 | * @param string $object_name Use an explicit object name instead of 83 | * getting it from the params. 84 | * 85 | * @return mixed The return from the invoked object. 86 | * 87 | */ 88 | public function __invoke($params = [], $object_name = null) 89 | { 90 | if ($object_name) { 91 | $object = $this->getObject($object_name); 92 | } else { 93 | $object = $this->getObjectByParams($params); 94 | } 95 | return $this->dispatch($object, $params); 96 | } 97 | 98 | /** 99 | * 100 | * Dispatches to the object with the params; if the result is an object 101 | * with the dispatchable method, a closure, or an invokable object, 102 | * recursively to that result with the same params. 103 | * 104 | * @param mixed $object Dispatch to this object. 105 | * 106 | * @param array|ArrayAccess $params Params for the invocation. 107 | * 108 | * @return The first non-dispatchable result. 109 | * 110 | */ 111 | protected function dispatch($object, $params = []) 112 | { 113 | $method = $this->getMethodByParams($params); 114 | if (is_callable([$object, $method]) && method_exists($object, $method)) { 115 | // the object has the specified method 116 | $result = $this->invokeMethod($object, $method, $params); 117 | } elseif ($object instanceof Closure) { 118 | // the object is a closure proper 119 | $result = $this->invokeClosure($object, $params); 120 | } elseif (is_object($object) && is_callable($object)) { 121 | // the object is invokable 122 | $result = $this->invokeMethod($object, '__invoke', $params); 123 | } else { 124 | // cannot dispatch any further; end recursion and return as-is 125 | return $object; 126 | } 127 | 128 | // recursively dispatch the result. 129 | return $this->dispatch($result, $params); 130 | } 131 | 132 | /** 133 | * 134 | * Sets the parameter indicating the dispatchable object name. 135 | * 136 | * @param string $object_param The parameter name to use. 137 | * 138 | * @return null 139 | * 140 | */ 141 | public function setObjectParam($object_param) 142 | { 143 | $this->object_param = $object_param; 144 | } 145 | 146 | /** 147 | * 148 | * Gets the parameter indicating the dispatchable object name. 149 | * 150 | * @return string 151 | * 152 | */ 153 | public function getObjectParam() 154 | { 155 | return $this->object_param; 156 | } 157 | 158 | /** 159 | * 160 | * Sets the parameter indicating the method to call on the created object. 161 | * 162 | * @param string $method_param The parameter name to use. 163 | * 164 | * @return null 165 | * 166 | */ 167 | public function setMethodParam($method_param) 168 | { 169 | $this->method_param = $method_param; 170 | } 171 | 172 | /** 173 | * 174 | * Gets the parameter indicating the method to call on the created object. 175 | * 176 | * @return string 177 | * 178 | */ 179 | public function getMethodParam() 180 | { 181 | return $this->method_param; 182 | } 183 | 184 | /** 185 | * 186 | * Set the array of dispatchable objects; this clears all existing objects. 187 | * 188 | * @param array $objects An array where the key is a name and the value 189 | * is a dispatchable object. 190 | * 191 | * @return null 192 | * 193 | */ 194 | public function setObjects(array $objects) 195 | { 196 | $this->objects = $objects; 197 | } 198 | 199 | /** 200 | * 201 | * Adds to the array of dispatchable objects; this merges with existing 202 | * objects. 203 | * 204 | * @param array $objects An array where the key is a name and the value 205 | * is a dispatchable object. 206 | * 207 | * @return null 208 | * 209 | */ 210 | public function addObjects(array $objects) 211 | { 212 | $this->objects = array_merge($this->objects, $objects); 213 | } 214 | 215 | /** 216 | * 217 | * Returns the array of dispatchable objects. 218 | * 219 | * @return array 220 | * 221 | */ 222 | public function getObjects() 223 | { 224 | return $this->objects; 225 | } 226 | 227 | /** 228 | * 229 | * Sets a dispatchable object by name. 230 | * 231 | * @param string $name The name. 232 | * 233 | * @param object $object The dispatchable object. 234 | * 235 | */ 236 | public function setObject($name, $object) 237 | { 238 | $this->objects[$name] = $object; 239 | } 240 | 241 | /** 242 | * 243 | * Does a dispatchable object exist? 244 | * 245 | * @param string $name The name of the dispatchable object. 246 | * 247 | * @return bool 248 | * 249 | */ 250 | public function hasObject($name) 251 | { 252 | return isset($this->objects[$name]); 253 | } 254 | 255 | /** 256 | * 257 | * Returns a dispatchable object using its name. 258 | * 259 | * @param string $name The name of the dispatchable object. 260 | * 261 | * @return object 262 | * 263 | */ 264 | public function getObject($name) 265 | { 266 | if ($this->hasObject($name)) { 267 | return $this->objects[$name]; 268 | } 269 | 270 | throw new Exception\ObjectNotDefined($name); 271 | } 272 | 273 | /** 274 | * 275 | * Returns a dispatchable object using an array of params; if the 276 | * `$object_param` is an object, it is returned directly, otherwise it is 277 | * treated as a dispatchable object name. 278 | * 279 | * @param array|ArrayAccess $params Params for the invocation. 280 | * 281 | * @return object The dispatchable object. 282 | * 283 | */ 284 | public function getObjectByParams($params) 285 | { 286 | // do we have an object param available? 287 | $key = $this->getObjectParam(); 288 | if (! isset($params[$key])) { 289 | throw new Exception\ObjectNotSpecified; 290 | } 291 | 292 | // is the object param value already an object? 293 | $value = $params[$key]; 294 | if (is_object($value)) { 295 | return $value; 296 | } 297 | 298 | // get the dispatchable object by name 299 | if ($this->hasObject($value)) { 300 | return $this->objects[$value]; 301 | } 302 | 303 | // could not find the dispatchable object by name 304 | throw new Exception\ObjectNotDefined($value); 305 | } 306 | 307 | /** 308 | * 309 | * Gets the method from the params. 310 | * 311 | * @param array|ArrayAccess $params Params for the invocation. 312 | * 313 | * @return mixed 314 | * 315 | */ 316 | public function getMethodByParams($params) 317 | { 318 | if ($this->method_param && isset($params[$this->method_param])) { 319 | return $params[$this->method_param]; 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aura.Dispatcher 2 | 3 | Provides tools to map arbitrary names to dispatchable objects, then to 4 | dispatch to those objects using named parameters. This is useful for invoking 5 | controller and command objects based on path-info parameters or command line 6 | arguments, for dispatching to closure-based controllers, and for building 7 | dispatchable objects from factories. 8 | 9 | ## Foreword 10 | 11 | ### Installation 12 | 13 | This library requires PHP 5.4 or later; we recommend using the latest available version of PHP as a matter of principle. It has no userland dependencies. 14 | 15 | It is installable and autoloadable via Composer as [aura/dispatcher](https://packagist.org/packages/aura/dispatcher). 16 | 17 | Alternatively, [download a release](https://github.com/auraphp/Aura.Dispatcher/releases) or clone this repository, then require or include its _autoload.php_ file. 18 | 19 | ### Quality 20 | 21 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/auraphp/Aura.Dispatcher/badges/quality-score.png?b=develop-2)](https://scrutinizer-ci.com/g/auraphp/Aura.Dispatcher/) 22 | [![Code Coverage](https://scrutinizer-ci.com/g/auraphp/Aura.Dispatcher/badges/coverage.png?b=develop-2)](https://scrutinizer-ci.com/g/auraphp/Aura.Dispatcher/) 23 | [![Build Status](https://travis-ci.org/auraphp/Aura.Dispatcher.png?branch=develop-2)](https://travis-ci.org/auraphp/Aura.Dispatcher) 24 | 25 | To run the unit tests at the command line, issue `phpunit` at the package root. (This requires [PHPUnit][] to be available as `phpunit`.) 26 | 27 | [PHPUnit]: http://phpunit.de/manual/ 28 | 29 | This library attempts to comply with [PSR-1][], [PSR-2][], and [PSR-4][]. If 30 | you notice compliance oversights, please send a patch via pull request. 31 | 32 | [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 33 | [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 34 | [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md 35 | 36 | ### Community 37 | 38 | To ask questions, provide feedback, or otherwise communicate with the Aura community, please join our [Google Group](http://groups.google.com/group/auraphp), follow [@auraphp on Twitter](http://twitter.com/auraphp), or chat with us on #auraphp on Freenode. 39 | 40 | 41 | ## Getting Started 42 | 43 | ### Overview 44 | 45 | First, an external routing mechanism such as [Aura.Router][] or a 46 | micro-framework router creates an array of parameters. (Alternatively, the 47 | parameters may be an object that implements [ArrayAccess][]). 48 | 49 | [Aura.Router]: https://github.com/auraphp/Aura.Router 50 | [ArrayAccess]: http://php.net/ArrayAccess 51 | 52 | The parameters are then passed to the _Dispatcher_. It examines them and picks 53 | an object to invoke with those parameters, optionally with a method determined 54 | by the parameters. 55 | 56 | The _Dispatcher_ then examines the returned result from that first invocation. 57 | If the result is itself a dispatchable object, the _Dispatcher_ will 58 | recursively dispatch the result until something other than a dispatchable 59 | object is returned. 60 | 61 | When a non-dispatchable result is returned, the _Dispatcher_ stops recursion 62 | and returns the non-dispatchable result. 63 | 64 | ### Closures and Invokable Objects 65 | 66 | First, we tell the _Dispatcher_ to examine the `controller` parameter to find 67 | the name of the object to dispatch to: 68 | 69 | ```php 70 | setObjectParam('controller'); 75 | ?> 76 | ``` 77 | 78 | Next, we set a closure object into the _Dispatcher_ using `setObject()`: 79 | 80 | ```php 81 | setObject('blog', function ($id) { 83 | return "Read blog entry $id"; 84 | }); 85 | ?> 86 | ``` 87 | 88 | We can now dispatch to that closure by using the name as the value for 89 | the `controller` parameter: 90 | 91 | ```php 92 | 'blog', 95 | 'id' => 88, 96 | ]; 97 | 98 | $result = $dispatcher($params); // or call __invoke() directly 99 | echo $result; // "Read blog entry 88" 100 | ?> 101 | ``` 102 | 103 | The same goes for invokable objects. First, define a class with an 104 | `__invoke()` method: 105 | 106 | ```php 107 | 116 | ``` 117 | 118 | Next, set an instance of the object into the _Dispatcher_: 119 | 120 | ```php 121 | setObject('blog', new InvokableBlog); 123 | ?> 124 | ``` 125 | 126 | Finally, dispatch to the invokable object (the parameters and logic are 127 | the same as above): 128 | 129 | ```php 130 | 'blog', 133 | 'id' => 88, 134 | ]; 135 | 136 | $result = $dispatcher($params); // or call __invoke() directly 137 | echo $result; // "Read blog entry 88" 138 | ?> 139 | ``` 140 | 141 | ### Object Method 142 | 143 | We can tell the _Dispatcher_ to examine the params for a method to call on the 144 | object. This method will take precedence over the `__invoke()` method on an 145 | object, if such a method exists. 146 | 147 | First, tell the _Dispatcher_ to examine the value of the `action` param to 148 | find the name of the method it should invoke. 149 | 150 | ```php 151 | setMethodParam('action'); 153 | ?> 154 | ``` 155 | 156 | Next, define the object we will dispatch to; note that the method is `read()` 157 | instead of `__invoke()`. 158 | 159 | ```php 160 | 169 | ``` 170 | 171 | Then, we set the object into the _Dispatcher_ ... 172 | 173 | ```php 174 | setObject('blog', new Blog); 176 | ?> 177 | ``` 178 | 179 | ... and finally, we invoke the _Dispatcher_; we have added an `action` 180 | parameter with the name of the method to invoke: 181 | 182 | ```php 183 | 'blog', 186 | 'action' => 'read', 187 | 'id' => 88, 188 | ]; 189 | 190 | $result = $dispatcher($params); // or call __invoke() directly 191 | echo $result; // "Read blog entry 88" 192 | ?> 193 | ``` 194 | 195 | ### Embedding Objects in Parameters 196 | 197 | If you like, you can place dispatchable objects directly in the parameters. 198 | (This is often how micro-framework routers work.) For example, let's put a 199 | closure into the `controller` parameter; when we invoke the _Dispatcher_, it 200 | will invoke that closure. 201 | 202 | ```php 203 | function ($id) { 206 | return "Read blog entry $id"; 207 | }, 208 | 'id' => 88, 209 | ]; 210 | 211 | $result = $dispatcher($params); // or call __invoke() directly 212 | echo $result; // "Read blog entry 88" 213 | ?> 214 | ``` 215 | 216 | The same is true for invokable objects ... 217 | 218 | ```php 219 | new InvokableBlog, 222 | 'id' => 88, 223 | ]; 224 | 225 | $result = $dispatcher($params); // or call __invoke() directly 226 | echo $result; // "Read blog entry 88" 227 | ?> 228 | ``` 229 | 230 | ... and for object-methods: 231 | 232 | 233 | ```php 234 | new Blog, 237 | 'action' => 'read', 238 | 'id' => 88, 239 | ]; 240 | 241 | $result = $dispatcher($params); // or call __invoke() directly 242 | echo $result; // "Read blog entry 88" 243 | ?> 244 | ``` 245 | 246 | 247 | ### Recursion and Lazy Loading 248 | 249 | The _Dispatcher_ is recursive. After dispatching to the first object, if that 250 | object returns a dispatchable object, the _Dispatcher_ will re-dispatch to 251 | that object. It will continue doing this until the returned result is not a 252 | dispatchable object. 253 | 254 | Let's turn the above example of an invokable object in the _Dispatcher_ into a 255 | lazy-loaded instantiation. All we have to do is wrap the instantiation in 256 | another dispatchable object (in this example, a closure). The benefit of this 257 | is that we can fill the _Dispatcher_ with as many objects as we like, and they 258 | won't get instantiated until the _Dispatcher_ calls on them. 259 | 260 | ```php 261 | setObject('blog', function () { 263 | return new Blog; 264 | }); 265 | ?> 266 | ``` 267 | 268 | Then we invoke the dispatcher with the same params as before. 269 | 270 | ```php 271 | 'blog', 274 | 'action' => 'read', 275 | 'id' => 88, 276 | ]; 277 | 278 | $result = $dispatcher($params); // or call __invoke() directly 279 | echo $result; // "Read blog entry 88" 280 | ?> 281 | ``` 282 | 283 | What happens is this: 284 | 285 | - The _Dispatcher_ finds the 'blog' dispatchable object, sees that it 286 | is a closure, and invokes it with the params. 287 | 288 | - The _Dispatcher_ examines the result, sees the result is a dispatchable 289 | object, and invokes it with the params. 290 | 291 | - The _Dispatcher_ examines *that* result, sees that it is *not* a callable 292 | object, and returns the result. 293 | 294 | 295 | ## Sending The Array Of Params Directly 296 | 297 | Sometimes you will want to send the entire array of parameters directly to the 298 | object method or closure, as opposed to matching parameter keys with function 299 | argument names. To do so, name a key in the parameters array for the argument 300 | name that will receive them, and then set the parameters array into itself 301 | using that name. If may be easier to do this by reference, or by copy, 302 | depending on your needs. 303 | 304 | ```php 305 | setObject('blog', function ($params) { 309 | return "Read blog entry {$params['id']}" 310 | }); 311 | 312 | // the initial params 313 | $params = [ 314 | 'controller' => 'blog', 315 | 'action' => 'read', 316 | 'id' => 88, 317 | ]; 318 | 319 | // set a params reference into itself; this corresponds with the 320 | // 'params' closure argument 321 | $params['params'] =& $params; 322 | 323 | // dispatch 324 | $result = $dispatcher($params); // or call __invoke() directly 325 | echo $result; // "Read blog entry 88" 326 | ?> 327 | ``` 328 | 329 | ## Refactoring To Architecture Changes 330 | 331 | The _Dispatcher_ is built with the idea that some developers may begin with a 332 | micro-framework architecture, and evolve over time toward a full-stack 333 | architecture. 334 | 335 | At first, the developer uses closures embedded in the params: 336 | 337 | ```php 338 | setObjectParam('controller'); 340 | 341 | $params = [ 342 | 'controller' => function ($id) { 343 | return "Read blog entry $id"; 344 | }, 345 | 'id' => 88, 346 | ]; 347 | 348 | $result = $dispatcher($params); // or call __invoke() directly 349 | echo $result; // "Read blog entry 88" 350 | ?> 351 | ``` 352 | 353 | After adding several controllers, the developer is likely to want to keep the 354 | routing configurations separate from the controller actions. At this point the 355 | developer may start putting the controller actions in the _Dispatcher_: 356 | 357 | ```php 358 | setObject('blog', function ($id) { 360 | return "Read blog entry $id!"; 361 | }); 362 | 363 | $params = [ 364 | 'controller' => 'blog', 365 | 'id' => 88, 366 | ]; 367 | 368 | $result = $dispatcher($params); // or call __invoke() directly 369 | echo $result; // "Read blog entry 88" 370 | ?> 371 | ``` 372 | 373 | As the number and complexity of controllers continues to grow, the developer 374 | may wish to put the controllers into their own classes, lazy-loading along the 375 | way: 376 | 377 | ```php 378 | setObject('blog', function () { 388 | return new Blog; 389 | }); 390 | 391 | $params = [ 392 | 'controller' => 'blog', 393 | 'id' => 88, 394 | ]; 395 | 396 | $result = $dispatcher($params); // or call __invoke() directly 397 | echo $result; // "Read blog entry 88" 398 | ?> 399 | ``` 400 | 401 | Finally, the developer may collect several actions into a single controller, 402 | keeping related functionality in the same class. At this point the developer 403 | should call `setMethodParam()` to tell the _Dispatcher_ where to find the 404 | method to invoke on the dispatchable object. 405 | 406 | ```php 407 | setMethodParam('action'); 437 | 438 | $dispatcher->setObject('blog', function () { 439 | return new Blog; 440 | }); 441 | 442 | $params = [ 443 | 'controller' => 'blog', 444 | 'action' => 'read', 445 | 'id' => 88, 446 | ]; 447 | 448 | $result = $dispatcher($params); // or call __invoke() directly 449 | echo $result; // "Read blog entry 88" 450 | ?> 451 | ``` 452 | 453 | ## Construction-Based Configuration 454 | 455 | You can set all dispatchable objects, along with the object parameter name and 456 | the method parameter name, at construction time. This makes it easier to 457 | configure the _Dispatcher_ object in a single call. 458 | 459 | ```php 460 | function () { 465 | return new BlogController; 466 | }, 467 | 'wiki' => function () { 468 | return new WikiController; 469 | }, 470 | 'forum' => function () { 471 | return new ForumController; 472 | }, 473 | ]; 474 | 475 | $dispatcher = new Dispatcher($objects, $object_param, $method_param); 476 | ?> 477 | ``` 478 | 479 | ## Intercessory Dispatch Methods 480 | 481 | Sometimes your classes will have an intercessory method that picks an action 482 | to run, either on itself or on another object. This package provides an 483 | _InvokeMethodTrait_ to invoke a method on an object using named parameters. 484 | (The _InvokeMethodTrait_ honors protected and private scopes.) 485 | 486 | ```php 487 | invokeMethod($this, $method, $params); 499 | } 500 | 501 | protected function actionRead($id = null) 502 | { 503 | return "Read blog entry $id"; 504 | } 505 | } 506 | ?> 507 | ``` 508 | 509 | You can then dispatch to the object as normal, and it will determine its own 510 | logical flow. 511 | 512 | ```php 513 | setObject('blog', function () { 515 | return new Blog; 516 | }); 517 | 518 | $params = [ 519 | 'controller' => 'blog', 520 | 'action' => 'read', 521 | 'id' => 88, 522 | ]; 523 | 524 | $result = $dispatcher($params); // or call __invoke() directly 525 | echo $result; // "Read blog entry 88" 526 | ?> 527 | ``` 528 | --------------------------------------------------------------------------------