diff --git a/composer.json b/composer.json index 67272996c504..c1b8e8951a5a 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "symfony/options-resolver": "self.version", "symfony/process": "self.version", "symfony/property-access": "self.version", + "symfony/property-info": "self.version", "symfony/proxy-manager-bridge": "self.version", "symfony/routing": "self.version", "symfony/security": "self.version", @@ -76,7 +77,11 @@ "monolog/monolog": "~1.11", "ircmaxell/password-compat": "~1.0", "ocramius/proxy-manager": "~0.4|~1.0", - "egulias/email-validator": "~1.2" + "egulias/email-validator": "~1.2", + "phpdocumentor/reflection": "^1.0.7" + }, + "conflict": { + "phpdocumentor/reflection": "<1.0.7" }, "autoload": { "psr-4": { diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php new file mode 100644 index 000000000000..dc22e235959b --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\PropertyInfo; + +use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory; +use Doctrine\Common\Persistence\Mapping\MappingException; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Extracts data using Doctrine ORM and ODM metadata. + * + * @author Kévin Dunglas + */ +class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface +{ + /** + * @var ClassMetadataFactory + */ + private $classMetadataFactory; + + public function __construct(ClassMetadataFactory $classMetadataFactory) + { + $this->classMetadataFactory = $classMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = array()) + { + try { + $metadata = $this->classMetadataFactory->getMetadataFor($class); + } catch (MappingException $exception) { + return; + } + + return array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); + } + + /** + * {@inheritdoc} + */ + public function getTypes($class, $property, array $context = array()) + { + try { + $metadata = $this->classMetadataFactory->getMetadataFor($class); + } catch (MappingException $exception) { + return; + } + + if ($metadata->hasAssociation($property)) { + $class = $metadata->getAssociationTargetClass($property); + + if ($metadata->isSingleValuedAssociation($property)) { + if ($metadata instanceof ClassMetadataInfo) { + $nullable = isset($metadata->discriminatorColumn['nullable']) ? $metadata->discriminatorColumn['nullable'] : false; + } else { + $nullable = false; + } + + return array(new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $class)); + } + + return array(new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + 'Doctrine\Common\Collections\Collection', + true, + new Type(Type::BUILTIN_TYPE_INT), + new Type(Type::BUILTIN_TYPE_OBJECT, false, $class) + )); + } + + if ($metadata->hasField($property)) { + $typeOfField = $metadata->getTypeOfField($property); + $nullable = $metadata instanceof ClassMetadataInfo && $metadata->isNullable($property); + + switch ($typeOfField) { + case 'date': + case 'datetime': + case 'datetimetz': + case 'time': + return array(new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')); + + case 'array': + return array(new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)); + + case 'simple_array': + return array(new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))); + + case 'json_array': + return array(new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)); + + default: + return array(new Type($this->getPhpType($typeOfField), $nullable)); + } + } + } + + /** + * Gets the corresponding built-in PHP type. + * + * @param string $doctrineType + * + * @return string + */ + private function getPhpType($doctrineType) + { + switch ($doctrineType) { + case 'smallint': + // No break + case 'bigint': + // No break + case 'integer': + return Type::BUILTIN_TYPE_INT; + + case 'decimal': + return Type::BUILTIN_TYPE_FLOAT; + + case 'text': + // No break + case 'guid': + return Type::BUILTIN_TYPE_STRING; + + case 'boolean': + return Type::BUILTIN_TYPE_BOOL; + + case 'blob': + // No break + case 'binary': + return Type::BUILTIN_TYPE_RESOURCE; + + default: + return $doctrineType; + } + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php new file mode 100644 index 000000000000..df6c35589508 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\PropertyInfo\Tests; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Tools\Setup; +use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Kévin Dunglas + */ +class DoctrineExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DoctrineExtractor + */ + private $extractor; + + public function setUp() + { + $config = Setup::createAnnotationMetadataConfiguration(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures'), true); + $entityManager = EntityManager::create(array('driver' => 'pdo_sqlite'), $config); + + $this->extractor = new DoctrineExtractor($entityManager->getMetadataFactory()); + } + + public function testGetProperties() + { + $this->assertEquals( + array( + 'id', + 'guid', + 'time', + 'json', + 'simpleArray', + 'bool', + 'binary', + 'foo', + 'bar', + ), + $this->extractor->getProperties('Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy') + ); + } + + /** + * @dataProvider typesProvider + */ + public function testExtract($property, array $type = null) + { + $this->assertEquals($type, $this->extractor->getTypes('Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy', $property, array())); + } + + public function typesProvider() + { + return array( + array('id', array(new Type(Type::BUILTIN_TYPE_INT))), + array('guid', array(new Type(Type::BUILTIN_TYPE_STRING))), + array('bool', array(new Type(Type::BUILTIN_TYPE_BOOL))), + array('binary', array(new Type(Type::BUILTIN_TYPE_RESOURCE))), + array('json', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true))), + array('foo', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation'))), + array('bar', array(new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + 'Doctrine\Common\Collections\Collection', + true, + new Type(Type::BUILTIN_TYPE_INT), + new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + ))), + array('simpleArray', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)))), + array('notMapped', null), + ); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php new file mode 100644 index 000000000000..864bd78407c4 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\ManyToMany; +use Doctrine\ORM\Mapping\ManyToOne; + +/** + * @Entity + * + * @author Kévin Dunglas + */ +class DoctrineDummy +{ + /** + * @Id + * @Column(type="smallint") + */ + public $id; + + /** + * @ManyToOne(targetEntity="DoctrineRelation") + */ + public $foo; + + /** + * @ManyToMany(targetEntity="DoctrineRelation") + */ + public $bar; + + /** + * @Column(type="guid") + */ + protected $guid; + + /** + * @Column(type="time") + */ + private $time; + + /** + * @Column(type="json_array") + */ + private $json; + + /** + * @Column(type="simple_array") + */ + private $simpleArray; + + /** + * @Column(type="boolean") + */ + private $bool; + + /** + * @Column(type="binary") + */ + private $binary; + + public $notMapped; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php new file mode 100644 index 000000000000..bfb27e9338d9 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Id; + +/** + * @Entity + * + * @author Kévin Dunglas + */ +class DoctrineRelation +{ + /** + * @Id + * @Column(type="smallint") + */ + public $id; +} diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index b27949cfaf9b..bf215f1697bf 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -26,6 +26,7 @@ "symfony/form": "~2.8|~3.0.0", "symfony/http-kernel": "~2.2|~3.0.0", "symfony/property-access": "~2.3|~3.0.0", + "symfony/property-info": "~2.8|3.0", "symfony/security": "~2.2|~3.0.0", "symfony/expression-language": "~2.2|~3.0.0", "symfony/validator": "~2.5,>=2.5.5|~3.0.0", @@ -37,6 +38,7 @@ "suggest": { "symfony/form": "", "symfony/validator": "", + "symfony/property-info": "", "doctrine/data-fixtures": "", "doctrine/dbal": "", "doctrine/orm": "" diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php new file mode 100644 index 000000000000..0e10fcbc13b1 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -0,0 +1,392 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use phpDocumentor\Reflection\ClassReflector; +use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\FileReflector; +use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Extracts data using a PHPDoc parser. + * + * @author Kévin Dunglas + */ +class PhpDocExtractor implements PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface +{ + const PROPERTY = 0; + const ACCESSOR = 1; + const MUTATOR = 2; + + /** + * @var FileReflector[] + */ + private $fileReflectors = array(); + + /** + * @var DocBlock[] + */ + private $docBlocks = array(); + + /** + * {@inheritdoc} + */ + public function getShortDescription($class, $property, array $context = array()) + { + list($docBlock) = $this->getDocBlock($class, $property); + if (!$docBlock) { + return; + } + + $shortDescription = $docBlock->getShortDescription(); + if ($shortDescription) { + return $shortDescription; + } + + foreach ($docBlock->getTagsByName('var') as $var) { + $parsedDescription = $var->getParsedDescription(); + + if (isset($parsedDescription[0]) && '' !== $parsedDescription[0]) { + return $parsedDescription[0]; + } + } + } + + /** + * {@inheritdoc} + */ + public function getLongDescription($class, $property, array $context = array()) + { + list($docBlock) = $this->getDocBlock($class, $property); + if (!$docBlock) { + return; + } + + $contents = $docBlock->getLongDescription()->getContents(); + + return '' === $contents ? null : $contents; + } + + /** + * {@inheritdoc} + */ + public function getTypes($class, $property, array $context = array()) + { + list($docBlock, $source, $prefix) = $this->getDocBlock($class, $property); + if (!$docBlock) { + return; + } + + switch ($source) { + case self::PROPERTY: + $tag = 'var'; + break; + + case self::ACCESSOR: + $tag = 'return'; + break; + + case self::MUTATOR: + $tag = 'param'; + break; + } + + $types = array(); + foreach ($docBlock->getTagsByName($tag) as $tag) { + $varTypes = $tag->getTypes(); + + // If null is present, all types are nullable + $nullKey = array_search(Type::BUILTIN_TYPE_NULL, $varTypes); + $nullable = false !== $nullKey; + + // Remove the null type from the type if other types are defined + if ($nullable && count($varTypes) > 1) { + unset($varTypes[$nullKey]); + } + + foreach ($varTypes as $varType) { + $type = $this->createType($varType, $nullable); + if (null !== $type) { + $types[] = $type; + } + } + } + + if (!isset($types[0])) { + return; + } + + if (!in_array($prefix, ReflectionExtractor::$arrayMutatorPrefixes)) { + return $types; + } + + return array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])); + } + + /** + * Gets the FileReflector associated with the class. + * + * @param \ReflectionClass $reflectionClass + * + * @return FileReflector|null + */ + private function getFileReflector(\ReflectionClass $reflectionClass) + { + if (!($fileName = $reflectionClass->getFileName()) || 'hh' === pathinfo($fileName, PATHINFO_EXTENSION)) { + return; + } + + if (isset($this->fileReflectors[$fileName])) { + return $this->fileReflectors[$fileName]; + } + + $this->fileReflectors[$fileName] = new FileReflector($fileName); + $this->fileReflectors[$fileName]->process(); + + return $this->fileReflectors[$fileName]; + } + + /** + * Gets the DocBlock for this property. + * + * @param string $class + * @param string $property + * + * @return array + */ + private function getDocBlock($class, $property) + { + $propertyHash = sprintf('%s::%s', $class, $property); + + if (isset($this->docBlocks[$propertyHash])) { + return $this->docBlocks[$propertyHash]; + } + + $ucFirstProperty = ucfirst($property); + + switch (true) { + case $docBlock = $this->getDocBlockFromProperty($class, $property): + $data = array($docBlock, self::PROPERTY, null); + break; + + case list($docBlock) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::ACCESSOR): + $data = array($docBlock, self::ACCESSOR, null); + break; + + case list($docBlock, $prefix) = $this->getDocBlockFromMethod($class, $ucFirstProperty, self::MUTATOR): + $data = array($docBlock, self::MUTATOR, $prefix); + break; + + default: + $data = array(null, null); + } + + return $this->docBlocks[$propertyHash] = $data; + } + + /** + * Gets the DocBlock from a property. + * + * @param string $class + * @param string $property + * + * @return DocBlock|null + */ + private function getDocBlockFromProperty($class, $property) + { + // Use a ReflectionProperty instead of $class to get the parent class if applicable + try { + $reflectionProperty = new \ReflectionProperty($class, $property); + } catch (\ReflectionException $reflectionException) { + return; + } + + $reflectionCLass = $reflectionProperty->getDeclaringClass(); + + $fileReflector = $this->getFileReflector($reflectionCLass); + if (!$fileReflector) { + return; + } + + foreach ($fileReflector->getClasses() as $classReflector) { + $className = $this->getClassName($classReflector); + + if ($className === $reflectionCLass->name) { + foreach ($classReflector->getProperties() as $propertyReflector) { + // strip the $ prefix + $propertyName = substr($propertyReflector->getName(), 1); + + if ($propertyName === $property) { + return $propertyReflector->getDocBlock(); + } + } + } + } + } + + /** + * Gets DocBlock from accessor or mutator method. + * + * @param string $class + * @param string $ucFirstProperty + * @param int $type + * + * @return DocBlock|null + */ + private function getDocBlockFromMethod($class, $ucFirstProperty, $type) + { + $prefixes = $type === self::ACCESSOR ? ReflectionExtractor::$accessorPrefixes : ReflectionExtractor::$mutatorPrefixes; + + foreach ($prefixes as $prefix) { + $methodName = $prefix.$ucFirstProperty; + + try { + $reflectionMethod = new \ReflectionMethod($class, $methodName); + + if ( + (self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters()) || + (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1) + ) { + break; + } + } catch (\ReflectionException $reflectionException) { + // Try the next prefix if the method doesn't exist + } + } + + if (!isset($reflectionMethod)) { + return; + } + + $reflectionClass = $reflectionMethod->getDeclaringClass(); + $fileReflector = $this->getFileReflector($reflectionClass); + + if (!$fileReflector) { + return; + } + + foreach ($fileReflector->getClasses() as $classReflector) { + $className = $this->getClassName($classReflector); + + if ($className === $reflectionClass->name) { + if ($methodReflector = $classReflector->getMethod($methodName)) { + return array($methodReflector->getDocBlock(), $prefix); + } + } + } + } + + /** + * Gets the normalized class name (without trailing backslash). + * + * @param ClassReflector $classReflector + * + * @return string + */ + private function getClassName(ClassReflector $classReflector) + { + $className = $classReflector->getName(); + if ('\\' === $className[0]) { + return substr($className, 1); + } + + return $className; + } + + /** + * Creates a {@see Type} from a PHPDoc type. + * + * @param string $docType + * @param bool $nullable + * + * @return Type|null + */ + private function createType($docType, $nullable) + { + // Cannot guess + if (!$docType || 'mixed' === $docType) { + return; + } + + if ($collection = '[]' === substr($docType, -2)) { + $docType = substr($docType, 0, -2); + } + + $docType = $this->normalizeType($docType); + list($phpType, $class) = $this->getPhpTypeAndClass($docType); + + $array = 'array' === $docType; + + if ($collection || $array) { + if ($array || 'mixed' === $docType) { + $collectionKeyType = null; + $collectionValueType = null; + } else { + $collectionKeyType = new Type(Type::BUILTIN_TYPE_INT); + $collectionValueType = new Type($phpType, false, $class); + } + + return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, $collectionKeyType, $collectionValueType); + } + + return new Type($phpType, $nullable, $class); + } + + /** + * Normalizes the type. + * + * @param string $docType + * + * @return string + */ + private function normalizeType($docType) + { + switch ($docType) { + case 'integer': + return 'int'; + + case 'boolean': + return 'bool'; + + // real is not part of the PHPDoc standard, so we ignore it + case 'double': + return 'float'; + + case 'callback': + return 'callable'; + + case 'void': + return 'null'; + + default: + return $docType; + } + } + + /** + * Gets an array containing the PHP type and the class. + * + * @param string $docType + * + * @return array + */ + private function getPhpTypeAndClass($docType) + { + if (in_array($docType, Type::$builtinTypes)) { + return array($docType, null); + } + + return array('object', substr($docType, 1)); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php new file mode 100644 index 000000000000..322b645b80e5 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -0,0 +1,341 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Extracts PHP informations using the reflection API. + * + * @author Kévin Dunglas + */ +class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface +{ + /** + * @internal + * + * @var string[] + */ + public static $mutatorPrefixes = array('add', 'remove', 'set'); + + /** + * @internal + * + * @var string[] + */ + public static $accessorPrefixes = array('is', 'can', 'get'); + + /** + * @internal + * + * @var array[] + */ + public static $arrayMutatorPrefixes = array('add', 'remove'); + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = array()) + { + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException $reflectionException) { + return; + } + + $properties = array(); + foreach ($reflectionClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflectionProperty) { + $properties[$reflectionProperty->name] = true; + } + + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { + $propertyName = $this->getPropertyName($reflectionMethod->name); + if ($propertyName) { + $properties[$propertyName] = true; + } + } + + return array_keys($properties); + } + + /** + * {@inheritdoc} + */ + public function getTypes($class, $property, array $context = array()) + { + if ($fromMutator = $this->extractFromMutator($class, $property)) { + return $fromMutator; + } + + if ($fromAccessor = $this->extractFromAccessor($class, $property)) { + return $fromAccessor; + } + } + + /** + * {@inheritdoc} + */ + public function isReadable($class, $property, array $context = array()) + { + if ($this->isPublicProperty($class, $property)) { + return true; + } + + list($reflectionMethod) = $this->getAccessorMethod($class, $property); + + return null !== $reflectionMethod; + } + + /** + * {@inheritdoc} + */ + public function isWritable($class, $property, array $context = array()) + { + if ($this->isPublicProperty($class, $property)) { + return true; + } + + list($reflectionMethod) = $this->getMutatorMethod($class, $property); + + return null !== $reflectionMethod; + } + + /** + * Tries to extract type information from mutators. + * + * @param string $class + * @param string $property + * + * @return Type[]|null + */ + private function extractFromMutator($class, $property) + { + list($reflectionMethod, $prefix) = $this->getMutatorMethod($class, $property); + if (null === $reflectionMethod) { + return; + } + + $reflectionParameters = $reflectionMethod->getParameters(); + $reflectionParameter = $reflectionParameters[0]; + + $arrayMutator = in_array($prefix, self::$arrayMutatorPrefixes); + + if (method_exists($reflectionParameter, 'getType') && $reflectionType = $reflectionParameter->getType()) { + $fromReflectionType = $this->extractFromReflectionType($reflectionType); + + if (!$arrayMutator) { + return array($fromReflectionType); + } + + $phpType = Type::BUILTIN_TYPE_ARRAY; + $collectionKeyType = new Type(Type::BUILTIN_TYPE_INT); + $collectionValueType = $fromReflectionType; + } + + if ($reflectionParameter->isArray()) { + $phpType = Type::BUILTIN_TYPE_ARRAY; + $collection = true; + } + + if ($arrayMutator) { + $collection = true; + $nullable = false; + $collectionNullable = $reflectionParameter->allowsNull(); + } else { + $nullable = $reflectionParameter->allowsNull(); + $collectionNullable = false; + } + + if (!isset($collection)) { + $collection = false; + } + + if (method_exists($reflectionParameter, 'isCallable') && $reflectionParameter->isCallable()) { + $phpType = Type::BUILTIN_TYPE_CALLABLE; + } + + if ($typeHint = $reflectionParameter->getClass()) { + if ($collection) { + $phpType = Type::BUILTIN_TYPE_ARRAY; + $collectionKeyType = new Type(Type::BUILTIN_TYPE_INT); + $collectionValueType = new Type(Type::BUILTIN_TYPE_OBJECT, $collectionNullable, $typeHint->name); + } else { + $phpType = Type::BUILTIN_TYPE_OBJECT; + $typeClass = $typeHint->name; + } + } + + // Nothing useful extracted + if (!isset($phpType)) { + return; + } + + return array( + new Type( + $phpType, + $nullable, + isset($typeClass) ? $typeClass : null, + $collection, + isset($collectionKeyType) ? $collectionKeyType : null, + isset($collectionValueType) ? $collectionValueType : null + ), + ); + } + + /** + * Tries to extract type information from accessors. + * + * @param string $class + * @param string $property + * + * @return Type[]|null + */ + private function extractFromAccessor($class, $property) + { + list($reflectionMethod, $prefix) = $this->getAccessorMethod($class, $property); + if (null === $reflectionMethod) { + return; + } + + if (method_exists($reflectionMethod, 'getReturnType') && $reflectionType = $reflectionMethod->getReturnType()) { + return array($this->extractFromReflectionType($reflectionType)); + } + + if (in_array($prefix, array('is', 'can'))) { + return array(new Type(Type::BUILTIN_TYPE_BOOL)); + } + } + + /** + * Extracts data from the PHP 7 reflection type. + * + * @param \ReflectionType $reflectionType + * + * @return Type + */ + private function extractFromReflectionType(\ReflectionType $reflectionType) + { + $phpTypeOrClass = (string) $reflectionType; + $nullable = $reflectionType->allowsNull(); + + if ($reflectionType->isBuiltin()) { + if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) { + $type = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true); + } else { + $type = new Type($phpTypeOrClass, $nullable); + } + } else { + $type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $phpTypeOrClass); + } + + return $type; + } + + /** + * Does the class have the given public property? + * + * @param string $class + * @param string $property + * + * @return bool + */ + private function isPublicProperty($class, $property) + { + try { + $reflectionProperty = new \ReflectionProperty($class, $property); + + return $reflectionProperty->isPublic(); + } catch (\ReflectionException $reflectionExcetion) { + // Return false if the property doesn't exist + } + + return false; + } + + /** + * Gets the accessor method. + * + * Returns an array with a the instance of \ReflectionMethod as first key + * and the prefix of the method as second or null if not found. + * + * @param string $class + * @param string $property + * + * @return array|null + */ + private function getAccessorMethod($class, $property) + { + $ucProperty = ucfirst($property); + + foreach (self::$accessorPrefixes as $prefix) { + try { + $reflectionMethod = new \ReflectionMethod($class, $prefix.$ucProperty); + + if (0 === $reflectionMethod->getNumberOfRequiredParameters()) { + return array($reflectionMethod, $prefix); + } + } catch (\ReflectionException $reflectionException) { + // Return null if the property doesn't exist + } + } + + return; + } + + /** + * Gets the mutator method. + * + * Returns an array with a the instance of \ReflectionMethod as first key + * and the prefix of the method as second or null if not found. + * + * @param string $class + * @param string $property + * + * @return array + */ + private function getMutatorMethod($class, $property) + { + $ucProperty = ucfirst($property); + + foreach (self::$mutatorPrefixes as $prefix) { + try { + $reflectionMethod = new \ReflectionMethod($class, $prefix.$ucProperty); + + // Parameter can be optional to allow things like: method(array $foo = null) + if ($reflectionMethod->getNumberOfParameters() >= 1) { + return array($reflectionMethod, $prefix); + } + } catch (\ReflectionException $reflectionException) { + // Try the next prefix if the method doesn't exist + } + } + } + + /** + * Extracts a property name from a method name. + * + * @param string $methodName + * + * @return string + */ + private function getPropertyName($methodName) + { + $pattern = implode('|', array_merge(self::$accessorPrefixes, self::$mutatorPrefixes)); + + if (preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) { + return $matches[2]; + } + } +} diff --git a/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php new file mode 100644 index 000000000000..b7edb8fa246b --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * Lists available properties using Symfony Serializer Component metadata. + * + * @author Kévin Dunglas + */ +class SerializerExtractor implements PropertyListExtractorInterface +{ + /** + * @var ClassMetadataFactoryInterface + */ + private $classMetadataFactory; + + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory) + { + $this->classMetadataFactory = $classMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = array()) + { + if (!isset($context['serializer_groups']) || !is_array($context['serializer_groups'])) { + return; + } + + if (!$this->classMetadataFactory->getMetadataFor($class)) { + return; + } + + $properties = array(); + $serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class); + + foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { + if (count(array_intersect($context['serializer_groups'], $serializerAttributeMetadata->getGroups())) > 0) { + $properties[] = $serializerAttributeMetadata->getName(); + } + } + + return $properties; + } +} diff --git a/src/Symfony/Component/PropertyInfo/LICENSE b/src/Symfony/Component/PropertyInfo/LICENSE new file mode 100644 index 000000000000..a69485974dab --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/PropertyInfo/PropertyAccessExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyAccessExtractorInterface.php new file mode 100644 index 000000000000..ecf44d9d656c --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyAccessExtractorInterface.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Guesses if the property can be accessed or mutated. + * + * @author Kévin Dunglas + */ +interface PropertyAccessExtractorInterface +{ + /** + * Is the property readable? + * + * @param string $class + * @param string $property + * @param array $context + * + * @return bool|null + */ + public function isReadable($class, $property, array $context = array()); + + /** + * Is the property writable? + * + * @param string $class + * @param string $property + * @param array $context + * + * @return bool|null + */ + public function isWritable($class, $property, array $context = array()); +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyDescriptionExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyDescriptionExtractorInterface.php new file mode 100644 index 000000000000..a2e98d0febb2 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyDescriptionExtractorInterface.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Description extractor Interface. + * + * @author Kévin Dunglas + */ +interface PropertyDescriptionExtractorInterface +{ + /** + * Gets the short description of the property. + * + * @param string $class + * @param string $property + * @param array $context + * + * @return string|null + */ + public function getShortDescription($class, $property, array $context = array()); + + /** + * Gets the long description of the property. + * + * @param string $class + * @param string $property + * @param array $context + * + * @return string|null + */ + public function getLongDescription($class, $property, array $context = array()); +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php new file mode 100644 index 000000000000..942318af605d --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Default {@see PropertyInfoExtractorInterface} implementation. + * + * @author Kévin Dunglas + */ +class PropertyInfoExtractor implements PropertyInfoExtractorInterface +{ + /** + * @var PropertyListExtractorInterface[] + */ + private $listExtractors; + + /** + * @var PropertyTypeExtractorInterface[] + */ + private $typeExtractors; + + /** + * @var PropertyDescriptionExtractorInterface[] + */ + private $descriptionExtractors; + + /** + * @var PropertyAccessExtractorInterface[] + */ + private $accessExtractors; + + /** + * @param PropertyListExtractorInterface[] $listExtractors + * @param PropertyTypeExtractorInterface[] $typeExtractors + * @param PropertyDescriptionExtractorInterface[] $descriptionExtractors + * @param PropertyAccessExtractorInterface[] $accessExtractors + */ + public function __construct(array $listExtractors = array(), array $typeExtractors = array(), array $descriptionExtractors = array(), array $accessExtractors = array()) + { + $this->listExtractors = $listExtractors; + $this->typeExtractors = $typeExtractors; + $this->descriptionExtractors = $descriptionExtractors; + $this->accessExtractors = $accessExtractors; + } + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = array()) + { + return $this->extract($this->listExtractors, 'getProperties', array($class, $context)); + } + + /** + * {@inheritdoc} + */ + public function getShortDescription($class, $property, array $context = array()) + { + return $this->extract($this->descriptionExtractors, 'getShortDescription', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function getLongDescription($class, $property, array $context = array()) + { + return $this->extract($this->descriptionExtractors, 'getLongDescription', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function getTypes($class, $property, array $context = array()) + { + return $this->extract($this->typeExtractors, 'getTypes', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function isReadable($class, $property, array $context = array()) + { + return $this->extract($this->accessExtractors, 'isReadable', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function isWritable($class, $property, array $context = array()) + { + return $this->extract($this->accessExtractors, 'isWritable', array($class, $property, $context)); + } + + /** + * Iterates over registered extractors and return the first value found. + * + * @param array $extractors + * @param string $method + * @param array $arguments + * + * @return mixed + */ + private function extract(array $extractors, $method, array $arguments) + { + foreach ($extractors as $extractor) { + $value = call_user_func_array(array($extractor, $method), $arguments); + if (null !== $value) { + return $value; + } + } + } +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractorInterface.php new file mode 100644 index 000000000000..8893018653f3 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractorInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Gets info about PHP class properties. + * + * A convenient interface inheriting all specific info interfaces. + * + * @author Kévin Dunglas + */ +interface PropertyInfoExtractorInterface extends PropertyTypeExtractorInterface, PropertyDescriptionExtractorInterface, PropertyAccessExtractorInterface, PropertyListExtractorInterface +{ +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyListExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyListExtractorInterface.php new file mode 100644 index 000000000000..1faae33d0253 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyListExtractorInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Extracts the list of properties available for the given class. + * + * @author Kévin Dunglas + */ +interface PropertyListExtractorInterface +{ + /** + * Gets the list of properties available for the given class. + * + * @param string $class + * @param array $context + * + * @return string[]|null + */ + public function getProperties($class, array $context = array()); +} diff --git a/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php new file mode 100644 index 000000000000..8aa08b4e85e5 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Type Extractor Interface. + * + * @author Kévin Dunglas + */ +interface PropertyTypeExtractorInterface +{ + /** + * Gets types of a property. + * + * @param string $class + * @param string $property + * @param array $context + * + * @return Type[]|null + */ + public function getTypes($class, $property, array $context = array()); +} diff --git a/src/Symfony/Component/PropertyInfo/README.md b/src/Symfony/Component/PropertyInfo/README.md new file mode 100644 index 000000000000..0f1af4019028 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/README.md @@ -0,0 +1,14 @@ +PropertyInfo Component +====================== + +PropertyInfo extracts information about PHP class' properties using metadata +of popular sources. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/PropertyInfo/ + $ composer install + $ phpunit diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php new file mode 100644 index 000000000000..2a1abd75095f --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractors/PhpDocExtractorTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\PhpDocExtractors; + +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Kévin Dunglas + */ +class PhpDocExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PhpDocExtractor + */ + private $extractor; + + public function setUp() + { + $this->extractor = new PhpDocExtractor(); + } + + /** + * @dataProvider typesProvider + */ + public function testExtract($property, array $type = null, $shortDescription, $longDescription) + { + $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); + $this->assertSame($shortDescription, $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); + $this->assertSame($longDescription, $this->extractor->getLongDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); + } + + public function typesProvider() + { + return array( + array('foo', null, 'Short description.', 'Long description.'), + array('bar', array(new Type(Type::BUILTIN_TYPE_STRING)), 'This is bar.', null), + array('baz', array(new Type(Type::BUILTIN_TYPE_INT)), 'Should be used.', null), + array('foo2', array(new Type(Type::BUILTIN_TYPE_FLOAT)), null, null), + array('foo3', array(new Type(Type::BUILTIN_TYPE_CALLABLE)), null, null), + array('foo4', array(new Type(Type::BUILTIN_TYPE_NULL)), null, null), + array('foo5', null, null, null), + array( + 'files', + array( + new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), + new Type(Type::BUILTIN_TYPE_RESOURCE), + ), + null, + null, + ), + array('bal', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')), null, null), + array('parent', array(new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')), null, null), + array('collection', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime'))), null, null), + array('a', array(new Type(Type::BUILTIN_TYPE_INT)), 'A.', null), + array('b', array(new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')), 'B.', null), + array('c', array(new Type(Type::BUILTIN_TYPE_BOOL, true)), null, null), + array('d', array(new Type(Type::BUILTIN_TYPE_BOOL)), null, null), + array('e', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))), null, null), + array('f', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime'))), null, null), + ); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php new file mode 100644 index 000000000000..ba00d1c792ca --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractors/ReflectionExtractorTest.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Extractor; + +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Kévin Dunglas + */ +class ReflectionExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ReflectionExtractor + */ + private $extractor; + + public function setUp() + { + $this->extractor = new ReflectionExtractor(); + } + + public function testGetProperties() + { + $this->assertEquals( + array( + 'bal', + 'parent', + 'collection', + 'foo', + 'foo2', + 'foo3', + 'foo4', + 'foo5', + 'files', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + ), + $this->extractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy') + ); + } + + /** + * @dataProvider typesProvider + */ + public function testExtractors($property, array $type = null) + { + $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property, array())); + } + + public function typesProvider() + { + return array( + array('a', null), + array('b', array(new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy'))), + array('c', array(new Type(Type::BUILTIN_TYPE_BOOL))), + array('d', array(new Type(Type::BUILTIN_TYPE_BOOL))), + array('e', null), + array('f', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')))), + ); + } + + /** + * @dataProvider php7TypesProvider + */ + public function testExtractPhp7Type($property, array $type = null) + { + if (!method_exists('\ReflectionMethod', 'getReturnType')) { + $this->markTestSkipped('Available only with PHP 7 and superior.'); + } + + $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy', $property, array())); + } + + public function php7TypesProvider() + { + return array( + array('foo', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true))), + array('bar', array(new Type(Type::BUILTIN_TYPE_INT))), + array('baz', array(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)))), + ); + } + + public function testIsReadable() + { + $this->assertFalse($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'bar', array())); + $this->assertFalse($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'baz', array())); + $this->assertTrue($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'parent', array())); + $this->assertTrue($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'a', array())); + $this->assertFalse($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'b', array())); + $this->assertTrue($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'c', array())); + $this->assertTrue($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'd', array())); + $this->assertFalse($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'e', array())); + $this->assertFalse($this->extractor->isReadable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'f', array())); + } + + public function testIsWritable() + { + $this->assertFalse($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'bar', array())); + $this->assertFalse($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'baz', array())); + $this->assertTrue($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'parent', array())); + $this->assertFalse($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'a', array())); + $this->assertTrue($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'b', array())); + $this->assertFalse($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'c', array())); + $this->assertFalse($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'd', array())); + $this->assertTrue($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'e', array())); + $this->assertTrue($this->extractor->isWritable('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', 'f', array())); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php new file mode 100644 index 000000000000..c170f2237add --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\PropertyInfo\Tests\Extractors; + +use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + +/** + * @author Kévin Dunglas + */ +class SerializerExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var SerializerExtractor + */ + private $extractor; + + public function setUp() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $this->extractor = new SerializerExtractor($classMetadataFactory); + } + + public function testGetProperties() + { + $this->assertEquals( + array('collection'), + $this->extractor->getProperties('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', array('serializer_groups' => array('a'))) + ); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php new file mode 100644 index 000000000000..8e80c0b59a9a --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @author Kévin Dunglas + */ +class Dummy extends ParentDummy +{ + /** + * @var string This is bar. + */ + private $bar; + + /** + * Should be used. + * + * @var int Should be ignored. + */ + protected $baz; + + /** + * @var \DateTime + */ + public $bal; + + /** + * @var ParentDummy + */ + public $parent; + + /** + * @var \DateTime[] + * @Groups({"a", "b"}) + */ + public $collection; + + /** + * A. + * + * @return int + */ + public function getA() + { + } + + /** + * B. + * + * @param ParentDummy|null $parent + */ + public function setB(ParentDummy $parent = null) + { + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php new file mode 100644 index 000000000000..cfabcf2a9021 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Kévin Dunglas + */ +class DummyExtractor implements PropertyListExtractorInterface, PropertyDescriptionExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface +{ + /** + * {@inheritdoc} + */ + public function getShortDescription($class, $property, array $context = array()) + { + return 'short'; + } + + /** + * {@inheritdoc} + */ + public function getLongDescription($class, $property, array $context = array()) + { + return 'long'; + } + + /** + * {@inheritdoc} + */ + public function getTypes($class, $property, array $context = array()) + { + return array(new Type(Type::BUILTIN_TYPE_INT)); + } + + /** + * {@inheritdoc} + */ + public function isReadable($class, $property, array $context = array()) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function isWritable($class, $property, array $context = array()) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = array()) + { + return array('a', 'b'); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php new file mode 100644 index 000000000000..330496827cfc --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +/** + * @author Kévin Dunglas + */ +class ParentDummy +{ + /** + * Short description. + * + * Long description. + */ + public $foo; + + /** + * @var float + */ + public $foo2; + + /** + * @var callback + */ + public $foo3; + + /** + * @var void + */ + public $foo4; + + /** + * @var mixed + */ + public $foo5; + + /** + * @var \SplFileInfo[]|resource + */ + public $files; + + /** + * @return bool|null + */ + public function isC() + { + } + + /** + * @return bool + */ + public function canD() + { + } + + /** + * @param resource $e + */ + public function addE($e) + { + } + + /** + * @param \DateTime $f + */ + public function removeF(\DateTime $f) + { + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php7Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php7Dummy.php new file mode 100644 index 000000000000..cd5ba380d9d5 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php7Dummy.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +/** + * @author Kévin Dunglas + */ +class Php7Dummy +{ + public function getFoo(): array + { + } + + public function setBar(int $bar) + { + } + + public function addBaz(string $baz) + { + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php new file mode 100644 index 000000000000..84e4af5c89ef --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\PropertyInfo\Tests; + +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Kévin Dunglas + */ +class PropertyInfoExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PropertyInfoExtractor + */ + private $propertyInfo; + + public function setUp() + { + $extractors = array(new DummyExtractor()); + $this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors); + } + + public function testInstanceOf() + { + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface', $this->propertyInfo); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface', $this->propertyInfo); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface', $this->propertyInfo); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface', $this->propertyInfo); + } + + public function testGetShortDescription() + { + $this->assertSame('short', $this->propertyInfo->getShortDescription('Foo', 'bar', array())); + } + + public function testGetLongDescription() + { + $this->assertSame('long', $this->propertyInfo->getLongDescription('Foo', 'bar', array())); + } + + public function testGetTypes() + { + $this->assertEquals(array(new Type(Type::BUILTIN_TYPE_INT)), $this->propertyInfo->getTypes('Foo', 'bar', array())); + } + + public function testIsReadable() + { + $this->assertTrue($this->propertyInfo->isReadable('Foo', 'bar', array())); + } + + public function testIsWritable() + { + $this->assertTrue($this->propertyInfo->isWritable('Foo', 'bar', array())); + } + + public function testGetProperties() + { + $this->assertEquals(array('a', 'b'), $this->propertyInfo->getProperties('Foo')); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php new file mode 100644 index 000000000000..7663cfc63684 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\PropertyInfo\Tests; + +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Kévin Dunglas + */ +class TypeTest extends \PHPUnit_Framework_TestCase +{ + public function testConstruct() + { + $type = new Type('object', true, 'ArrayObject', true, new Type('int'), new Type('string')); + + $this->assertEquals(Type::BUILTIN_TYPE_OBJECT, $type->getBuiltinType()); + $this->assertTrue($type->isNullable()); + $this->assertEquals('ArrayObject', $type->getClassName()); + $this->assertTrue($type->isCollection()); + + $collectionKeyType = $type->getCollectionKeyType(); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\Type', $collectionKeyType); + $this->assertEquals(Type::BUILTIN_TYPE_INT, $collectionKeyType->getBuiltinType()); + + $collectionValueType = $type->getCollectionValueType(); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\Type', $collectionValueType); + $this->assertEquals(Type::BUILTIN_TYPE_STRING, $collectionValueType->getBuiltinType()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage "foo" is not a valid PHP type. + */ + public function testInvalidType() + { + new Type('foo'); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Type.php b/src/Symfony/Component/PropertyInfo/Type.php new file mode 100644 index 000000000000..8a55a7cbc29e --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Type.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +/** + * Type value object (immutable). + * + * @author Kévin Dunglas + */ +class Type +{ + const BUILTIN_TYPE_INT = 'int'; + const BUILTIN_TYPE_FLOAT = 'float'; + const BUILTIN_TYPE_STRING = 'string'; + const BUILTIN_TYPE_BOOL = 'bool'; + const BUILTIN_TYPE_RESOURCE = 'resource'; + const BUILTIN_TYPE_OBJECT = 'object'; + const BUILTIN_TYPE_ARRAY = 'array'; + const BUILTIN_TYPE_NULL = 'null'; + const BUILTIN_TYPE_CALLABLE = 'callable'; + + /** + * List of PHP builtin types. + * + * @var string[] + */ + public static $builtinTypes = array( + self::BUILTIN_TYPE_INT, + self::BUILTIN_TYPE_FLOAT, + self::BUILTIN_TYPE_STRING, + self::BUILTIN_TYPE_BOOL, + self::BUILTIN_TYPE_RESOURCE, + self::BUILTIN_TYPE_OBJECT, + self::BUILTIN_TYPE_ARRAY, + self::BUILTIN_TYPE_CALLABLE, + self::BUILTIN_TYPE_NULL, + ); + + /** + * @var string + */ + private $builtinType; + + /** + * @var bool + */ + private $nullable; + + /** + * @var string|null + */ + private $class; + + /** + * @var bool + */ + private $collection; + + /** + * @var Type|null + */ + private $collectionKeyType; + + /** + * @var Type|null + */ + private $collectionValueType; + + /** + * @param string $builtinType + * @param bool $nullable + * @param string|null $class + * @param bool $collection + * @param Type|null $collectionKeyType + * @param Type|null $collectionValueType + * + * @throws \InvalidArgumentException + */ + public function __construct($builtinType, $nullable = false, $class = null, $collection = false, Type $collectionKeyType = null, Type $collectionValueType = null) + { + if (!in_array($builtinType, self::$builtinTypes)) { + throw new \InvalidArgumentException(sprintf('"%s" is not a valid PHP type.', $builtinType)); + } + + $this->builtinType = $builtinType; + $this->nullable = $nullable; + $this->class = $class; + $this->collection = $collection; + $this->collectionKeyType = $collectionKeyType; + $this->collectionValueType = $collectionValueType; + } + + /** + * Gets built-in type. + * + * Can be bool, int, float, string, array, object, resource, null or callback. + * + * @return string + */ + public function getBuiltinType() + { + return $this->builtinType; + } + + /** + * Allows null value? + * + * @return bool + */ + public function isNullable() + { + return $this->nullable; + } + + /** + * Gets the class name. + * + * Only applicable if the built-in type is object. + * + * @return string|null + */ + public function getClassName() + { + return $this->class; + } + + /** + * Is collection? + * + * @return bool + */ + public function isCollection() + { + return $this->collection; + } + + /** + * Gets collection key type. + * + * Only applicable for a collection type. + * + * @return Type|null + */ + public function getCollectionKeyType() + { + return $this->collectionKeyType; + } + + /** + * Gets collection value type. + * + * Only applicable for a collection type. + * + * @return Type|null + */ + public function getCollectionValueType() + { + return $this->collectionValueType; + } +} diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json new file mode 100644 index 000000000000..a9826d42d71c --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/composer.json @@ -0,0 +1,51 @@ +{ + "name": "symfony/property-info", + "type": "library", + "description": "Symfony Property Info Component", + "keywords": [ + "property", + "type", + "PHPDoc", + "symfony", + "validator", + "doctrine" + ], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7", + "symfony/serializer": "~2.7", + "phpdocumentor/reflection": "^1.0.7", + "doctrine/annotations": "~1.0" + }, + "conflict": { + "phpdocumentor/reflection": "<1.0.7" + }, + "suggest": { + "symfony/doctrine-bridge": "To use Doctrine metadata", + "phpdocumentor/reflection": "To use the PHPDoc", + "symfony/serializer": "To use Serializer metadata" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\PropertyInfo\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + } +} diff --git a/src/Symfony/Component/PropertyInfo/phpunit.xml.dist b/src/Symfony/Component/PropertyInfo/phpunit.xml.dist new file mode 100644 index 000000000000..518e472db748 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + +