Skip to content
This repository was archived by the owner on Feb 19, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/GeneratedHydrator/ClassGenerator/HydratorGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ class HydratorGenerator
* and a map of properties to be used to
*
* @param \ReflectionClass $originalClass
* @param array $options
*
* @return \PHPParser_Node[]
*/
public function generate(ReflectionClass $originalClass)
public function generate(ReflectionClass $originalClass, array $options = array())
{
$builder = new ClassBuilder();

Expand All @@ -67,7 +68,7 @@ function () {
// step 2: implement new methods and interfaces, extend original class
$implementor = new PHPParser_NodeTraverser();

$implementor->addVisitor(new HydratorMethodsVisitor($originalClass));
$implementor->addVisitor(new HydratorMethodsVisitor($originalClass, $options));
$implementor->addVisitor(new ClassExtensionVisitor($originalClass->getName(), $originalClass->getName()));
$implementor->addVisitor(
new ClassImplementorVisitor($originalClass->getName(), array('Zend\\Stdlib\\Hydrator\\HydratorInterface'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
*/
class HydratorMethodsVisitor extends PHPParser_NodeVisitorAbstract
{
/**
* When an array of keys is supplied for this option only properties of
* supplied keys will be used
*/
const OPTION_ALLOWED_PROPERTIES = 'allowedProperties';

/**
* @var ReflectionClass
*/
Expand All @@ -39,16 +45,44 @@ class HydratorMethodsVisitor extends PHPParser_NodeVisitorAbstract
/**
* @param ReflectionClass $reflectedClass
*/
public function __construct(ReflectionClass $reflectedClass)
public function __construct(ReflectionClass $reflectedClass, array $options = array())
{
$this->reflectedClass = $reflectedClass;
$this->accessibleProperties = $this->reflectedClass->getProperties(
(ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PUBLIC)
& ~ReflectionProperty::IS_STATIC
);
$this->makeAccessibleProperties($options);
$this->makePropertyWriters($options);
}

/**
* Initialize $accessibleProperties with option OPTION_ALLOWED_PROPERTIES check
*/
private function makeAccessibleProperties($options)
{
$this->accessibleProperties = $this->getProtectedProperties($this->reflectedClass);

if (isset($options[self::OPTION_ALLOWED_PROPERTIES])) {
foreach ($this->accessibleProperties as $key => $reflProp) {
if (! in_array($reflProp->getName(), $options[self::OPTION_ALLOWED_PROPERTIES])) {
unset($this->accessibleProperties[$key]);
}
}
}
}

/**
* Initialize $propertyWriters with option OPTION_ALLOWED_PROPERTIES check
*/
private function makePropertyWriters($options)
{
foreach ($this->getPrivateProperties($this->reflectedClass) as $property) {
$name = $property->getName();

foreach ($reflectedClass->getProperties(ReflectionProperty::IS_PRIVATE) as $property) {
$this->propertyWriters[$property->getName()] = new PropertyAccessor($property, 'Writer');
if (isset($options[self::OPTION_ALLOWED_PROPERTIES])) {
if (in_array($name, $options[self::OPTION_ALLOWED_PROPERTIES])) {
$this->propertyWriters[$name] = new PropertyAccessor($property, 'Writer');
}
} else {
$this->propertyWriters[$name] = new PropertyAccessor($property, 'Writer');
}
}
}

Expand Down Expand Up @@ -210,4 +244,38 @@ function (PHPParser_Node_Stmt_ClassMethod $method) use ($name) {

return $method;
}

/**
* Retrieve instance public/protected properties
*
* @param ReflectionClass $reflectedClass
*
* @return ReflectionProperty[]
*/
private function getProtectedProperties(ReflectionClass $reflectedClass)
{
return array_filter(
$reflectedClass->getProperties(),
function (ReflectionProperty $property) {
return ($property->isPublic() || $property->isProtected()) && ! $property->isStatic();
}
);
}

/**
* Retrieve instance private properties
*
* @param ReflectionClass $reflectedClass
*
* @return ReflectionProperty[]
*/
private function getPrivateProperties(ReflectionClass $reflectedClass)
{
return array_filter(
$reflectedClass->getProperties(),
function (ReflectionProperty $property) {
return $property->isPrivate() && ! $property->isStatic();
}
);
}
}
6 changes: 4 additions & 2 deletions src/GeneratedHydrator/Factory/HydratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ public function __construct(Configuration $configuration)
/**
* Retrieves the generated hydrator FQCN
*
* @param array $options
*
* @return string
*/
public function getHydratorClass()
public function getHydratorClass(array $options = array())
{
$inflector = $this->configuration->getClassNameInflector();
$realClassName = $inflector->getUserClassName($this->configuration->getHydratedClassName());
Expand All @@ -59,7 +61,7 @@ public function getHydratorClass()
if (! class_exists($hydratorClassName) && $this->configuration->doesAutoGenerateProxies()) {
$generator = new HydratorGenerator();
$originalClass = new ReflectionClass($realClassName);
$generatedAst = $generator->generate($originalClass);
$generatedAst = $generator->generate($originalClass, $options);
$traverser = new PHPParser_NodeTraverser();

$traverser->addVisitor(new ClassRenamerVisitor($originalClass, $hydratorClassName));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,44 @@ class HydratorMethodsVisitorTest extends PHPUnit_Framework_TestCase
*
* @param string $className
* @param PHPParser_Node_Stmt_Class $classNode
* @param string[] $properties
*/
public function testBasicCodeGeneration($className, PHPParser_Node_Stmt_Class $classNode)
public function testBasicCodeGeneration($className, PHPParser_Node_Stmt_Class $classNode, array $properties)
{
$visitor = new HydratorMethodsVisitor(new ReflectionClass($className));

/* @var $modifiedAst PHPParser_Node_Stmt_Class */
$modifiedNode = $visitor->leaveNode($classNode);

$this->checkMethodExistence('hydrate', $modifiedNode);
$this->checkMethodExistence('extract', $modifiedNode);
$this->checkMethodExistence('__construct', $modifiedNode);
$this->assertMethodExistence('hydrate', $modifiedNode);
$this->assertMethodExistence('extract', $modifiedNode);
$this->assertMethodExistence('__construct', $modifiedNode);
$this->assertContainsPropertyAccessors($modifiedNode, $properties);
}

/**
* Tests the Allowed Properties option.
*
* BaseClass has a public, protected and private property. Normally
* `accessibleProperties` would have 2 properties (public & protected) and
* `propertyWriters` just one (private).
*/
public function testAllowPropertiesOption() {
$visitor = new HydratorMethodsVisitor(
new ReflectionClass('GeneratedHydratorTestAsset\\BaseClass'),
array(HydratorMethodsVisitor::OPTION_ALLOWED_PROPERTIES => array(
'protectedProperty'
))
);

$reflClass = new ReflectionClass($visitor);
$reflProperty1 = $reflClass->getProperty('accessibleProperties');
$reflProperty1->setAccessible(true);
$reflProperty2 = $reflClass->getProperty('propertyWriters');
$reflProperty2->setAccessible(true);

$this->assertCount(1, $reflProperty1->getValue($visitor));
$this->assertCount(0, $reflProperty2->getValue($visitor));
}

/**
Expand All @@ -61,9 +88,8 @@ public function testBasicCodeGeneration($className, PHPParser_Node_Stmt_Class $c
* @param string $methodName
* @param PHPParser_Node_Stmt_Class $class
*/
private function checkMethodExistence($methodName, PHPParser_Node_Stmt_Class $class)
private function assertMethodExistence($methodName, PHPParser_Node_Stmt_Class $class)
{

$members = $class->stmts;

$this->assertCount(
Expand All @@ -78,6 +104,62 @@ function (PHPParser_Node $node) use ($methodName) {
);
}

/**
* Verifies that the given properties and only the given properties are added to the hydrator logic
*
* @param PHPParser_Node_Stmt_Class $class
* @param array $properties
*/
private function assertContainsPropertyAccessors(PHPParser_Node_Stmt_Class $class, array $properties)
{
$lookupProperties = array_flip($properties);

foreach ($class->stmts as $method) {
if ($method instanceof \PHPParser_Node_Stmt_ClassMethod && $method->name === 'hydrate') {
foreach ($method->stmts as $assignment) {
if ($assignment instanceof \PHPParser_Node_Expr_Assign) {
$var = $assignment->var;

if ($var instanceof \PHPParser_Node_Expr_PropertyFetch && is_string($var->name)) {
if (! isset($lookupProperties[$var->name])) {
$this->fail(sprintf('Property "%s" should not be hydrated', $var->name));
}

unset($lookupProperties[$var->name]);
}
}
}
}

if ($method instanceof \PHPParser_Node_Stmt_ClassMethod && $method->name === '__construct') {
foreach ($method->stmts as $assignment) {
if ($assignment instanceof \PHPParser_Node_Expr_Assign) {
$var = $assignment->var;

if ($var instanceof \PHPParser_Node_Expr_PropertyFetch
&& preg_match('/(.*)Writer[a-zA-Z0-9]+/', $var->name, $matches)
) {
if (! isset($lookupProperties[$matches[1]])) {
$this->fail(sprintf('Property "%s" should not be hydrated', $matches[1]));
}

unset($lookupProperties[$matches[1]]);
}
}
}
}
}

if (empty($lookupProperties)) {
return;
}

$this->fail(sprintf(
'Could not match following properties in the generated code: %s',
var_export(array_flip($lookupProperties), true)
));
}

/**
* @return \PHPParser_Node[][]
*/
Expand All @@ -86,13 +168,20 @@ public function classAstProvider()
$parser = new PHPParser_Parser(new PHPParser_Lexer());

$className = UniqueIdentifierGenerator::getIdentifier('Foo');
$classCode = 'class ' . $className . ' { private $bar; private $baz; protected $tab;'
$classCode = 'class ' . $className . ' { private $bar; private $baz; protected $tab; '
. 'protected $tar; public $taw; public $tam; }';

eval($classCode);

return array(
array($className, $parser->parse('<?php ' . $classCode)[0]),
);
$staticClassName = UniqueIdentifierGenerator::getIdentifier('Foo');
$staticClassCode = 'class ' . $staticClassName . ' { private static $bar; '
. 'protected static $baz; public static $tab; private $taz; }';

eval($staticClassCode);

return [
[$className, $parser->parse('<?php ' . $classCode)[0], ['bar', 'baz', 'tab', 'tar', 'taw', 'tam']],
[$staticClassName, $parser->parse('<?php ' . $staticClassCode)[0], ['taz']],
];
}
}