diff --git a/src/Symfony/Component/ValueExporter/.gitignore b/src/Symfony/Component/ValueExporter/.gitignore new file mode 100644 index 0000000000000..5414c2c655e72 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor/ diff --git a/src/Symfony/Component/ValueExporter/CHANGELOG.md b/src/Symfony/Component/ValueExporter/CHANGELOG.md new file mode 100644 index 0000000000000..58eb13c207df2 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +3.2.0 +----- + + * introducing the component diff --git a/src/Symfony/Component/ValueExporter/Exception/InvalidFormatterException.php b/src/Symfony/Component/ValueExporter/Exception/InvalidFormatterException.php new file mode 100644 index 0000000000000..a8ae16aa43680 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Exception/InvalidFormatterException.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\ValueExporter\Exception; + +/** + * Thrown when a {@link \Symfony\Component\ValueExporter\Formatter\FormatterInterface} + * is not supported by the {@link \Symfony\Component\ValueExporter\Exporter\ValueExporterInterface}. + * + * @author Jules Pietri + */ +class InvalidFormatterException extends \InvalidArgumentException +{ + /** + * @param string $formatterClass The invalid formatter class + * @param string $exporterClass The exporter class + * @param string $expectedInterface The expected formatter interface + */ + public function __construct($formatterClass, $exporterClass, $expectedInterface) + { + parent::__construct(sprintf('The exporter "%s" expects formatters implementing "%", but was given "%s" class.', $exporterClass, $expectedInterface, $formatterClass)); + } +} diff --git a/src/Symfony/Component/ValueExporter/Exporter/AbstractValueExporter.php b/src/Symfony/Component/ValueExporter/Exporter/AbstractValueExporter.php new file mode 100644 index 0000000000000..11c0bffef0da4 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Exporter/AbstractValueExporter.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Exporter; + +use Symfony\Component\ValueExporter\Exception\InvalidFormatterException; +use Symfony\Component\ValueExporter\Formatter\ExpandedFormatterTrait; +use Symfony\Component\ValueExporter\Formatter\FormatterInterface; + +/** + * ValueExporterInterface implementations export PHP values. + * + * @author Jules Pietri + */ +abstract class AbstractValueExporter implements ValueExporterInterface +{ + /** + * @var int + */ + protected $depth; + /** + * @var bool + */ + protected $expand; + + /** + * The supported formatter interface. + * + * @var string + */ + protected $formatterInterface = FormatterInterface::class; + + /** + * An array indexed by formatter FQCN with a corresponding priority as value. + * + * @var int[] + */ + private $formatters = array(); + + /** + * An array of formatters instances sorted by priority or null. + * + * @var FormatterInterface[]|null + */ + private $sortedFormatters; + + /** + * An array of cached formatters instances by their FQCN. + * + * @var FormatterInterface[] + */ + private $cachedFormatters = array(); + + /** + * Takes {@link FormatterInterface} FQCN as arguments. + * + * They will be called in the given order. + * Alternatively, instead of a class, you can pass an array with + * a class and its priority {@see self::addFormatters}. + */ + final public function __construct() + { + $this->addFormatters(func_get_args()); + } + + /** + * {@inheritdoc} + */ + final public function addFormatters(array $formatters) + { + $this->sortedFormatters = null; + + foreach ($formatters as $formatter) { + if (is_array($formatter)) { + $priority = (int) $formatter[1]; + $formatterClass = $formatter[0]; + } else { + $priority = 0; + $formatterClass = $formatter; + } + + if (!in_array($this->formatterInterface, class_implements($formatterClass), true)) { + throw new InvalidFormatterException($formatterClass, static::class, $this->formatterInterface); + } + + // Using the class as key prevents duplicate and allows to + // dynamically change the priority + $this->formatters[$formatterClass] = $priority; + } + } + + /** + * @return FormatterInterface[] + */ + final protected function formatters() + { + if (null === $this->sortedFormatters) { + arsort($this->formatters); + + foreach (array_keys($this->formatters) as $formatterClass) { + if (isset($this->cachedFormatters[$formatterClass])) { + $this->sortedFormatters[] = $this->cachedFormatters[$formatterClass]; + + continue; + } + + $formatter = new $formatterClass(); + + if (in_array(ExpandedFormatterTrait::class, class_uses($formatterClass), true)) { + /* @var ExpandedFormatterTrait $formatter */ + $formatter->setExporter($this); + } + + $this->sortedFormatters[] = $this->cachedFormatters[$formatterClass] = $formatter; + } + } + + return $this->sortedFormatters; + } +} diff --git a/src/Symfony/Component/ValueExporter/Exporter/ValueExporterInterface.php b/src/Symfony/Component/ValueExporter/Exporter/ValueExporterInterface.php new file mode 100644 index 0000000000000..cdc9275a72637 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Exporter/ValueExporterInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Exporter; + +use Symfony\Component\ValueExporter\Exception\InvalidFormatterException; +use Symfony\Component\ValueExporter\Exporter; +use Symfony\Component\ValueExporter\Formatter\FormatterInterface; + +/** + * ValueExporterInterface implementations export PHP values. + * + * An implementation can rely on {@link FormatterInterface} implementations + * to handle specific types of value. + * + * @author Jules Pietri + */ +interface ValueExporterInterface +{ + /** + * Exports a PHP value. + * + * ValueExporter instance should always deal with array or \Traversable + * values first in order to handle depth and expand arguments. + * + * Usually you don't need to define the depth but it will be incremented + * in recursive calls. When expand is false any expandable values such as + * arrays or objects should be inline in their exported representation. + * + * @param mixed $value The PHP value to export + * @param int $depth The level of indentation + * @param bool $expand Whether to inline or expand nested values + */ + public function exportValue($value, $depth = 1, $expand = false); + + /** + * Adds {@link FormatterInterface} that will be called by priority. + * + * @param (FormatterInterface|array)[] $formatters + * + * @throws InvalidFormatterException If the exporter does not support a given formatter + */ + public function addFormatters(array $formatters); +} diff --git a/src/Symfony/Component/ValueExporter/Exporter/ValueToStringExporter.php b/src/Symfony/Component/ValueExporter/Exporter/ValueToStringExporter.php new file mode 100644 index 0000000000000..480c6bf7a5abb --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Exporter/ValueToStringExporter.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Exporter; + +use Symfony\Component\ValueExporter\Formatter\StringFormatterInterface; + +/** + * @author Fabien Potencier + * @author Bernhard Schussek + * @author Quentin Schuler + * @author Jules Pietri + */ +class ValueToStringExporter extends AbstractValueExporter +{ + protected $formatterInterface = StringFormatterInterface::class; + + public function exportValue($value, $depth = 1, $expand = false) + { + // Use set properties for recursive calls + $depth = null === $this->depth ? $depth : $this->depth; + $expand = null === $this->expand ? $expand : $this->expand; + // Arrays have to be handled first to deal with nested level and depth, + // this implementation intentionally ignores \Traversable values. + // Therefor, \Traversable instances might be treated as objects unless + // implementing a {@link StringFormatterInterface} and passing it to + // the exporter in order to support them. + if (is_array($value) && !is_callable($value)) { + if (empty($value)) { + return 'array()'; + } + $indent = str_repeat(' ', $depth); + + $a = array(); + foreach ($value as $k => $v) { + if (is_array($v) && !empty($v)) { + $this->expand = true; + $this->depth = $depth + 1; + } + $a[] = sprintf('%s => %s', is_string($k) ? sprintf("'%s'", $k) : $k, $this->exportValue($v)); + $this->depth = null; + $this->expand = null; + } + if ($expand) { + return sprintf("array(\n%s%s\n%s)", $indent, implode(sprintf(", \n%s", $indent), $a), str_repeat(' ', $depth - 1)); + } + + $s = sprintf('array(%s)', implode(', ', $a)); + + if (80 > strlen($s)) { + return $s; + } + + return sprintf("array(\n%s%s\n)", $indent, implode(sprintf(",\n%s", $indent), $a)); + } + // Not an array, test each formatter + foreach ($this->formatters() as $formatter) { + /** @var StringFormatterInterface $formatter */ + if ($formatter->supports($value)) { + return $formatter->formatToString($value); + } + } + // Fallback on default + if (is_object($value)) { + if (method_exists($value, '__toString')) { + return sprintf('object(%s) "%s"', get_class($value), $value); + } + + return sprintf('object(%s)', get_class($value)); + } + if (is_resource($value)) { + return sprintf('resource(%s#%d)', get_resource_type($value), $value); + } + if (is_float($value)) { + return sprintf('(float) %s', $value); + } + if (is_int($value)) { + return sprintf('(int) %d', $value); + } + if (is_string($value)) { + return sprintf('"%s"', $value); + } + if (null === $value) { + return 'null'; + } + if (false === $value) { + return 'false'; + } + if (true === $value) { + return 'true'; + } + + return (string) $value; + } +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/CallableToStringFormatter.php b/src/Symfony/Component/ValueExporter/Formatter/CallableToStringFormatter.php new file mode 100644 index 0000000000000..8240919bf01eb --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/CallableToStringFormatter.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Formatter; + +/** + * Returns a string representation of a string or array callable. + * + * @author Jules Pietri + */ +class CallableToStringFormatter implements StringFormatterInterface +{ + /** + * {@inheritdoc} + */ + public function supports($value) + { + return is_callable($value) && !$value instanceof \Closure; + } + + /** + * {@inheritdoc} + */ + public function formatToString($value) + { + if (is_string($value)) { + return sprintf('(function) "%s"', $value); + } + + $caller = is_object($value) ? get_class($value) : (is_object($value[0]) ? get_class($value[0]) : $value[0]); + if (is_object($value) || (is_object($value[0]) && isset($value[1]) && '__invoke' === $value[1])) { + return sprintf('(invokable) "%s"', $caller); + } + + $method = $value[1]; + if (false !== $cut = strpos($method, $caller)) { + $method = substr($method, $cut); + } + + if ((new \ReflectionMethod($caller, $method))->isStatic()) { + return sprintf('(static) "%s::%s"', $caller, $method); + } + + return sprintf('(callable) "%s::%s"', $caller, $method); + } +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/DateTimeToStringFormatter.php b/src/Symfony/Component/ValueExporter/Formatter/DateTimeToStringFormatter.php new file mode 100644 index 0000000000000..efa4b0b6ccc26 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/DateTimeToStringFormatter.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Formatter; + +/** + * Returns a string representation of a DateTimeInterface instance. + * + * Based on the contribution by @scuben (https://github.com/scuben) + * https://github.com/symfony/symfony/commit/a1762fb65423dc94d69c5fb6abaed37f2ad576e6 + * + * @author Jules Pietri + */ +class DateTimeToStringFormatter implements StringFormatterInterface +{ + /** + * {@inheritdoc} + */ + public function supports($value) + { + return $value instanceof \DateTimeInterface; + } + + /** + * {@inheritdoc} + */ + public function formatToString($value) + { + return sprintf('object(%s) - %s', get_class($value), $value->format(\DateTime::ISO8601)); + } +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/EntityToStringFormatter.php b/src/Symfony/Component/ValueExporter/Formatter/EntityToStringFormatter.php new file mode 100644 index 0000000000000..162fc2fe0dd22 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/EntityToStringFormatter.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Formatter; + +/** + * Returns a string representation of a DateTimeInterface instance. + * + * Based on the contribution by @scuben (https://github.com/scuben) + * https://github.com/symfony/symfony/commit/a1762fb65423dc94d69c5fb6abaed37f2ad576e6 + * + * @author Jules Pietri + */ +class EntityToStringFormatter implements StringFormatterInterface +{ + /** + * {@inheritdoc} + */ + public function supports($value) + { + return is_object($value) + && !$value instanceof \Closure + && (isset($value->id) || is_callable(array($value, 'id')) || is_callable(array($value, 'getId'))) + ; + } + + /** + * {@inheritdoc} + */ + public function formatToString($value) + { + $id = isset($value->id) ? $value->id : (is_callable(array($value, 'id')) ? $value->id() : $value->getId()); + + if (method_exists($value, '__toString')) { + return sprintf('entity:%s(%s) "%s"', $id, get_class($value), $value); + } + + return sprintf('entity:%s(%s)', $id, get_class($value)); + } +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/ExpandedFormatterTrait.php b/src/Symfony/Component/ValueExporter/Formatter/ExpandedFormatterTrait.php new file mode 100644 index 0000000000000..24816b818d25d --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/ExpandedFormatterTrait.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Formatter; + +use Symfony\Component\ValueExporter\Exporter\ValueExporterInterface; + +/** + * ExpandedFormatter. + * + * A trait holding the {@link ValueExporterInterface} to export nested values. + * + * @author Jules Pietri + */ +trait ExpandedFormatterTrait +{ + /** + * @var ValueExporterInterface + */ + private $exporter; + + /** + * Sets the exporter to call on nested values. + * + * @param ValueExporterInterface $exporter The exporter + */ + final public function setExporter(ValueExporterInterface $exporter) + { + $this->exporter = $exporter; + } + + /** + * @param mixed $value The nested value to export + * + * @return mixed The exported nested value + */ + final protected function export($value) + { + return $this->exporter->exportValue($value); + } +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/FormatterInterface.php b/src/Symfony/Component/ValueExporter/Formatter/FormatterInterface.php new file mode 100644 index 0000000000000..e32504905b05a --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/FormatterInterface.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\ValueExporter\Formatter; + +/** + * FormatterInterface. + * + * Returns a formatted representation of (a) supported type(s) of PHP value. + * + * @author Jules Pietri + */ +interface FormatterInterface +{ + /** + * Returns whether the formatter can format the type(s) of the given value. + * + * @param mixed $value The given value to format + * + * @return bool Whether the given value can be formatted + */ + public function supports($value); +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/PhpIncompleteClassToStringFormatter.php b/src/Symfony/Component/ValueExporter/Formatter/PhpIncompleteClassToStringFormatter.php new file mode 100644 index 0000000000000..b3ba602d1bed5 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/PhpIncompleteClassToStringFormatter.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Formatter; + +/** + * Returns a string representation of a __PHP_Incomplete_Class instance. + * + * @author Yonel Ceruto González + * @author Jules Pietri + */ +class PhpIncompleteClassToStringFormatter implements StringFormatterInterface +{ + /** + * {@inheritdoc} + */ + public function supports($value) + { + return $value instanceof \__PHP_Incomplete_Class; + } + + /** + * {@inheritdoc} + */ + public function formatToString($value) + { + return sprintf('__PHP_Incomplete_Class(%s)', $this->getClassNameFromIncomplete($value)); + } + + private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value) + { + $array = new \ArrayObject($value); + + return $array['__PHP_Incomplete_Class_Name']; + } +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/StringFormatterInterface.php b/src/Symfony/Component/ValueExporter/Formatter/StringFormatterInterface.php new file mode 100644 index 0000000000000..8c6fa260e98a3 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/StringFormatterInterface.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\ValueExporter\Formatter; + +/** + * StringFormatter. + * + * Returns a string representation of a given value. + * + * @author Jules Pietri + */ +interface StringFormatterInterface extends FormatterInterface +{ + /** + * Returns a given value formatted to string. + * + * @param mixed $value The given value to format to string + * + * @return string A string representation of the given value + */ + public function formatToString($value); +} diff --git a/src/Symfony/Component/ValueExporter/Formatter/TraversableToStringFormatter.php b/src/Symfony/Component/ValueExporter/Formatter/TraversableToStringFormatter.php new file mode 100644 index 0000000000000..db45b5d193637 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Formatter/TraversableToStringFormatter.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Formatter; + +/** + * Returns a string representation of an instance implementing \Traversable. + * + * @author Jules Pietri + */ +class TraversableToStringFormatter implements StringFormatterInterface +{ + use ExpandedFormatterTrait; + + /** + * {@inheritdoc} + */ + public function supports($value) + { + return $value instanceof \Traversable; + } + + /** + * {@inheritdoc} + */ + public function formatToString($value) + { + $nested = array(); + foreach ($value as $k => $v) { + $nested[] = sprintf('%s => %s', is_string($k) ? sprintf("'%s'", $k) : $k, $this->export($v)); + } + + return sprintf("Traversable:\"%s\"(\n %s\n)", get_class($value), implode(",\n ", $nested)); + } +} diff --git a/src/Symfony/Component/ValueExporter/LICENSE.txt b/src/Symfony/Component/ValueExporter/LICENSE.txt new file mode 100644 index 0000000000000..0564c5a9b7f1f --- /dev/null +++ b/src/Symfony/Component/ValueExporter/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2016 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/ValueExporter/README.md b/src/Symfony/Component/ValueExporter/README.md new file mode 100644 index 0000000000000..cde83ef943a37 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/README.md @@ -0,0 +1,16 @@ +ValueExporter Component +======================= + +The ValueExporter component provides mechanisms to export any arbitrary +PHP variable in a desired format. Built on top, it provides a `to_string()` +function that you can safely use instead of casting `(string) $value`, finely +represented thanks to formatters. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/value_exporter/introduction.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/ValueExporter/Resources/functions/to_string.php b/src/Symfony/Component/ValueExporter/Resources/functions/to_string.php new file mode 100644 index 0000000000000..367fca038e77b --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Resources/functions/to_string.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\ValueExporter\ValueExporter; + +if (!function_exists('to_string')) { + /** + * @author Nicolas Grekas + * @author Jules Pietri + */ + function to_string($value, $depth = 1, $expand = false) + { + return ValueExporter::export($value, $depth, $expand); + } +} diff --git a/src/Symfony/Component/ValueExporter/Tests/Fixtures/Entity.php b/src/Symfony/Component/ValueExporter/Tests/Fixtures/Entity.php new file mode 100644 index 0000000000000..06c38e984552a --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Tests/Fixtures/Entity.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Tests\Fixtures; + +/** + * Entity with an id getter. + * + * @author Jules Pietri + */ +class Entity +{ + private $id; + + public function __construct($id) + { + $this->id = $id; + } + + public function getId() + { + return $this->id; + } +} diff --git a/src/Symfony/Component/ValueExporter/Tests/Fixtures/EntityImplementingToString.php b/src/Symfony/Component/ValueExporter/Tests/Fixtures/EntityImplementingToString.php new file mode 100644 index 0000000000000..370981ce0f73c --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Tests/Fixtures/EntityImplementingToString.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Tests\Fixtures; + +/** + * Entity with an id getter. + * + * @author Jules Pietri + */ +class EntityImplementingToString +{ + public $id; + private $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString() + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Component/ValueExporter/Tests/Fixtures/ObjectImplementingToString.php b/src/Symfony/Component/ValueExporter/Tests/Fixtures/ObjectImplementingToString.php new file mode 100644 index 0000000000000..9455301fd5c90 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Tests/Fixtures/ObjectImplementingToString.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Tests\Fixtures; + +/** + * Entity with an id getter. + * + * @author Jules Pietri + */ +class ObjectImplementingToString +{ + private $name; + + public function __construct($name) + { + $this->name = $name; + } + + public function __toString() + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Component/ValueExporter/Tests/Fixtures/PublicEntity.php b/src/Symfony/Component/ValueExporter/Tests/Fixtures/PublicEntity.php new file mode 100644 index 0000000000000..310dd32b716a6 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Tests/Fixtures/PublicEntity.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Tests\Fixtures; + +/** + * Entity with a public id. + * + * @author Jules Pietri + */ +class PublicEntity +{ + public $id; + + public function __construct($id) + { + $this->id = $id; + } +} diff --git a/src/Symfony/Component/ValueExporter/Tests/Fixtures/TraversableInstance.php b/src/Symfony/Component/ValueExporter/Tests/Fixtures/TraversableInstance.php new file mode 100644 index 0000000000000..6a3bfc32a2aed --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Tests/Fixtures/TraversableInstance.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\ValueExporter\Tests\Fixtures; + +/** + * TraversableInstance. + * + * @author Jules Pietri + */ +class TraversableInstance implements \IteratorAggregate +{ + public $property1 = 'value1'; + public $property2 = 'value2'; + + /** + * {@inheritdoc} + */ + public function getIterator() + { + return new \ArrayIterator($this); + } +} diff --git a/src/Symfony/Component/ValueExporter/Tests/ValueExporterTest.php b/src/Symfony/Component/ValueExporter/Tests/ValueExporterTest.php new file mode 100644 index 0000000000000..ec544c6000112 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/Tests/ValueExporterTest.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter\Tests; + +use Symfony\Component\ValueExporter\Formatter\TraversableToStringFormatter; +use Symfony\Component\ValueExporter\Tests\Fixtures\Entity; +use Symfony\Component\ValueExporter\Tests\Fixtures\EntityImplementingToString; +use Symfony\Component\ValueExporter\Tests\Fixtures\ObjectImplementingToString; +use Symfony\Component\ValueExporter\Tests\Fixtures\PublicEntity; +use Symfony\Component\ValueExporter\Tests\Fixtures\TraversableInstance; +use Symfony\Component\ValueExporter\ValueExporter; + +class ValueExporterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider valueProvider + */ + public function testExportValue($value, $string) + { + $this->assertSame($string, ValueExporter::export($value)); + } + + /** + * @dataProvider valueProvider + */ + public function testToStringFunctionWrapper($value, $string) + { + $this->assertSame($string, to_string($value)); + } + + public function testExportValueExpanded() + { + $value = array( + array(ValueExporter::class, 'export'), + ); + + $exportedValue = << (static) "Symfony\Component\ValueExporter\ValueExporter::export" +) +EOT; + + $this->assertSame($exportedValue, ValueExporter::export($value, 1, true)); + } + + public function testExportTraversable() + { + ValueExporter::addFormatters(array(TraversableToStringFormatter::class)); + + $value = new TraversableInstance(); + $exportedValue = << "value1", + 'property2' => "value2" +) +EOT; + + $this->assertSame($exportedValue, ValueExporter::export($value)); + } + + public function valueProvider() + { + $foo = new \__PHP_Incomplete_Class(); + $array = new \ArrayObject($foo); + $array['__PHP_Incomplete_Class_Name'] = 'AppBundle/Foo'; + + return array( + 'null' => array(null, 'null'), + 'true' => array(true, 'true'), + 'false' => array(false, 'false'), + 'int' => array(4, '(int) 4'), + 'float' => array(4.5, '(float) 4.5'), + 'string' => array('test', '"test"'), + 'empty array' => array(array(), 'array()'), + 'numeric array' => array( + array(0 => null, 1 => true, 2 => 1, 3 => '2', 4 => new \stdClass()), + 'array(0 => null, 1 => true, 2 => (int) 1, 3 => "2", 4 => object(stdClass))', + ), + 'mixed keys array' => array( + array(0 => 0, '1' => 'un', 'key' => 4.5), + 'array(0 => (int) 0, 1 => "un", \'key\' => (float) 4.5)', + ), + 'object implementing to string' => array( + new ObjectImplementingToString('test'), + 'object(Symfony\Component\ValueExporter\Tests\Fixtures\ObjectImplementingToString) "test"', + ), + 'closure' => array(function() {}, 'object(Closure)'), + 'callable string' => array('strlen', '(function) "strlen"'), + 'callable array' => array( + array($this, 'testExportValue'), + '(callable) "Symfony\Component\ValueExporter\Tests\ValueExporterTest::testExportValue"', + ), + 'invokable object' => array($this, '(invokable) "Symfony\Component\ValueExporter\Tests\ValueExporterTest"'), + 'invokable object as array' => array(array($this, '__invoke'), '(invokable) "Symfony\Component\ValueExporter\Tests\ValueExporterTest"'), + 'datetime' => array( + new \DateTime('2014-06-10 07:35:40', new \DateTimeZone('UTC')), + 'object(DateTime) - 2014-06-10T07:35:40+0000', + ), + 'datetime immutable' => array( + new \DateTimeImmutable('2014-06-10 07:35:40', new \DateTimeZone('UTC')), + 'object(DateTimeImmutable) - 2014-06-10T07:35:40+0000', + ), + 'php incomplete class' => array($foo, '__PHP_Incomplete_Class(AppBundle/Foo)'), + 'entity' => array(new Entity(23), 'entity:23(Symfony\Component\ValueExporter\Tests\Fixtures\Entity)'), + 'public entity' => array(new PublicEntity(23), 'entity:23(Symfony\Component\ValueExporter\Tests\Fixtures\PublicEntity)'), + 'entity implementing to string' => array( + new EntityImplementingToString(23, 'test'), + 'entity:23(Symfony\Component\ValueExporter\Tests\Fixtures\EntityImplementingToString) "test"', + ), + ); + } + + public function __invoke() + { + return 'TEST'; + } +} diff --git a/src/Symfony/Component/ValueExporter/ValueExporter.php b/src/Symfony/Component/ValueExporter/ValueExporter.php new file mode 100644 index 0000000000000..974a4e8a0ebec --- /dev/null +++ b/src/Symfony/Component/ValueExporter/ValueExporter.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ValueExporter; + +use Symfony\Component\ValueExporter\Exporter\ValueExporterInterface; +use Symfony\Component\ValueExporter\Exporter\ValueToStringExporter; +use Symfony\Component\ValueExporter\Formatter\CallableToStringFormatter; +use Symfony\Component\ValueExporter\Formatter\DateTimeToStringFormatter; +use Symfony\Component\ValueExporter\Formatter\EntityToStringFormatter; +use Symfony\Component\ValueExporter\Formatter\FormatterInterface; +use Symfony\Component\ValueExporter\Formatter\PhpIncompleteClassToStringFormatter; + +// Load the global to_string() function +require_once __DIR__.'/Resources/functions/to_string.php'; + +/** + * @author Nicolas Grekas + * @author Jules Pietri + */ +class ValueExporter +{ + private static $handler; + private static $exporter; + private static $formatters = array(); + + public static function export($value, $depth = 1, $expand = false) + { + if (null === self::$handler) { + $exporter = self::$exporter ?: new ValueToStringExporter( + CallableToStringFormatter::class, + DateTimeToStringFormatter::class, + EntityToStringFormatter::class, + PhpIncompleteClassToStringFormatter::class + ); + $exporter->addFormatters(self::$formatters); + // Clear formatters + self::$formatters = array(); + self::$handler = function ($value, $depth = 1, $expand = false) use ($exporter) { + return $exporter->exportValue($value, $depth, $expand); + }; + } + + return call_user_func(self::$handler, $value, $depth, $expand); + } + + public static function setHandler(callable $callable = null) + { + $prevHandler = self::$handler; + self::$handler = $callable; + + return $prevHandler; + } + + /** + * Sets a new {@link ValueExporterInterface} instance as exporter. + * + * @param ValueExporterInterface $exporter The exporter instance + */ + public static function setExporter(ValueExporterInterface $exporter) + { + self::$handler = null; + self::$exporter = $exporter; + self::$formatters = array(); + } + + /** + * Adds {@link FormatterInterface} to the {@link ValueExporterInterface}. + * + * You can simple pass an instance or an array with the instance and the priority: + * + * + * ValueExporter::addFormatters(array( + * new AcmeFormatter, + * array(new AcmeOtherFormatter(), 10) + * ); + * + * + * @param mixed $formatters An array of FormatterInterface instances and/or + * arrays holding an instance and its priority + */ + public static function addFormatters($formatters) + { + self::$handler = null; + foreach ($formatters as $formatter) { + self::$formatters[] = $formatter; + } + } +} diff --git a/src/Symfony/Component/ValueExporter/composer.json b/src/Symfony/Component/ValueExporter/composer.json new file mode 100644 index 0000000000000..48a6b310ecf79 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/composer.json @@ -0,0 +1,49 @@ +{ + "name": "symfony/value-exporter", + "type": "library", + "description": "Symfony mechanism for formatting PHP variables", + "keywords": ["export", "php values", "logs"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Jules Pietri", + "email": "jules@heahprod.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.5.9" + }, + "suggest": { + "symfony/var-dumper": "To dump PHP values" + }, + "autoload": { + "files": [ "Resources/functions/to_string.php" ], + "psr-4": { "Symfony\\Component\\ValueExporter\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + } +} diff --git a/src/Symfony/Component/ValueExporter/phpunit.xml.dist b/src/Symfony/Component/ValueExporter/phpunit.xml.dist new file mode 100644 index 0000000000000..4e1bb0a660cd4 --- /dev/null +++ b/src/Symfony/Component/ValueExporter/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + +