Skip to content

Instantly share code, notes, and snippets.

@sagebind
Last active August 29, 2015 14:10
Show Gist options
  • Save sagebind/dce90f77a4c8d190fe26 to your computer and use it in GitHub Desktop.
Save sagebind/dce90f77a4c8d190fe26 to your computer and use it in GitHub Desktop.
Delegate
<?php
/*
* Copyright 2014 Stephen Coakley <me@stephencoakley.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
namespace Gyre;
/**
* Represents a delegate, which holds a reference to a static class method,
* instance method, function or closure for dynamic invocation.
*/
final class Delegate
{
/**
* A closure that invokes the method or function being delegated.
* @type \Closure
*/
private $closure = null;
/**
* An array of parameters closed over the delegate.
* @type mixed[]
*/
private $closedArgs = [];
/**
* The object context that the delegated method or function is called in.
* @type object
*/
private $target = null;
/**
* The name of the class of the delegated method or function if it is a class method.
* @type string
*/
private $className = null;
/**
* The name of the method or function being delegated if not anonymous.
* @type string
*/
private $methodName = null;
/**
* Creates a new delegate for a static class method or instance method.
*
* @param string|object $object A class name or a class instance.
* @param string $methodName The name of the method to delegate.
* @return Delegate A new delegate that refers to the given class method.
*
* @throws \InvalidArgumentException Thrown if a bad class or method name is passed.
* @throws \BadMethodCallException Thrown if the given class or method doesn't exist.
*/
public static function forMethod($object, $methodName)
{
$delegate = new static();
// get class name and target object if an instance method
if (is_string($object)) {
$delegate->className = $object;
} elseif (is_object($object)) {
$delegate->target = $object;
$delegate->className = get_class($object);
} else {
throw new \InvalidArgumentException('First argument must be a class name or object instance.');
}
// get method name
if (is_string($methodName)) {
$delegate->methodName = $methodName;
} else {
throw new \InvalidArgumentException('Second argument must be a method name.');
}
// verify the given class and method exist
if (!is_callable([$object, $methodName])) {
throw new \BadMethodCallException('The given class or method does not exist.');
}
// create a closure around the method call bound to the class to enable
// protected and private access and disallow access to this class
$delegate->closure = $delegate->createNonStaticWrapperClosure(
[$object, $methodName],
$delegate->target,
$delegate->className
);
return $delegate;
}
/**
* Creates a new delegate for a named user function.
*
* @param string $functionName The name of the function to be delegated.
* @return Delegate A new delegate that refers to the given function.
*
* @throws \InvalidArgumentException Thrown if a non-string is given for the function name.
* @throws \BadFunctionCallException Thrown if the given function isn't defined.
*/
public static function forFunction($functionName)
{
$delegate = new static();
if (!is_string($functionName)) {
throw new \InvalidArgumentException('Function name must be a string.');
}
// verify the function exists
if (!function_exists($functionName)) {
throw new \BadFunctionCallException('The given function is not defined.');
}
$delegate->methodName = $functionName;
$delegate->closure = $delegate->createNonStaticWrapperClosure($functionName, null, 'static');
return $delegate;
}
/**
* Creates a new delegate for a given closure.
*
* @param \Closure $closure A closure to be delegated.
* @return Delegate A new delegate that refers to the given closure.
*/
public static function forClosure(\Closure $closure)
{
$delegate = new static();
$delegate->closure = $closure;
return $delegate;
}
/**
* Checks if the delegated function is a class instance method.
*
* @return boolean True if the delegated function is a class instance
* method, otherwise false.
*/
public function isInstanceMethod()
{
return $this->target != null;
}
/**
* Checks if the delegated function is a static class method.
*
* @return boolean True if the delegated function is a static class method,
* otherwise false.
*/
public function isStaticMethod()
{
return $this->target == null && $this->className != null;
}
/**
* Checks if the delegated function is a non-class, named function.
*
* @return boolean True if the delegated function is a non-class, named
* function, otherwise false.
*/
public function isFunction()
{
return $this->className == null && $this->methodName != null;
}
/**
* Checks if the delegated function is a closure.
*
* @return boolean True if the delegated function is a closure, otherwise false1.
*/
public function isClosure()
{
return $this->methodName == null;
}
/**
* Gets the object context the delegated method is invoked in.
*
* @return object The object context the delegated method is invoked in.
*/
public function getTargetObject()
{
return $this->target;
}
/**
* Gets the name of the class the delegated method belongs to.
*
* @return string The class name the delegated method belongs to.
*/
public function getClassName()
{
return $this->className;
}
/**
* Gets the name of the delegated method or function if exists.
*
* @return string The name of the delegated method or function.
*/
public function getMethodName()
{
return $this->methodName;
}
/**
* Gets a closure for the delegated method or function.
*
* @return \Closure
*/
public function getClosure()
{
return $this->closure;
}
/**
* Gets arguments that are closed over the delegate.
*
* Closed arguments are arguments that are defined beforehand to be passed
* to the delegated method or function at invoke time.
*
* @return array
*/
public function getArgs()
{
return $this->closedArgs;
}
/**
* Adds closed arguments to the delegate.
*
* @param mixed $param1,... Arguments to close over the delegate invocation.
* @return Delegate
*/
public function addArgs()
{
$this->closedArgs = array_merge($this->closedArgs, func_get_args());
return $this;
}
/**
* Removes any closed arguments from the delegate.
*/
public function clearArgs()
{
$this->closedArgs = [];
}
/**
* Gets a reflection object for the delegated method or function.
*
* @return \ReflectionFunctionAbstract
*/
public function getReflection()
{
if ($this->className != null) {
return new \ReflectionMethod($this->className, $this->methodName);
} elseif ($this->methodName != null) {
return new \ReflectionFunction($this->methodName);
} else {
return new \ReflectionFunction($this->closure);
}
}
/**
* Invokes the delegated method or function. Any arguments passed will be
* passed to the delegated method or function.
*
* @param mixed $param1,... Arguments to pass as parameters to the delegated method or function.
* @return mixed The return value of the delegated method or function.
*/
public function invoke()
{
return $this->invokeArgs(func_get_args());
}
/**
* Invokes the delegated method or function with an array of parameters.
*
* @param array $args An array of arguments to pass to the delegated method or function as parameters.
* @return mixed The return value of the delegated method or function.
*/
public function invokeArgs($args)
{
// shift closed args to the beginning
$args = array_merge($this->closedArgs, $args);
// invoke the closure and return the result
return call_user_func_array($this->closure, $args);
}
/**
* Implements callable magic for the delegate that invokes the delegated
* method or function.
*
* @param mixed $param1,... Arguments to pass as parameters to the delegated method or function.
* @return mixed The return value of the delegated method or function.
*/
public function __invoke()
{
return $this->invokeArgs(func_get_args());
}
/**
* Creates a new non-static closure that wraps around the invocation of a
* callable with a given binding and class scope.
*
* A work-around for a behavior that apparently isn't a bug. See
* {@see https://bugs.php.net/bug.php?id=64761} for more information.
*
* @param mixed $callable The callable that the closure should invoke.
* @param object $target The object to bind the closure to, or NULL for the closure to be unbound.
* @param mixed $scope The class scope the closure is to be associated, or 'static' to keep the current one.
* @return \Closure
*
* @see \Closure::bind()
*/
private function createNonStaticWrapperClosure($callable, $target, $scope = 'static')
{
return \Closure::bind(function() use ($callable) {
return call_user_func_array($callable, func_get_args());
}, $target, $scope);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment