PHP7 Anonymous Classes:
Behind the Mask
PHP7 Anonymous Classes
• What is an Anonymous Class?
• A class that is defined “inline”, within user code, at runtime
• Sometimes called “inline” or “nested” classes
• Don’t have the scope of the class where they are created
• Not assigned a class name
PHP7 Anonymous Classes
• What is an Anonymous Class?
• How do I use an Anonymous Class?
PHP7 Anonymous Classes
• How do I use an Anonymous Class?
• No name given to the class (internally assigned by PHP)
• Closing semi-colon (;)
• Constructor arguments passed in class definition
• All objects created by the same anonymous class declaration are instances of
that class (the internal class reference is identical).
$objectInstance = new class( 'arguments'... ) {
// defined properties and methods
};
PHP7 Anonymous Classes
function locationFormatBuilder (array $location = []) {
return new class (...$location) {
const POINT_MASK = '% 3d° % 2d' %5.2f" %s';
public function __construct($latitude, $longitude) {
$this->latitude = $latitude;
$this->longitude = $longitude;
}
protected function point($point, $orientation) {
…
return vsprintf(self::POINT_MASK, [$degrees, $minutes, $seconds, $direction]);
}
public function __toString() {
return vsprintf(
'%s, %s',
[$this->point($this->latitude, ['N','S']), $this->point($this->longitude, ['E','W'])]
);
}
};
}
PHP7 Anonymous Classes
$locations = [
[ 51.5074, -0.1278 ], // London
[ 40.7128, -74.0059 ], // New York
[ -22.9068, -43.1729 ], // Rio de Janeiro
[ -33.9249, 18.4241 ], // Cape Town
[ 19.0760, 72.8777 ], // Mumbai
[ 35.6895, 139.6917 ], // Tokyo
[ -33.8688, 151.2093 ], // Sydney
[ 31.7683, 35.2137 ], // Jerusalem
];
foreach ($locations as $location) {
echo locationFormatBuilder($location), PHP_EOL;
}
51° 30' 26.00" N, 0° 7' 40.00" W
40° 42' 46.00" N, 74° 0' 21.00" W
22° 54' 24.00" S, 43° 10' 22.00" W
33° 55' 29.00" S, 18° 25' 26.00" E
19° 4' 33.00" N, 72° 52' 39.00" E
35° 41' 22.00" N, 139° 41' 30.00" E
33° 52' 7.00" S, 151° 12' 33.00" E
31° 46' 5.00" N, 35° 12' 49.00" E
PHP7 Anonymous Classes
PHP7 Anonymous Classes
• How do I use an Anonymous Class?
• Can “implement” and “extend”
• Can use Traits
$objectInstance = new class( 'arguments'... ) {
// defined properties and methods
};
PHP7 Anonymous Classes
• Anonymous Class Factories
• Builds an Anonymous Class that extends the “userModel” class with optional
Traits
• Uses “eval()”
$anonymousModel = (new AnonymousClassFactory('userModel'))
->withConstructorArguments($databaseConnection)
->withTraits('softDelete', 'auditable')
->create();
PHP7 Anonymous Classes
trait softDelete {
protected $softDelete = true;
}
trait auditable {
protected $auditable = true;
}
class userModel {
protected $dbConnection;
protected $loggingEnabled;
public function __construct($dbConnection, $loggingEnabled = false) {
$this->dbConnection = $dbConnection;
$this->loggingEnabled = $loggingEnabled;
}
}
PHP7 Anonymous Classes
$dbConnection = new PDO('sqlite:example.db');
$loggingEnabled = true;
$className = 'userModel';
$traits = implode(', ', ['softDelete', 'auditable']);
$classDefinition = <<<ANONYMOUS
new class($dbConnection, $loggingEnabled) extends {$className} {
use {$traits};
};
ANONYMOUS;
$anonymousUserModelClassInstance = eval("return $classDefinition;");
PHP7 Anonymous Classes
object(class@anonymous)#2 (4) {
["dbConnection":protected]=>
object(PDO)#1 (0) {
}
["loggingEnabled":protected]=>
bool(true)
["softDelete":protected]=>
bool(true)
["auditable":protected]=>
bool(true)
}
PHP7 Anonymous Classes
• Anonymous Class Factories
• In Search of an Anonymous Class Factory
https://markbakeruk.net/2016/05/03/in-search-of-an-anonymous-class-factory/
• Anonymous Class Factory – The Results are in
https://markbakeruk.net/2016/05/12/anonymous-class-factory-the-results-are-in/
$anonymousModel = (new AnonymousClassFactory('modelClass'))
->withConstructorArguments($databaseConnection)
->withTraits('softDelete', 'auditable')
->create();
PHP7 Anonymous Classes
class userController {
protected $user;
public function __construct(userModel $user) {
$this->user = $user;
}
}
$controller = new UserController($anonymousUserModelClassInstance);
PHP7 Anonymous Classes
• What is an Anonymous Class?
• How do I use an Anonymous Class?
• What Limitations are there to Anonymous Classes?
PHP7 Anonymous Classes
• What Limitations are there to Anonymous Classes?
• Definition and Instantiation in a Single Step
• Cannot be Serialized
• Effectively Final
• No OpCaching
• No Documentation (DocBlocks)
• No IDE Hinting
PHP7 Anonymous Classes
• What is an Anonymous Class?
• How do I use an Anonymous Class?
• What Limitations are there to Anonymous Classes?
• Why would I use Anonymous Classes?
PHP7 Anonymous Classes
• Why would I use Anonymous Classes?
• One-off Objects
• Private Classes
• Test Mocking
• On-demand Typed Structures (instead of generic arrays or StdClass)
• Ad-hoc adapters in a loosely coupled architecture
• To bypass Autoloading
PHP7 Anonymous Classes
• Why would I use Anonymous Classes?
• Test Mocking
• https://mfyu.co.uk/post/anonymous-classes-in-php-7-are-fantastic-for-tests
PHP7 Anonymous Classes
• Inner- or Nested-Classes
class SpyMaster {
private $targetObject;
protected $objectMission;
public function __construct($targetObject) {
$this->targetObject = $targetObject;
$this->objectMission = new SpyObjectMission($targetObject);
}
protected function getHandler() {
…
}
public function infiltrate() {
return $this->getHandler();
}
}
PHP7 Anonymous Classes
• Inner- or Nested-Classes
class SpyObjectMission {
public $methods = [];
public $invoker;
public function __construct($targetObject) {
$reflector = new ReflectionObject($targetObject);
$this->invoker = $this->getInvoker($reflector, $targetObject);
}
protected function getInvoker(ReflectionObject $reflector, $targetObject) {
$staticMethods = array_column($reflector->getMethods(ReflectionMethod::IS_STATIC), 'name’);
$this->methods = array_diff(
array_column($reflector->getMethods(), 'name’), $staticMethods)
);
$invoker = function($methodName, ...$args) {
return self::$methodName(...$args);
};
return $invoker->bindTo($targetObject, get_class($targetObject));
}
}
PHP7 Anonymous Classes
• Inner- or Nested-Classes
protected function getHandler() {
return new class ($this->objectMission, $this->staticMission) {
private $objectMission;
public function __construct(SpyObjectMission $objectMission) {
$this->objectMission = $objectMission;
}
public function __call($methodName, $args) {
if (in_array($methodName, $this->objectMission->methods)) {
$invoker = $this->objectMission->invoker;
return $invoker($methodName, ...$args);
}
throw new Exception("Object Method {$methodName} does not exist");
}
};
}
PHP7 Anonymous Classes
• Inner- or Nested-Classes
$test = new classThatWeWantToTest(1, 2, 3);
$spy = (new SpyMaster($test))
->infiltrate();
echo 'SPY FOR TEST #1', PHP_EOL;
echo $spy->privateMethodThatWeWantToTest(), PHP_EOL;
PHP7 Anonymous Classes
• Inner- or Nested-Classes
• Closures, Anonymous Classes and an alternative approach to Test Mocking
(Part 2)
• https://markbakeruk.net/2017/07/30/closures-anonymous-classes-and-an-alternative-
approach-to-test-mocking-part-2/
• Closures, Anonymous Classes and an alternative approach to Test Mocking
(Part 4)
• https://markbakeruk.net/2018/01/23/closures-anonymous-classes-and-an-alternative-
approach-to-test-mocking-part-4/
PHP7 Anonymous Classes
Phpanpy
Mock object framework for unit testing
Like Mockery https://packagist.org/packages/mockery/mockery
PHP7 Anonymous Classes
$dummyConstructorArguments = [1,2];
$points = (new PhpanpyAnonymousDoubleFactory('GeodeticLatLong', ...$dummyConstructorArguments))
->setMethodReturnValuesList([
'getLatitude' => [51.5074, 40.7128, -22.9068, -33.9249, 19.0760, 35.6895, -33.8688, 31.7683],
'getLongitude' => [-0.1278, -74.0059, -43.1729, 18.4241, 72.8777, 139.6917, 151.2093, 35.2137],
])->setMethodReturnValuesFixed([
'getElevation' => 0.0
])->create();
$line = (new PhpanpyAnonymousDoubleFactory('GeodeticLine’))
->suppressOriginalConstructor()
->setMethodReturnValuesFixed([
'getNextPoint' => $points
])->create();
PHP7 Anonymous Classes
try {
for($i = 0; $i < 10; ++$i) {
$point = $line->getNextPoint();
echo sprintf(
"%+ 9.4f %+ 9.4f % 7.4f",
$point->getLatitude(), $point->getLongitude(), $point->getElevation()
), PHP_EOL;
}
} catch (Exception $e) {
echo $e->getMessage();
}
PHP7 Anonymous Classes
new class(...$this->constructorArgs) extends GeodeticLatLong {
use PhpanpyAnonymousPrePostHooks, PhpanpylistReturnValuesTrait, PhpanpyfixedReturnValueTrait;
public function __construct ($latitude, $longitude, $elevation = 0) {
$args = func_get_args();
$this->firePreHookInstance(__FUNCTION__, ...$args);
parent::__construct($latitude, $longitude, $elevation);
$this->firePostHookInstance(__FUNCTION__, ...$args);
}
public function getLatitude ($radians = false) {
$args = func_get_args();
$this->firePreHookInstance(__FUNCTION__, ...$args);
$result = $this->traitMethodCallReturnNextValueFromList(__FUNCTION__);
$this->firePostHookInstance(__FUNCTION__, $result, ...$args);
return $result;
}
public function getElevation () {
$args = func_get_args();
$this->firePreHookInstance(__FUNCTION__, ...$args);
$result = $this->traitMethodCallReturnFixedValue(__FUNCTION__);
$this->firePostHookInstance(__FUNCTION__, $result, ...$args);
return $result;
}
}
Anonymous Classes: Behind the Mask

Anonymous Classes: Behind the Mask

  • 1.
  • 2.
    PHP7 Anonymous Classes •What is an Anonymous Class? • A class that is defined “inline”, within user code, at runtime • Sometimes called “inline” or “nested” classes • Don’t have the scope of the class where they are created • Not assigned a class name
  • 3.
    PHP7 Anonymous Classes •What is an Anonymous Class? • How do I use an Anonymous Class?
  • 4.
    PHP7 Anonymous Classes •How do I use an Anonymous Class? • No name given to the class (internally assigned by PHP) • Closing semi-colon (;) • Constructor arguments passed in class definition • All objects created by the same anonymous class declaration are instances of that class (the internal class reference is identical). $objectInstance = new class( 'arguments'... ) { // defined properties and methods };
  • 5.
    PHP7 Anonymous Classes functionlocationFormatBuilder (array $location = []) { return new class (...$location) { const POINT_MASK = '% 3d° % 2d' %5.2f" %s'; public function __construct($latitude, $longitude) { $this->latitude = $latitude; $this->longitude = $longitude; } protected function point($point, $orientation) { … return vsprintf(self::POINT_MASK, [$degrees, $minutes, $seconds, $direction]); } public function __toString() { return vsprintf( '%s, %s', [$this->point($this->latitude, ['N','S']), $this->point($this->longitude, ['E','W'])] ); } }; }
  • 6.
    PHP7 Anonymous Classes $locations= [ [ 51.5074, -0.1278 ], // London [ 40.7128, -74.0059 ], // New York [ -22.9068, -43.1729 ], // Rio de Janeiro [ -33.9249, 18.4241 ], // Cape Town [ 19.0760, 72.8777 ], // Mumbai [ 35.6895, 139.6917 ], // Tokyo [ -33.8688, 151.2093 ], // Sydney [ 31.7683, 35.2137 ], // Jerusalem ]; foreach ($locations as $location) { echo locationFormatBuilder($location), PHP_EOL; }
  • 7.
    51° 30' 26.00"N, 0° 7' 40.00" W 40° 42' 46.00" N, 74° 0' 21.00" W 22° 54' 24.00" S, 43° 10' 22.00" W 33° 55' 29.00" S, 18° 25' 26.00" E 19° 4' 33.00" N, 72° 52' 39.00" E 35° 41' 22.00" N, 139° 41' 30.00" E 33° 52' 7.00" S, 151° 12' 33.00" E 31° 46' 5.00" N, 35° 12' 49.00" E PHP7 Anonymous Classes
  • 8.
    PHP7 Anonymous Classes •How do I use an Anonymous Class? • Can “implement” and “extend” • Can use Traits $objectInstance = new class( 'arguments'... ) { // defined properties and methods };
  • 9.
    PHP7 Anonymous Classes •Anonymous Class Factories • Builds an Anonymous Class that extends the “userModel” class with optional Traits • Uses “eval()” $anonymousModel = (new AnonymousClassFactory('userModel')) ->withConstructorArguments($databaseConnection) ->withTraits('softDelete', 'auditable') ->create();
  • 10.
    PHP7 Anonymous Classes traitsoftDelete { protected $softDelete = true; } trait auditable { protected $auditable = true; } class userModel { protected $dbConnection; protected $loggingEnabled; public function __construct($dbConnection, $loggingEnabled = false) { $this->dbConnection = $dbConnection; $this->loggingEnabled = $loggingEnabled; } }
  • 11.
    PHP7 Anonymous Classes $dbConnection= new PDO('sqlite:example.db'); $loggingEnabled = true; $className = 'userModel'; $traits = implode(', ', ['softDelete', 'auditable']); $classDefinition = <<<ANONYMOUS new class($dbConnection, $loggingEnabled) extends {$className} { use {$traits}; }; ANONYMOUS; $anonymousUserModelClassInstance = eval("return $classDefinition;");
  • 12.
    PHP7 Anonymous Classes object(class@anonymous)#2(4) { ["dbConnection":protected]=> object(PDO)#1 (0) { } ["loggingEnabled":protected]=> bool(true) ["softDelete":protected]=> bool(true) ["auditable":protected]=> bool(true) }
  • 13.
    PHP7 Anonymous Classes •Anonymous Class Factories • In Search of an Anonymous Class Factory https://markbakeruk.net/2016/05/03/in-search-of-an-anonymous-class-factory/ • Anonymous Class Factory – The Results are in https://markbakeruk.net/2016/05/12/anonymous-class-factory-the-results-are-in/ $anonymousModel = (new AnonymousClassFactory('modelClass')) ->withConstructorArguments($databaseConnection) ->withTraits('softDelete', 'auditable') ->create();
  • 14.
    PHP7 Anonymous Classes classuserController { protected $user; public function __construct(userModel $user) { $this->user = $user; } } $controller = new UserController($anonymousUserModelClassInstance);
  • 15.
    PHP7 Anonymous Classes •What is an Anonymous Class? • How do I use an Anonymous Class? • What Limitations are there to Anonymous Classes?
  • 16.
    PHP7 Anonymous Classes •What Limitations are there to Anonymous Classes? • Definition and Instantiation in a Single Step • Cannot be Serialized • Effectively Final • No OpCaching • No Documentation (DocBlocks) • No IDE Hinting
  • 17.
    PHP7 Anonymous Classes •What is an Anonymous Class? • How do I use an Anonymous Class? • What Limitations are there to Anonymous Classes? • Why would I use Anonymous Classes?
  • 18.
    PHP7 Anonymous Classes •Why would I use Anonymous Classes? • One-off Objects • Private Classes • Test Mocking • On-demand Typed Structures (instead of generic arrays or StdClass) • Ad-hoc adapters in a loosely coupled architecture • To bypass Autoloading
  • 19.
    PHP7 Anonymous Classes •Why would I use Anonymous Classes? • Test Mocking • https://mfyu.co.uk/post/anonymous-classes-in-php-7-are-fantastic-for-tests
  • 20.
    PHP7 Anonymous Classes •Inner- or Nested-Classes class SpyMaster { private $targetObject; protected $objectMission; public function __construct($targetObject) { $this->targetObject = $targetObject; $this->objectMission = new SpyObjectMission($targetObject); } protected function getHandler() { … } public function infiltrate() { return $this->getHandler(); } }
  • 21.
    PHP7 Anonymous Classes •Inner- or Nested-Classes class SpyObjectMission { public $methods = []; public $invoker; public function __construct($targetObject) { $reflector = new ReflectionObject($targetObject); $this->invoker = $this->getInvoker($reflector, $targetObject); } protected function getInvoker(ReflectionObject $reflector, $targetObject) { $staticMethods = array_column($reflector->getMethods(ReflectionMethod::IS_STATIC), 'name’); $this->methods = array_diff( array_column($reflector->getMethods(), 'name’), $staticMethods) ); $invoker = function($methodName, ...$args) { return self::$methodName(...$args); }; return $invoker->bindTo($targetObject, get_class($targetObject)); } }
  • 22.
    PHP7 Anonymous Classes •Inner- or Nested-Classes protected function getHandler() { return new class ($this->objectMission, $this->staticMission) { private $objectMission; public function __construct(SpyObjectMission $objectMission) { $this->objectMission = $objectMission; } public function __call($methodName, $args) { if (in_array($methodName, $this->objectMission->methods)) { $invoker = $this->objectMission->invoker; return $invoker($methodName, ...$args); } throw new Exception("Object Method {$methodName} does not exist"); } }; }
  • 23.
    PHP7 Anonymous Classes •Inner- or Nested-Classes $test = new classThatWeWantToTest(1, 2, 3); $spy = (new SpyMaster($test)) ->infiltrate(); echo 'SPY FOR TEST #1', PHP_EOL; echo $spy->privateMethodThatWeWantToTest(), PHP_EOL;
  • 24.
    PHP7 Anonymous Classes •Inner- or Nested-Classes • Closures, Anonymous Classes and an alternative approach to Test Mocking (Part 2) • https://markbakeruk.net/2017/07/30/closures-anonymous-classes-and-an-alternative- approach-to-test-mocking-part-2/ • Closures, Anonymous Classes and an alternative approach to Test Mocking (Part 4) • https://markbakeruk.net/2018/01/23/closures-anonymous-classes-and-an-alternative- approach-to-test-mocking-part-4/
  • 25.
    PHP7 Anonymous Classes Phpanpy Mockobject framework for unit testing Like Mockery https://packagist.org/packages/mockery/mockery
  • 26.
    PHP7 Anonymous Classes $dummyConstructorArguments= [1,2]; $points = (new PhpanpyAnonymousDoubleFactory('GeodeticLatLong', ...$dummyConstructorArguments)) ->setMethodReturnValuesList([ 'getLatitude' => [51.5074, 40.7128, -22.9068, -33.9249, 19.0760, 35.6895, -33.8688, 31.7683], 'getLongitude' => [-0.1278, -74.0059, -43.1729, 18.4241, 72.8777, 139.6917, 151.2093, 35.2137], ])->setMethodReturnValuesFixed([ 'getElevation' => 0.0 ])->create(); $line = (new PhpanpyAnonymousDoubleFactory('GeodeticLine’)) ->suppressOriginalConstructor() ->setMethodReturnValuesFixed([ 'getNextPoint' => $points ])->create();
  • 27.
    PHP7 Anonymous Classes try{ for($i = 0; $i < 10; ++$i) { $point = $line->getNextPoint(); echo sprintf( "%+ 9.4f %+ 9.4f % 7.4f", $point->getLatitude(), $point->getLongitude(), $point->getElevation() ), PHP_EOL; } } catch (Exception $e) { echo $e->getMessage(); }
  • 28.
    PHP7 Anonymous Classes newclass(...$this->constructorArgs) extends GeodeticLatLong { use PhpanpyAnonymousPrePostHooks, PhpanpylistReturnValuesTrait, PhpanpyfixedReturnValueTrait; public function __construct ($latitude, $longitude, $elevation = 0) { $args = func_get_args(); $this->firePreHookInstance(__FUNCTION__, ...$args); parent::__construct($latitude, $longitude, $elevation); $this->firePostHookInstance(__FUNCTION__, ...$args); } public function getLatitude ($radians = false) { $args = func_get_args(); $this->firePreHookInstance(__FUNCTION__, ...$args); $result = $this->traitMethodCallReturnNextValueFromList(__FUNCTION__); $this->firePostHookInstance(__FUNCTION__, $result, ...$args); return $result; } public function getElevation () { $args = func_get_args(); $this->firePreHookInstance(__FUNCTION__, ...$args); $result = $this->traitMethodCallReturnFixedValue(__FUNCTION__); $this->firePostHookInstance(__FUNCTION__, $result, ...$args); return $result; } }