C#-like extension methods in PHP?
I like the way in C# where you can write an extension method, and then do things like this:
string ourString = "hello";
ourString.MyExtension("another");
or even
"hello".MyExtention("another");
Is there a way to have sim开发者_如何学Pythonilar behavior in PHP?
You could if you reimplemented all your strings as objects.
class MyString {
...
function foo () { ... }
}
$str = new MyString('Bar');
$str->foo('baz');
But you really wouldn't want to do this. PHP simply isn't an object oriented language at its core, strings are just primitive types and have no methods.
The 'Bar'->foo('baz')
syntax is impossible to achieve in PHP without extending the core engine (which is not something you want to get into either, at least not for this purpose :)).
There's also nothing about extending the functionality of an object that makes it inherently better than simply writing a new function which accepts a primitive. In other words, the PHP equivalent to
"hello".MyExtention("another");
is
my_extension("hello", "another");
For all intends and purposes it has the same functionality, just different syntax.
I have another implementation for that in PHP >= 5.3.0 and its like a Decorator that Northborn Design explained.
All we need is an API to create extensions and a decorator to apply the extensions.
We have to remember that on C# extension methods they do not break the encapsulation of the object extended, and they do not modify the object (no point for that, instead implementing the method would be more effective). And the extensions methods are pure static and they receive the instance of the object, like in the example below (C#, from MSDN):
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
Of course we do not have String objects in PHP, but for all other objects we can create generic decorators to that kind of wizardry.
Lets see my implementation for that:
The API:
<?php
namespace Pattern\Extension;
/**
* API for extension methods in PHP (like C#).
*/
class Extension
{
/**
* Apply extension to an instance.
*
* @param object $instance
* @return \Pattern\Extension\ExtensionWrapper
*/
public function __invoke($instance)
{
return Extension::apply($instance);
}
/**
* Apply extension to an instance.
*
* @param object $instance
* @return \Pattern\Extension\ExtensionWrapper
*/
public static function apply($instance)
{
return new ExtensionWrapper($instance, \get_called_class());
}
/**
* @param mixed $instance
* @return boolean
*/
public static function isExtensible($instance)
{
return ($instance instanceof Extensible);
}
}
?>
The Decorator:
<?php
namespace Pattern\Extension;
/**
* Demarcate decorators that resolve the extension.
*/
interface Extensible
{
/**
* Verify the instance of the holded object.
*
* @param string $className
* @return bool true if the instance is of the type $className, false otherwise.
*/
public function holdsInstanceOf($className);
/**
* Returns the wrapped object.
* If the wrapped object is a Extensible the returns the unwrap of it and so on.
*
* @return mixed
*/
public function unwrap();
/**
* Magic method for the extension methods.
*
* @param string $name
* @param array $args
* @return mixed
*/
public function __call($name, array $args);
}
?>
And the generic implementation:
<?php
namespace Pattern\Extension;
/**
* Generic version for the Extensible Interface.
*/
final class ExtensionWrapper implements Extensible
{
/**
* @var mixed
*/
private $that;
/**
* @var Extension
*/
private $extension;
/**
* @param object $instance
* @param string | Extension $extensionClass
* @throws \InvalidArgumentException
*/
public function __construct($instance, $extensionClass)
{
if (!\is_object($instance)) {
throw new \InvalidArgumentException('ExtensionWrapper works only with objects.');
}
$this->that = $instance;
$this->extension = $extensionClass;
}
/**
* {@inheritDoc}
* @see \Pattern\Extension\Extensible::__call()
*/
public function __call($name, array $args)
{
$call = null;
if (\method_exists($this->extension, '_'.$name)) {
// this is for abstract default interface implementation
\array_unshift($args, $this->unwrap());
$call = array($this->extension, '_'.$name);
} elseif (\method_exists($this->extension, $name)) {
// this is for real implementations
\array_unshift($args, $this->unwrap());
$call = array($this->extension, $name);
} else {
// this is for real call on object
$call = array($this->that, $name);
}
return \call_user_func_array($call, $args);
}
/**
* {@inheritDoc}
* @see \Pattern\Extension\Extensible::unwrap()
*/
public function unwrap()
{
return (Extension::isExtensible($this->that) ? $this->that->unwrap() : $this->that);
}
/**
* {@inheritDoc}
* @see \Pattern\Extension\Extensible::holdsInstanceof()
*/
public function holdsInstanceOf($className)
{
return \is_a($this->unwrap(), $className);
}
}
?>
The Use:
Assume a Third party Class exists:
class ThirdPartyHello
{
public function sayHello()
{
return "Hello";
}
}
Create your extension:
use Pattern\Extension\Extension;
class HelloWorldExtension extends Extension
{
public static function sayHelloWorld(ThirdPartyHello $that)
{
return $that->sayHello().' World!';
}
}
Plus: For Interface Lovers, create an Abstract Extension:
<?php
interface HelloInterfaceExtension
{
public function sayHelloFromInterface();
}
?>
<?php
use Pattern\Extension\Extension;
abstract class AbstractHelloExtension extends Extension implements HelloInterfaceExtension
{
public static function _sayHelloFromInterface(ThirdPartyOrLegacyClass $that)
{
return $that->sayHello(). ' from Hello Interface';
}
}
?>
Then use it:
////////////////////////////
// You can hide this snippet in a Dependency Injection method
$thatClass = new ThirdPartyHello();
/** @var ThirdPartyHello|HelloWorldExtension $extension */
$extension = HelloWorldExtension::apply($thatClass);
//////////////////////////////////////////
$extension->sayHello(); // returns 'Hello'
$extension->sayHelloWorld(); // returns 'Hello World!'
//////////////////////////////////////////
// Abstract extension
$thatClass = new ThirdPartyHello();
/** @var ThirdPartyHello|HelloInterfaceExtension $extension */
$extension = AbstractHelloExtension::apply($instance);
$extension->sayHello(); // returns 'Hello'
$extension->sayHelloFromInterface(); // returns 'Hello from Hello Interface'
Pros:
- Very similar way of C# extension methods in PHP;
- Cannot test directly an extension instance as an instance of the extended object, but this is nice because its more secure because we can have places where the instance of that classe is not extended;
- As the intent of a framework to improve agility of the team you have to write less;
- The extension use seems to be part of the object but its is just decorated (maybe this is interesting to teams to develop fast but review in future the implementation of that extended object if legacy is involved);
- You can use the static method of the extension directly for performance improvements but doing that you lose the capacity to mock parts of your code (DI is highly indicated).
Cons:
- The extension must be declared to the object, Its is not just as an import like in C#, you must "decorate" the desired instance to give extension to it.
- Cannot test directly an extension instance as an instance of the extended object, more code to test using the API;
- Performance drawbacks because of the use of magic methods (but when performance is needed we change language, recreate the core, use minimalist frameworks, assembler if its needed);
Here a Gist for that Api: https://gist.github.com/tennaito/9ab4331a4b837f836ccdee78ba58dff8
Moving away from the issue of non-object primitives in PHP, when dealing with actual classes in PHP, if your environment is sane, chances are you can decorate a given class to sort-of § emulate extension methods.
Given an interface and implementation:
interface FooInterface {
function sayHello();
function sayWorld();
}
class Foo implements FooInterface {
public function sayHello() {
echo 'Hello';
}
public function sayWorld() {
echo 'World';
}
}
So long as any dependencies on Foo
are actually dependent on the interface FooInterface
(this is what I mean about sane) you can implement FooInterface
yourself as a wrapper to Foo
, forward calls to Foo
when necessary, and add additional methods as you need:
class DecoratedFoo implements FooInterface {
private $foo;
public function __construct(FooInterface $foo) {
$this->foo = $foo;
}
public function sayHello() {
$this->foo->sayHello();
}
public function sayWorld() {
$this->foo->sayWorld();
}
public function sayDanke() {
echo 'Danke';
}
public function sayShoen() {
echo 'Shoen';
}
}
The methods sayHello()
and sayWorld()
are patched through to the containing Foo
object, however we've added sayDanke()
and sayShoen()
.
The following:
function acceptsFooInterface(FooInterface $foo) {
$foo->sayHello();
$foo->sayWorld();
}
$foo = new Foo();
acceptsFooInterface($foo);
Works as expected, yielding HelloWorld
; but so does this:
$decoratedFoo = new DecoratedFoo($foo);
acceptsFooInterface($decoratedFoo);
$decoratedFoo->sayDanke();
$decoratedFoo->sayShoen();
Which results in HelloWorldDankeShoen
.
This is a limited use of the potential in the decorator pattern; you can modify the behavior of the implemented methods, or simply not forward them at all (however, we want to maintain the intended behavior by the original class definition in this example)
Is this a one-to-one solution for implementing extension methods (as per C#) in PHP? Nope, definitely not; but the extensibility afforded by this approach will help solve problems more loosely.
§ Figured I'd elaborate based on some chat conversation of the subject: you're never going to replicate it (not today, and probably not tommorow) in PHP, however the key in my answer is design patterns. They provide the opportunity to port strategies from one language to another when you can't necessarily (typically, or ever) port features.
Since PHP 5.4 there are traits which can be used as extension methods.
Example:
<?php
trait HelloWorld {
public function sayHelloWorld() {
echo 'Hello World';
}
}
class MyHelloWorld {
use HelloWorld;
public function sayExclamationMark() {
echo '!';
}
}
$o = new MyHelloWorld();
$o->sayHelloWorld();
$o->sayExclamationMark();
?>
result:
Hello World!
Once You include a trait to a class, lets call it for example with a name Extension
, You can add any methods You want and locate them elsewhere. Then in that example the use Extension
becomes just one-time decoration for the extendable classes.
精彩评论