From d46ae15afcda7429ddf05afdc3f713bcd101b509 Mon Sep 17 00:00:00 2001 From: Jelte Steijaert Date: Thu, 12 Nov 2015 09:45:22 +0100 Subject: [PATCH 1/2] Extract the profiler to a new component --- composer.json | 1 + .../DataCollector/DoctrineDataCollector.php | 2 + .../Profiler/DoctrineDataCollector.php | 130 ++++ .../Doctrine/Profiler/DoctrineProfileData.php | 233 +++++++ .../Profiler/DoctrineDataCollectorTest.php | 150 ++++ src/Symfony/Bridge/Doctrine/composer.json | 2 + .../Bridge/Monolog/Profiler/LoggerData.php | 162 +++++ .../Monolog/Profiler/LoggerDataCollector.php | 43 ++ .../Profiler/LoggerDataCollectorTest.php | 90 +++ src/Symfony/Bridge/Monolog/composer.json | 6 +- .../Twig/DataCollector/TwigDataCollector.php | 2 + .../Bridge/Twig/Extension/DumpExtension.php | 8 +- .../Twig/Profiler/TwigDataCollector.php | 37 + .../Bridge/Twig/Profiler/TwigProfileData.php | 132 ++++ .../Compiler/DumpDataCollectorPass.php | 8 +- .../DependencyInjection/DebugExtension.php | 10 +- .../DebugBundle/Resources/config/services.xml | 14 +- .../Compiler/DumpDataCollectorPassTest.php | 34 +- src/Symfony/Bundle/FrameworkBundle/Client.php | 7 +- .../DataCollector/AjaxDataCollector.php | 15 +- .../DataCollector/RouterDataCollector.php | 21 +- .../FrameworkExtension.php | 2 +- .../DataCollector/RouterDataCollector.php | 37 + .../Resources/config/collectors.xml | 28 +- .../Resources/config/profiling.xml | 12 +- .../Bundle/FrameworkBundle/composer.json | 3 + .../DataCollector/SecurityDataCollector.php | 2 + .../Tests/Functional/SwitchUserTest.php | 6 +- .../TwigBundle/Resources/config/twig.xml | 2 +- .../Controller/ExceptionController.php | 15 +- .../Controller/ProfilerController.php | 113 ++- .../Controller/RouterController.php | 16 +- .../Profiler/TemplateManager.php | 15 +- .../Resources/config/commands.xml | 4 +- .../Resources/config/profiler.xml | 3 + .../Resources/views/Collector/form.html.twig | 30 +- .../views/Collector/request.html.twig | 6 +- .../Resources/views/Collector/time.html.twig | 10 +- .../Resources/views/Profiler/search.html.twig | 10 +- .../views/Profiler/toolbar.html.twig | 2 +- .../Controller/ProfilerControllerTest.php | 38 +- .../WebProfilerExtensionTest.php | 7 +- .../Tests/Fixtures/profile.data | 2 +- .../Tests/Profiler/TemplateManagerTest.php | 52 +- .../Bundle/WebProfilerBundle/composer.json | 3 +- .../Debug/Profiler/ExceptionData.php | 114 ++++ .../Debug/Profiler/ExceptionDataCollector.php | 73 ++ .../Profiler/ExceptionDataCollectorTest.php | 57 ++ src/Symfony/Component/Debug/composer.json | 6 +- .../EventDispatcher/Debug/WrappedListener.php | 9 +- .../EventDispatcher/Profiler/EventData.php | 81 +++ .../Profiler/EventDataCollector.php | 42 ++ .../Tests/Profiler/EventDataCollectorTest.php | 50 ++ .../Component/EventDispatcher/composer.json | 4 +- .../DataCollector/DataCollectorExtension.php | 2 + .../EventListener/DataCollectorListener.php | 2 + .../FormDataCollectorInterface.php | 2 + .../DataCollector/FormDataExtractor.php | 2 + .../FormDataExtractorInterface.php | 2 + .../Proxy/ResolvedTypeDataCollectorProxy.php | 2 + .../ResolvedTypeFactoryDataCollectorProxy.php | 2 + .../Type/DataCollectorTypeExtension.php | 2 + .../Profiler/DataCollectorExtension.php | 45 ++ .../Form/Extension/Profiler/FormData.php | 57 ++ .../Extension/Profiler/FormDataCollector.php | 249 +++++++ .../Profiler/FormDataCollectorInterface.php | 64 ++ .../Extension/Profiler/FormDataExtractor.php | 206 ++++++ .../Profiler/FormDataExtractorInterface.php | 60 ++ .../Proxy/ResolvedTypeDataCollectorProxy.php | 148 ++++ .../ResolvedTypeFactoryDataCollectorProxy.php | 54 ++ .../Type/DataCollectorTypeExtension.php | 52 ++ .../Profiler/DataCollectorExtensionTest.php | 45 ++ .../Profiler/FormDataCollectorTest.php | 642 ++++++++++++++++++ .../Profiler/FormDataExtractorTest.php | 432 ++++++++++++ .../Type/DataCollectorTypeExtensionTest.php | 48 ++ src/Symfony/Component/Form/composer.json | 6 +- .../DataCollector/AjaxDataCollector.php | 33 - .../DataCollector/ConfigDataCollector.php | 3 + .../DataCollector/DataCollector.php | 2 + .../DataCollector/DataCollectorInterface.php | 5 +- .../DataCollector/DumpDataCollector.php | 2 + .../DataCollector/EventDataCollector.php | 4 +- .../DataCollector/ExceptionDataCollector.php | 2 + .../LateDataCollectorInterface.php | 6 +- .../DataCollector/LoggerDataCollector.php | 5 + .../DataCollector/MemoryDataCollector.php | 5 + .../DataCollector/RequestDataCollector.php | 5 + .../DataCollector/RouterDataCollector.php | 5 + .../DataCollector/TimeDataCollector.php | 5 + .../DataCollector/Util/ValueExporter.php | 6 +- .../EventListener/ProfilerListener.php | 84 ++- .../HttpKernel/Profiler/AjaxData.php | 42 ++ .../HttpKernel/Profiler/AjaxDataCollector.php | 27 + .../Profiler/FileProfilerStorage.php | 5 + .../HttpKernel/Profiler/KernelData.php | 150 ++++ .../Profiler/KernelDataCollector.php | 46 ++ .../Profiler/PdoProfilerStorage.php | 3 +- .../Component/HttpKernel/Profiler/Profile.php | 215 +++--- .../HttpKernel/Profiler/Profiler.php | 240 ++----- .../Profiler/ProfilerStorageInterface.php | 35 +- .../HttpKernel/Profiler/RequestData.php | 323 +++++++++ .../Profiler/RequestDataCollector.php | 99 +++ .../HttpKernel/Profiler/RouterData.php | 97 +++ .../Profiler/RouterDataCollector.php | 111 +++ .../HttpKernel/Profiler/TimeDataCollector.php | 59 ++ .../DataCollector/ConfigDataCollectorTest.php | 3 + .../DataCollector/DumpDataCollectorTest.php | 1 + .../ExceptionDataCollectorTest.php | 3 + .../DataCollector/LoggerDataCollectorTest.php | 3 + .../DataCollector/MemoryDataCollectorTest.php | 3 + .../RequestDataCollectorTest.php | 3 + .../DataCollector/TimeDataCollectorTest.php | 1 + .../DataCollector/Util/ValueExporterTest.php | 3 + .../EventListener/ProfilerListenerTest.php | 190 +++++- .../Profiler/FileProfilerStorageTest.php | 3 + .../Profiler/KernelDataCollectorTest.php | 96 +++ .../Tests/Profiler/ProfilerTest.php | 8 +- .../Profiler/RequestDataCollectorTest.php | 266 ++++++++ .../Tests/Profiler/RequestDataTest.php | 143 ++++ .../Profiler/RouterDataCollectorTest.php | 118 ++++ .../Tests/Profiler/TimeDataCollectorTest.php | 106 +++ .../Component/HttpKernel/composer.json | 1 + .../DataCollector/ConfigDataCollector.php | 67 ++ .../DataCollector/DataCollectorInterface.php | 30 + .../LateDataCollectorInterface.php | 21 + .../DataCollector/MemoryDataCollector.php | 41 ++ .../DataCollector/TimeDataCollector.php | 53 ++ src/Symfony/Component/Profiler/Profile.php | 244 +++++++ .../ProfileData/AbstractProfileData.php | 74 ++ .../Profiler/ProfileData/ConfigData.php | 213 ++++++ .../ProfileData/GenericProfileData.php | 65 ++ .../Profiler/ProfileData/MemoryData.php | 104 +++ .../ProfileData/ProfileDataInterface.php | 22 + .../Profiler/ProfileData/TimeData.php | 131 ++++ .../TokenAwareProfileDataInterface.php | 29 + .../ProfileData/Util/ValueExporter.php | 78 +++ src/Symfony/Component/Profiler/Profiler.php | 153 +++++ src/Symfony/Component/Profiler/README.md | 50 ++ .../Storage/AbstractProfilerStorage.php | 99 +++ .../Profiler/Storage/FileProfilerStorage.php | 235 +++++++ .../Storage/ProfilerStorageInterface.php | 63 ++ .../DataCollector/ConfigDataCollectorTest.php | 74 ++ .../DataCollector/MemoryDataCollectorTest.php | 63 ++ .../DataCollector/TimeDataCollectorTest.php | 62 ++ .../ProfileData/Util/ValueExporterTest.php | 67 ++ .../Component/Profiler/Tests/ProfileTest.php | 53 ++ .../Component/Profiler/Tests/ProfilerTest.php | 131 ++++ .../Storage/AbstractProfilerStorageTest.php | 261 +++++++ .../Tests/Storage/FileProfilerStorageTest.php | 81 +++ src/Symfony/Component/Profiler/composer.json | 42 ++ .../Component/Profiler/phpunit.xml.dist | 29 + .../Security/Core/Profiler/SecurityData.php | 197 ++++++ .../Core/Profiler/SecurityDataCollector.php | 53 ++ .../Profiler/SecurityDataCollectorTest.php | 114 ++++ .../Component/Security/Core/composer.json | 1 + src/Symfony/Component/Security/composer.json | 1 + .../TranslationDataCollector.php | 3 + .../Translation/Profiler/TranslationData.php | 142 ++++ .../Profiler/TranslationDataCollector.php | 44 ++ .../Profiler/TranslationDataCollectorTest.php | 121 ++++ .../VarDumper/Dumper/TraceableDumper.php | 258 +++++++ .../Component/VarDumper/Profiler/DumpData.php | 110 +++ .../VarDumper/Profiler/DumpDataCollector.php | 102 +++ .../Tests/Dumper/TraceableDumperTest.php | 204 ++++++ .../Tests/Profiler/DumpDataCollectorTest.php | 143 ++++ .../Tests/Profiler/Mock/TwigTemplate.php | 77 +++ src/Symfony/Component/VarDumper/composer.json | 5 + 167 files changed, 10216 insertions(+), 654 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Profiler/DoctrineDataCollector.php create mode 100644 src/Symfony/Bridge/Doctrine/Profiler/DoctrineProfileData.php create mode 100644 src/Symfony/Bridge/Doctrine/Tests/Profiler/DoctrineDataCollectorTest.php create mode 100644 src/Symfony/Bridge/Monolog/Profiler/LoggerData.php create mode 100644 src/Symfony/Bridge/Monolog/Profiler/LoggerDataCollector.php create mode 100644 src/Symfony/Bridge/Monolog/Tests/Profiler/LoggerDataCollectorTest.php create mode 100644 src/Symfony/Bridge/Twig/Profiler/TwigDataCollector.php create mode 100644 src/Symfony/Bridge/Twig/Profiler/TwigProfileData.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Profiler/DataCollector/RouterDataCollector.php create mode 100644 src/Symfony/Component/Debug/Profiler/ExceptionData.php create mode 100644 src/Symfony/Component/Debug/Profiler/ExceptionDataCollector.php create mode 100644 src/Symfony/Component/Debug/Tests/Profiler/ExceptionDataCollectorTest.php create mode 100644 src/Symfony/Component/EventDispatcher/Profiler/EventData.php create mode 100644 src/Symfony/Component/EventDispatcher/Profiler/EventDataCollector.php create mode 100644 src/Symfony/Component/EventDispatcher/Tests/Profiler/EventDataCollectorTest.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/DataCollectorExtension.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/FormData.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/FormDataCollector.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/FormDataCollectorInterface.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/FormDataExtractor.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/FormDataExtractorInterface.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeDataCollectorProxy.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeFactoryDataCollectorProxy.php create mode 100644 src/Symfony/Component/Form/Extension/Profiler/Type/DataCollectorTypeExtension.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Profiler/DataCollectorExtensionTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataCollectorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataExtractorTest.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Profiler/Type/DataCollectorTypeExtensionTest.php delete mode 100644 src/Symfony/Component/HttpKernel/DataCollector/AjaxDataCollector.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/AjaxData.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/AjaxDataCollector.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/KernelData.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/KernelDataCollector.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/RequestData.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/RequestDataCollector.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/RouterData.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/RouterDataCollector.php create mode 100644 src/Symfony/Component/HttpKernel/Profiler/TimeDataCollector.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Profiler/KernelDataCollectorTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataCollectorTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Profiler/RouterDataCollectorTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Profiler/TimeDataCollectorTest.php create mode 100644 src/Symfony/Component/Profiler/DataCollector/ConfigDataCollector.php create mode 100644 src/Symfony/Component/Profiler/DataCollector/DataCollectorInterface.php create mode 100644 src/Symfony/Component/Profiler/DataCollector/LateDataCollectorInterface.php create mode 100644 src/Symfony/Component/Profiler/DataCollector/MemoryDataCollector.php create mode 100644 src/Symfony/Component/Profiler/DataCollector/TimeDataCollector.php create mode 100644 src/Symfony/Component/Profiler/Profile.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/AbstractProfileData.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/ConfigData.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/GenericProfileData.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/MemoryData.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/ProfileDataInterface.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/TimeData.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/TokenAwareProfileDataInterface.php create mode 100644 src/Symfony/Component/Profiler/ProfileData/Util/ValueExporter.php create mode 100644 src/Symfony/Component/Profiler/Profiler.php create mode 100644 src/Symfony/Component/Profiler/README.md create mode 100644 src/Symfony/Component/Profiler/Storage/AbstractProfilerStorage.php create mode 100644 src/Symfony/Component/Profiler/Storage/FileProfilerStorage.php create mode 100644 src/Symfony/Component/Profiler/Storage/ProfilerStorageInterface.php create mode 100644 src/Symfony/Component/Profiler/Tests/DataCollector/ConfigDataCollectorTest.php create mode 100644 src/Symfony/Component/Profiler/Tests/DataCollector/MemoryDataCollectorTest.php create mode 100644 src/Symfony/Component/Profiler/Tests/DataCollector/TimeDataCollectorTest.php create mode 100644 src/Symfony/Component/Profiler/Tests/ProfileData/Util/ValueExporterTest.php create mode 100644 src/Symfony/Component/Profiler/Tests/ProfileTest.php create mode 100644 src/Symfony/Component/Profiler/Tests/ProfilerTest.php create mode 100644 src/Symfony/Component/Profiler/Tests/Storage/AbstractProfilerStorageTest.php create mode 100644 src/Symfony/Component/Profiler/Tests/Storage/FileProfilerStorageTest.php create mode 100644 src/Symfony/Component/Profiler/composer.json create mode 100644 src/Symfony/Component/Profiler/phpunit.xml.dist create mode 100644 src/Symfony/Component/Security/Core/Profiler/SecurityData.php create mode 100644 src/Symfony/Component/Security/Core/Profiler/SecurityDataCollector.php create mode 100644 src/Symfony/Component/Security/Core/Tests/Profiler/SecurityDataCollectorTest.php create mode 100644 src/Symfony/Component/Translation/Profiler/TranslationData.php create mode 100644 src/Symfony/Component/Translation/Profiler/TranslationDataCollector.php create mode 100644 src/Symfony/Component/Translation/Tests/Profiler/TranslationDataCollectorTest.php create mode 100644 src/Symfony/Component/VarDumper/Dumper/TraceableDumper.php create mode 100644 src/Symfony/Component/VarDumper/Profiler/DumpData.php create mode 100644 src/Symfony/Component/VarDumper/Profiler/DumpDataCollector.php create mode 100644 src/Symfony/Component/VarDumper/Tests/Dumper/TraceableDumperTest.php create mode 100644 src/Symfony/Component/VarDumper/Tests/Profiler/DumpDataCollectorTest.php create mode 100644 src/Symfony/Component/VarDumper/Tests/Profiler/Mock/TwigTemplate.php diff --git a/composer.json b/composer.json index 5fcec2e747e42..985eac6f78e5e 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "symfony/monolog-bridge": "self.version", "symfony/options-resolver": "self.version", "symfony/process": "self.version", + "symfony/profiler": "self.version", "symfony/property-access": "self.version", "symfony/property-info": "self.version", "symfony/proxy-manager-bridge": "self.version", diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index a57b9ae6ea151..424af65cfcf91 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -22,6 +22,8 @@ * DoctrineDataCollector. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Bridge\Doctrine\Profiler\DoctrineDataCollector instead. */ class DoctrineDataCollector extends DataCollector { diff --git a/src/Symfony/Bridge/Doctrine/Profiler/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/Profiler/DoctrineDataCollector.php new file mode 100644 index 0000000000000..85e6052e032eb --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Profiler/DoctrineDataCollector.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Profiler; + +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\DBAL\Logging\DebugStack; +use Doctrine\DBAL\Types\Type; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; + +/** + * DoctrineDataCollector. + * + * @author Fabien Potencier + */ +class DoctrineDataCollector implements DataCollectorInterface +{ + private $registry; + private $loggers = array(); + + public function __construct(ManagerRegistry $registry) + { + $this->registry = $registry; + } + + /** + * Adds the stack logger for a connection. + * + * @param string $name + * @param DebugStack $logger + */ + public function addLogger($name, DebugStack $logger) + { + $this->loggers[$name] = $logger; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + $queries = array(); + foreach ($this->loggers as $name => $logger) { + $queries[$name] = $this->sanitizeQueries($name, $logger->queries); + } + + return new DoctrineProfileData($queries, $this->registry); + } + + private function sanitizeQueries($connectionName, array $queries) + { + foreach ($queries as $i => $query) { + $queries[$i] = $this->sanitizeQuery($connectionName, $query); + } + + return $queries; + } + + private function sanitizeQuery($connectionName, $query) + { + $query['explainable'] = true; + if (!is_array($query['params'])) { + $query['params'] = array($query['params']); + } + foreach ($query['params'] as $j => $param) { + if (isset($query['types'][$j])) { + // Transform the param according to the type + $type = $query['types'][$j]; + if (is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $query['types'][$j] = $type->getBindingType(); + $param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform()); + } + } + + list($query['params'][$j], $explainable) = $this->sanitizeParam($param); + if (!$explainable) { + $query['explainable'] = false; + } + } + + return $query; + } + + /** + * Sanitizes a param. + * + * The return value is an array with the sanitized value and a boolean + * indicating if the original value was kept (allowing to use the sanitized + * value to explain the query). + * + * @param mixed $var + * + * @return array + */ + private function sanitizeParam($var) + { + if (is_object($var)) { + return array(sprintf('Object(%s)', get_class($var)), false); + } + + if (is_array($var)) { + $a = array(); + $original = true; + foreach ($var as $k => $v) { + list($value, $orig) = $this->sanitizeParam($v); + $original = $original && $orig; + $a[$k] = $value; + } + + return array($a, $original); + } + + if (is_resource($var)) { + return array(sprintf('Resource(%s)', get_resource_type($var)), false); + } + + return array($var, true); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Profiler/DoctrineProfileData.php b/src/Symfony/Bridge/Doctrine/Profiler/DoctrineProfileData.php new file mode 100644 index 0000000000000..34b1848e5300c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Profiler/DoctrineProfileData.php @@ -0,0 +1,233 @@ + 0, 'hits' => 0, 'misses' => 0); + private $cacheRegions = array('puts' => array(), 'hits' => array(), 'misses' => array()); + private $errors = array(); + private $entities = array(); + + public function __construct(array $queries, ManagerRegistry $registry) + { + $this->queries = $queries; + $this->connections = $registry->getConnectionNames(); + $this->managers = $registry->getManagerNames(); + + /* + * @var string + * @var \Doctrine\ORM\EntityManager + */ + foreach ($registry->getManagers() as $name => $em) { + $this->entities[$name] = array(); + /** @var $factory \Doctrine\ORM\Mapping\ClassMetadataFactory */ + $factory = $em->getMetadataFactory(); + $validator = new SchemaValidator($em); + + /** @var $class \Doctrine\ORM\Mapping\ClassMetadataInfo */ + foreach ($factory->getLoadedMetadata() as $class) { + if (!isset($entities[$name][$class->getName()])) { + $classErrors = $validator->validateClass($class); + $this->entities[$name][$class->getName()] = $class->getName(); + + if (!empty($classErrors)) { + $this->errors[$name][$class->getName()] = $classErrors; + } + } + } + + if (version_compare(Version::VERSION, '2.5.0-DEV') < 0) { + continue; + } + + /** @var $emConfig \Doctrine\ORM\Configuration */ + $emConfig = $em->getConfiguration(); + $slcEnabled = $emConfig->isSecondLevelCacheEnabled(); + + if (!$slcEnabled) { + continue; + } + + $this->cacheEnabled = true; + + /** @var $cacheConfiguration \Doctrine\ORM\Cache\CacheConfiguration */ + $cacheConfiguration = $emConfig->getSecondLevelCacheConfiguration(); + /** @var $cacheLoggerChain \Doctrine\ORM\Cache\Logging\CacheLoggerChain */ + $cacheLoggerChain = $cacheConfiguration->getCacheLogger(); + + if (!$cacheLoggerChain || !$cacheLoggerChain->getLogger('statistics')) { + continue; + } + + /** @var $cacheLoggerStats \Doctrine\ORM\Cache\Logging\StatisticsCacheLogger */ + $cacheLoggerStats = $cacheLoggerChain->getLogger('statistics'); + $this->cacheLogEnabled = true; + + $this->cacheCounts['puts'] += $cacheLoggerStats->getPutCount(); + $this->cacheCounts['hits'] += $cacheLoggerStats->getHitCount(); + $this->cacheCounts['misses'] += $cacheLoggerStats->getMissCount(); + + foreach ($cacheLoggerStats->getRegionsPut() as $key => $value) { + if (!isset($this->cacheRegions['hits'][$key])) { + $this->cacheRegions['hits'][$key] = 0; + } + + $this->cacheRegions['puts'][$key] += $value; + } + + foreach ($cacheLoggerStats->getRegionsHit() as $key => $value) { + if (!isset($this->cacheRegions['hits'][$key])) { + $this->cacheRegions['hits'][$key] = 0; + } + + $this->cacheRegions['hits'][$key] += $value; + } + + foreach ($cacheLoggerStats->getRegionsMiss() as $key => $value) { + if (!isset($this->cacheRegions['misses'][$key])) { + $this->cacheRegions['misses'][$key] = 0; + } + + $this->cacheRegions['misses'][$key] += $value; + } + } + } + + public function getManagers() + { + return $this->managers; + } + + public function getConnections() + { + return $this->connections; + } + + public function getQueryCount() + { + return array_sum(array_map('count', $this->queries)); + } + + public function getQueries() + { + return $this->queries; + } + + public function getTime() + { + $time = 0; + foreach ($this->queries as $queries) { + foreach ($queries as $query) { + $time += $query['executionMS']; + } + } + + return $time; + } + + public function getEntities() + { + return $this->entities; + } + + public function getMappingErrors() + { + return $this->errors; + } + + public function getCacheHitsCount() + { + return $this->cacheCounts['hits']; + } + + public function getCachePutsCount() + { + return $this->cacheCounts['puts']; + } + + public function getCacheMissesCount() + { + return $this->cacheCounts['misses']; + } + + public function getCacheEnabled() + { + return $this->cacheEnabled; + } + + public function getCacheRegions() + { + return $this->cacheRegions; + } + + public function getCacheCounts() + { + return $this->cacheCounts; + } + + public function getInvalidEntityCount() + { + if (null === $this->invalidEntityCount) { + $this->invalidEntityCount = array_sum(array_map('count', $this->errors)); + } + + return $this->invalidEntityCount; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array( + 'queries' => $this->queries, + 'connections' => $this->connections, + 'managers' => $this->managers, + 'invalidEntityCount' => $this->invalidEntityCount, + 'cacheEnabled' => $this->cacheEnabled, + 'cacheLogEnabled' => $this->cacheLogEnabled, + 'cacheCounts' => $this->cacheCounts, + 'cacheRegions' => $this->cacheRegions, + 'errors' => $this->errors, + 'entities' => $this->entities, + )); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $unserialized = unserialize($serialized); + $this->queries = $unserialized['queries']; + $this->connections = $unserialized['connections']; + $this->managers = $unserialized['managers']; + $this->invalidEntityCount = $unserialized['invalidEntityCount']; + $this->cacheEnabled = $unserialized['cacheEnabled']; + $this->cacheLogEnabled = $unserialized['cacheLogEnabled']; + $this->cacheCounts = $unserialized['cacheCounts']; + $this->cacheRegions = $unserialized['cacheRegions']; + $this->errors = $unserialized['errors']; + $this->entities = $unserialized['entities']; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'db'; + } +} \ No newline at end of file diff --git a/src/Symfony/Bridge/Doctrine/Tests/Profiler/DoctrineDataCollectorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Profiler/DoctrineDataCollectorTest.php new file mode 100644 index 0000000000000..1330904d3ee88 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Profiler/DoctrineDataCollectorTest.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Profiler; + +use Doctrine\DBAL\Platforms\MySqlPlatform; +use Symfony\Bridge\Doctrine\Profiler\DoctrineDataCollector; + +class DoctrineDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollectConnections() + { + $c = $this->createCollector(array()); + $data = $c->getCollectedData(); + $this->assertEquals(array('default' => 'doctrine.dbal.default_connection'), $data->getConnections()); + } + + public function testCollectManagers() + { + $c = $this->createCollector(array()); + $data = $c->getCollectedData(); + $this->assertEquals(array('default' => 'doctrine.orm.default_entity_manager'), $data->getManagers()); + } + + public function testCollectQueryCount() + { + $c = $this->createCollector(array()); + $data = $c->getCollectedData(); + $this->assertEquals(0, $data->getQueryCount()); + + $queries = array( + array('sql' => 'SELECT * FROM table1', 'params' => array(), 'types' => array(), 'executionMS' => 0), + ); + $c = $this->createCollector($queries); + $data = $c->getCollectedData(); + $this->assertEquals(1, $data->getQueryCount()); + } + + public function testCollectTime() + { + $c = $this->createCollector(array()); + $data = $c->getCollectedData(); + $this->assertEquals(0, $data->getTime()); + + $queries = array( + array('sql' => 'SELECT * FROM table1', 'params' => array(), 'types' => array(), 'executionMS' => 1), + ); + $c = $this->createCollector($queries); + $data = $c->getCollectedData(); + $this->assertEquals(1, $data->getTime()); + + $queries = array( + array('sql' => 'SELECT * FROM table1', 'params' => array(), 'types' => array(), 'executionMS' => 1), + array('sql' => 'SELECT * FROM table2', 'params' => array(), 'types' => array(), 'executionMS' => 2), + ); + $c = $this->createCollector($queries); + $data = $c->getCollectedData(); + $this->assertEquals(3, $data->getTime()); + } + + /** + * @dataProvider paramProvider + */ + public function testCollectQueries($param, $types, $expected, $explainable) + { + $queries = array( + array('sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => array($param), 'types' => $types, 'executionMS' => 1), + ); + $c = $this->createCollector($queries); + $data = $c->getCollectedData(); + + $collected_queries = $data->getQueries(); + $this->assertEquals($expected, $collected_queries['default'][0]['params'][0]); + $this->assertEquals($explainable, $collected_queries['default'][0]['explainable']); + } + + /** + * @dataProvider paramProvider + */ + public function testSerialization($param, $types, $expected, $explainable) + { + $queries = array( + array('sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => array($param), 'types' => $types, 'executionMS' => 1), + ); + $c = $this->createCollector($queries); + $data = $c->getCollectedData(); + $data = unserialize(serialize($data)); + + $collected_queries = $data->getQueries(); + $this->assertEquals($expected, $collected_queries['default'][0]['params'][0]); + $this->assertEquals($explainable, $collected_queries['default'][0]['explainable']); + } + + public function paramProvider() + { + return array( + array('some value', array(), 'some value', true), + array(1, array(), 1, true), + array(true, array(), true, true), + array(null, array(), null, true), + array(new \DateTime('2011-09-11'), array('date'), '2011-09-11', true), + array(fopen(__FILE__, 'r'), array(), 'Resource(stream)', false), + array(new \SplFileInfo(__FILE__), array(), 'Object(SplFileInfo)', false), + ); + } + + private function createCollector($queries) + { + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->expects($this->any()) + ->method('getDatabasePlatform') + ->will($this->returnValue(new MySqlPlatform())); + + $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); + $registry + ->expects($this->any()) + ->method('getManagers') + ->will($this->returnValue(array())); + + $registry + ->expects($this->any()) + ->method('getConnectionNames') + ->will($this->returnValue(array('default' => 'doctrine.dbal.default_connection'))); + $registry + ->expects($this->any()) + ->method('getManagerNames') + ->will($this->returnValue(array('default' => 'doctrine.orm.default_entity_manager'))); + $registry->expects($this->any()) + ->method('getConnection') + ->will($this->returnValue($connection)); + + $logger = $this->getMock('Doctrine\DBAL\Logging\DebugStack'); + $logger->queries = $queries; + + $collector = new DoctrineDataCollector($registry); + $collector->addLogger('default', $logger); + + return $collector; + } +} diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index f07527f5cb523..b9125ea7497a7 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -24,6 +24,7 @@ "symfony/dependency-injection": "~2.2|~3.0.0", "symfony/form": "~2.8|~3.0.0", "symfony/http-kernel": "~2.2|~3.0.0", + "symfony/profiler": "~2.8|~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", @@ -36,6 +37,7 @@ }, "suggest": { "symfony/form": "", + "symfony/profiler": "", "symfony/validator": "", "symfony/property-info": "", "doctrine/data-fixtures": "", diff --git a/src/Symfony/Bridge/Monolog/Profiler/LoggerData.php b/src/Symfony/Bridge/Monolog/Profiler/LoggerData.php new file mode 100644 index 0000000000000..77522b2c59c45 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Profiler/LoggerData.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Profiler; + +use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +/** + * Class LoggerData. + * + * @author Jelte Steijaert + */ +class LoggerData implements ProfileDataInterface +{ + private $data = array(); + private $logs; + + public function __construct(DebugLoggerInterface $logger) + { + $this->data = $this->computeErrorsCount($logger); + $this->logs = $this->sanitizeLogs($logger->getLogs()); + } + + /** + * Gets the called events. + * + * @return array An array of called events + * + * @see TraceableEventDispatcherInterface + */ + public function countErrors() + { + return isset($this->data['error_count']) ? $this->data['error_count'] : 0; + } + + /** + * Gets the logs. + * + * @return array An array of logs + */ + public function getLogs() + { + return $this->logs; + } + + public function getPriorities() + { + return isset($this->data['priorities']) ? $this->data['priorities'] : array(); + } + + public function countDeprecations() + { + return isset($this->data['deprecation_count']) ? $this->data['deprecation_count'] : 0; + } + + public function countScreams() + { + return isset($this->data['scream_count']) ? $this->data['scream_count'] : 0; + } + + private function sanitizeLogs($logs) + { + foreach ($logs as $i => $log) { + $context = $this->sanitizeContext($log['context']); + if (isset($context['type'], $context['level']) && !($context['type'] & $context['level'])) { + $context['scream'] = true; + } + $logs[$i]['context'] = $context; + } + + return $logs; + } + + private function sanitizeContext($context) + { + if (is_array($context)) { + foreach ($context as $key => $value) { + $context[$key] = $this->sanitizeContext($value); + } + + return $context; + } + + if (is_resource($context)) { + return sprintf('Resource(%s)', get_resource_type($context)); + } + + if (is_object($context)) { + return sprintf('Object(%s)', get_class($context)); + } + + return $context; + } + + private function computeErrorsCount(DebugLoggerInterface $logger) + { + $count = array( + 'error_count' => $logger->countErrors(), + 'deprecation_count' => 0, + 'scream_count' => 0, + 'priorities' => array(), + ); + + foreach ($logger->getLogs() as $log) { + if (isset($count['priorities'][$log['priority']])) { + ++$count['priorities'][$log['priority']]['count']; + } else { + $count['priorities'][$log['priority']] = array( + 'count' => 1, + 'name' => $log['priorityName'], + ); + } + + if (isset($log['context']['type'], $log['context']['level'])) { + if (E_DEPRECATED === $log['context']['type'] || E_USER_DEPRECATED === $log['context']['type']) { + ++$count['deprecation_count']; + } elseif (!($log['context']['type'] & $log['context']['level'])) { + ++$count['scream_count']; + } + } + } + + ksort($count['priorities']); + + return $count; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'logger'; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array('data' => $this->data, 'logs' => $this->logs)); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $unserialized = unserialize($serialized); + $this->data = $unserialized['data']; + $this->logs = $unserialized['logs']; + } +} diff --git a/src/Symfony/Bridge/Monolog/Profiler/LoggerDataCollector.php b/src/Symfony/Bridge/Monolog/Profiler/LoggerDataCollector.php new file mode 100644 index 0000000000000..7b17aadc340fc --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Profiler/LoggerDataCollector.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\Bridge\Monolog\Profiler; + +use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface; + +/** + * LoggerDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class LoggerDataCollector implements LateDataCollectorInterface +{ + private $logger; + + public function __construct($logger = null) + { + if (null !== $logger && $logger instanceof DebugLoggerInterface) { + $this->logger = $logger; + } + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + if (null !== $this->logger) { + return new LoggerData($this->logger); + } + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Profiler/LoggerDataCollectorTest.php b/src/Symfony/Bridge/Monolog/Tests/Profiler/LoggerDataCollectorTest.php new file mode 100644 index 0000000000000..17863a8a42171 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Profiler/LoggerDataCollectorTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Profiler; + +use Symfony\Bridge\Monolog\Profiler\LoggerDataCollector; + +class LoggerDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getCollectTestData + */ + public function testCollect($nb, $logs, $expectedLogs, $expectedDeprecationCount, $expectedScreamCount, $expectedPriorities = null) + { + $logger = $this->getMock('Symfony\Component\HttpKernel\Log\DebugLoggerInterface'); + $logger->expects($this->once())->method('countErrors')->will($this->returnValue($nb)); + $logger->expects($this->exactly(2))->method('getLogs')->will($this->returnValue($logs)); + + $c = new LoggerDataCollector($logger); + $data = $c->getCollectedData(); + + $this->assertSame($nb, $data->countErrors()); + $this->assertSame($expectedLogs ?: $logs, $data->getLogs()); + $this->assertSame($expectedDeprecationCount, $data->countDeprecations()); + $this->assertSame($expectedScreamCount, $data->countScreams()); + + if (isset($expectedPriorities)) { + $this->assertSame($expectedPriorities, $data->getPriorities()); + } + } + + public function testCollectWithoutLogger() + { + $c = new LoggerDataCollector(); + $this->assertNull($c->getCollectedData()); + } + + public function getCollectTestData() + { + return array( + array( + 1, + array(array('message' => 'foo', 'context' => array(), 'priority' => 100, 'priorityName' => 'DEBUG')), + null, + 0, + 0, + ), + array( + 1, + array(array('message' => 'foo', 'context' => array('foo' => fopen(__FILE__, 'r')), 'priority' => 100, 'priorityName' => 'DEBUG')), + array(array('message' => 'foo', 'context' => array('foo' => 'Resource(stream)'), 'priority' => 100, 'priorityName' => 'DEBUG')), + 0, + 0, + ), + array( + 1, + array(array('message' => 'foo', 'context' => array('foo' => new \stdClass()), 'priority' => 100, 'priorityName' => 'DEBUG')), + array(array('message' => 'foo', 'context' => array('foo' => 'Object(stdClass)'), 'priority' => 100, 'priorityName' => 'DEBUG')), + 0, + 0, + ), + array( + 1, + array( + array('message' => 'foo', 'context' => array('type' => E_DEPRECATED, 'level' => E_ALL), 'priority' => 100, 'priorityName' => 'DEBUG'), + array('message' => 'foo2', 'context' => array('type' => E_USER_DEPRECATED, 'level' => E_ALL), 'priority' => 100, 'priorityName' => 'DEBUG'), + ), + null, + 2, + 0, + array(100 => array('count' => 2, 'name' => 'DEBUG')), + ), + array( + 1, + array(array('message' => 'foo3', 'context' => array('type' => E_USER_WARNING, 'level' => 0), 'priority' => 100, 'priorityName' => 'DEBUG')), + array(array('message' => 'foo3', 'context' => array('type' => E_USER_WARNING, 'level' => 0, 'scream' => true), 'priority' => 100, 'priorityName' => 'DEBUG')), + 0, + 1, + ), + ); + } +} diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 22ae4c9dab0eb..189b403cc75ad 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -22,12 +22,14 @@ "require-dev": { "symfony/http-kernel": "~2.4|~3.0.0", "symfony/console": "~2.4|~3.0.0", - "symfony/event-dispatcher": "~2.2|~3.0.0" + "symfony/event-dispatcher": "~2.2|~3.0.0", + "symfony/profiler": "~2.8|~3.0.0" }, "suggest": { "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings. You need version ~2.3 of the console for it.", - "symfony/event-dispatcher": "Needed when using log messages in console commands." + "symfony/event-dispatcher": "Needed when using log messages in console commands.", + "symfony/profiler": "" }, "autoload": { "psr-4": { "Symfony\\Bridge\\Monolog\\": "" }, diff --git a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php index 90b21d0495ff7..2a74ab51c186a 100644 --- a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php +++ b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php @@ -20,6 +20,8 @@ * TwigDataCollector. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Bridge\Twig\Profiler\TwigDataCollector instead. */ class TwigDataCollector extends DataCollector implements LateDataCollectorInterface { diff --git a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php index 30318ecac6d02..b53f389f8cf90 100644 --- a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php @@ -31,8 +31,13 @@ public function __construct(ClonerInterface $cloner) public function getFunctions() { + $function = new \Twig_SimpleFunction('dump', array($this, 'dump'), array('is_safe' => array('html'), 'needs_context' => true, 'needs_environment' => true)); + if ( function_exists('dump') ) { + $function = new \Twig_SimpleFunction('dump', 'dump'); + } + return array( - new \Twig_SimpleFunction('dump', array($this, 'dump'), array('is_safe' => array('html'), 'needs_context' => true, 'needs_environment' => true)), + $function, ); } @@ -75,5 +80,6 @@ public function dump(\Twig_Environment $env, $context) rewind($dump); return stream_get_contents($dump); + } } diff --git a/src/Symfony/Bridge/Twig/Profiler/TwigDataCollector.php b/src/Symfony/Bridge/Twig/Profiler/TwigDataCollector.php new file mode 100644 index 0000000000000..a434ef8151ebb --- /dev/null +++ b/src/Symfony/Bridge/Twig/Profiler/TwigDataCollector.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Profiler; + +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface; + +/** + * TwigDataCollector. + * + * @author Fabien Potencier + */ +class TwigDataCollector implements LateDataCollectorInterface +{ + private $profile; + + public function __construct(\Twig_Profiler_Profile $profile) + { + $this->profile = $profile; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + return new TwigProfileData($this->profile); + } +} diff --git a/src/Symfony/Bridge/Twig/Profiler/TwigProfileData.php b/src/Symfony/Bridge/Twig/Profiler/TwigProfileData.php new file mode 100644 index 0000000000000..1af9b0856a022 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Profiler/TwigProfileData.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Profiler; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +class TwigProfileData implements ProfileDataInterface +{ + private $profile; + private $computed; + + public function __construct(\Twig_Profiler_Profile $profile) + { + $this->profile = $profile; + } + + public function getTime() + { + return $this->profile->getDuration() * 1000; + } + + public function getTemplateCount() + { + return $this->getComputedData('template_count'); + } + + public function getTemplates() + { + return $this->getComputedData('templates'); + } + + public function getBlockCount() + { + return $this->getComputedData('block_count'); + } + + public function getMacroCount() + { + return $this->getComputedData('macro_count'); + } + + public function getHtmlCallGraph() + { + $dumper = new \Twig_Profiler_Dumper_Html(); + + return new \Twig_Markup($dumper->dump($this->profile), 'UTF-8'); + } + + public function getProfile() + { + return $this->profile; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'twig'; + } + + private function getComputedData($index) + { + if (null === $this->computed) { + $this->computed = $this->computeData($this->profile); + } + + return $this->computed[$index]; + } + + private function computeData(\Twig_Profiler_Profile $profile) + { + $data = array( + 'template_count' => 0, + 'block_count' => 0, + 'macro_count' => 0, + ); + + $templates = array(); + foreach ($profile as $p) { + $d = $this->computeData($p); + + $data['template_count'] += ($p->isTemplate() ? 1 : 0) + $d['template_count']; + $data['block_count'] += ($p->isBlock() ? 1 : 0) + $d['block_count']; + $data['macro_count'] += ($p->isMacro() ? 1 : 0) + $d['macro_count']; + + if ($p->isTemplate()) { + if (!isset($templates[$p->getTemplate()])) { + $templates[$p->getTemplate()] = 1; + } else { + ++$templates[$p->getTemplate()]; + } + } + + foreach ($d['templates'] as $template => $count) { + if (!isset($templates[$template])) { + $templates[$template] = $count; + } else { + $templates[$template] += $count; + } + } + } + $data['templates'] = $templates; + + return $data; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize($this->profile); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $this->profile = unserialize($serialized); + } +} \ No newline at end of file diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/DumpDataCollectorPass.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/DumpDataCollectorPass.php index 4dcb3908e9997..dabc879d2cfeb 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/DumpDataCollectorPass.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/DumpDataCollectorPass.php @@ -27,18 +27,18 @@ class DumpDataCollectorPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { - if (!$container->hasDefinition('data_collector.dump')) { + if (!$container->hasDefinition('var_dumper.traceable_dumper')) { return; } - $definition = $container->getDefinition('data_collector.dump'); + $definition = $container->getDefinition('var_dumper.traceable_dumper'); if ($container->hasParameter('templating.helper.code.file_link_format')) { - $definition->replaceArgument(1, $container->getParameter('templating.helper.code.file_link_format')); + $definition->replaceArgument(2, $container->getParameter('templating.helper.code.file_link_format')); } if (!$container->hasParameter('web_profiler.debug_toolbar.mode') || WebDebugToolbarListener::DISABLED === $container->getParameter('web_profiler.debug_toolbar.mode')) { - $definition->replaceArgument(3, null); + $definition->replaceArgument(0, null); } } } diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index a6169889c4a75..a5d594b22c181 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -40,12 +40,10 @@ public function load(array $configs, ContainerBuilder $container) ->addMethodCall('setMaxString', array($config['max_string_length'])); if (null !== $config['dump_destination']) { - $container->getDefinition('var_dumper.cli_dumper') - ->replaceArgument(0, $config['dump_destination']) - ; - $container->getDefinition('data_collector.dump') - ->replaceArgument(4, new Reference('var_dumper.cli_dumper')) - ; + + /*$container->getDefinition('data_collector.dump') + ->replaceArgument(1, new Reference('var_dumper.cli_dumper')) + ;*/ } } diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 001b987e90f8b..8dc60d4d79b99 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -10,19 +10,25 @@ - - + + null null %kernel.charset% + + + + + + - null + - + diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/DumpDataCollectorPassTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/DumpDataCollectorPassTest.php index 6500a85a84302..55127ff388c7f 100644 --- a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/DumpDataCollectorPassTest.php +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/DumpDataCollectorPassTest.php @@ -15,7 +15,7 @@ use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; class DumpDataCollectorPassTest extends \PHPUnit_Framework_TestCase { @@ -25,12 +25,12 @@ public function testProcessWithFileLinkFormatParameter() $container->addCompilerPass(new DumpDataCollectorPass()); $container->setParameter('templating.helper.code.file_link_format', 'file-link-format'); - $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, null)); - $container->setDefinition('data_collector.dump', $definition); + $definition = new Definition('Symfony\Component\VarDumper\Dumper\TraceableDumper', array(null, null, null, null)); + $container->setDefinition('var_dumper.traceable_dumper', $definition); $container->compile(); - $this->assertSame('file-link-format', $definition->getArgument(1)); + $this->assertSame('file-link-format', $definition->getArgument(2)); } public function testProcessWithoutFileLinkFormatParameter() @@ -38,27 +38,27 @@ public function testProcessWithoutFileLinkFormatParameter() $container = new ContainerBuilder(); $container->addCompilerPass(new DumpDataCollectorPass()); - $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, null)); - $container->setDefinition('data_collector.dump', $definition); + $definition = new Definition('Symfony\Component\VarDumper\Dumper\TraceableDumper', array(null, null, null, null)); + $container->setDefinition('var_dumper.traceable_dumper', $definition); $container->compile(); - $this->assertNull($definition->getArgument(1)); + $this->assertNull($definition->getArgument(2)); } public function testProcessWithToolbarEnabled() { $container = new ContainerBuilder(); $container->addCompilerPass(new DumpDataCollectorPass()); - $requestStack = new RequestStack(); + $dumper = new HtmlDumper(); - $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, $requestStack)); - $container->setDefinition('data_collector.dump', $definition); + $definition = new Definition('Symfony\Component\VarDumper\Dumper\TraceableDumper', array($dumper, null, null, null)); + $container->setDefinition('var_dumper.traceable_dumper', $definition); $container->setParameter('web_profiler.debug_toolbar.mode', WebDebugToolbarListener::ENABLED); $container->compile(); - $this->assertSame($requestStack, $definition->getArgument(3)); + $this->assertSame($dumper, $definition->getArgument(0)); } public function testProcessWithToolbarDisabled() @@ -66,13 +66,13 @@ public function testProcessWithToolbarDisabled() $container = new ContainerBuilder(); $container->addCompilerPass(new DumpDataCollectorPass()); - $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, new RequestStack())); - $container->setDefinition('data_collector.dump', $definition); + $definition = new Definition('Symfony\Component\VarDumper\Dumper\TraceableDumper', array(new HtmlDumper(), null, null, null)); + $container->setDefinition('var_dumper.traceable_dumper', $definition); $container->setParameter('web_profiler.debug_toolbar.mode', WebDebugToolbarListener::DISABLED); $container->compile(); - $this->assertNull($definition->getArgument(3)); + $this->assertNull($definition->getArgument(0)); } public function testProcessWithoutToolbar() @@ -80,11 +80,11 @@ public function testProcessWithoutToolbar() $container = new ContainerBuilder(); $container->addCompilerPass(new DumpDataCollectorPass()); - $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, new RequestStack())); - $container->setDefinition('data_collector.dump', $definition); + $definition = new Definition('Symfony\Component\VarDumper\Dumper\TraceableDumper', array(new HtmlDumper(), null, null, null)); + $container->setDefinition('var_dumper.traceable_dumper', $definition); $container->compile(); - $this->assertNull($definition->getArgument(3)); + $this->assertNull($definition->getArgument(0)); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Client.php b/src/Symfony/Bundle/FrameworkBundle/Client.php index 82e627a726a07..a11d9e504d739 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Client.php +++ b/src/Symfony/Bundle/FrameworkBundle/Client.php @@ -66,11 +66,14 @@ public function getKernel() */ public function getProfile() { - if (!$this->kernel->getContainer()->has('profiler')) { + if (!$this->kernel->getContainer()->has('profiler.storage')) { + return false; + } + if (!$token = $this->response->headers->get('X-Debug-Token')) { return false; } - return $this->kernel->getContainer()->get('profiler')->loadProfileFromResponse($this->response); + return $this->kernel->getContainer()->get('profiler.storage')->read($token); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/AjaxDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AjaxDataCollector.php index f44c8f5aaf80e..807e04f38b3b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DataCollector/AjaxDataCollector.php +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AjaxDataCollector.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\DataCollector; -use Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector as BaseAjaxDataCollector; +use Symfony\Bundle\FrameworkBundle\Profiler\DataCollector\AjaxDataCollector as BaseAjaxDataCollector; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; -@trigger_error('The '.__NAMESPACE__.'\AjaxDataCollector class is deprecated since version 2.8 and will be removed in 3.0. Use Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector instead.', E_USER_DEPRECATED); +@trigger_error('The '.__NAMESPACE__.'\AjaxDataCollector class is deprecated since version 2.8 and will be removed in 3.0. Use Symfony\Bundle\FrameworkBundle\Profiler\DataCollector\AjaxDataCollector instead.', E_USER_DEPRECATED); /** * AjaxDataCollector. @@ -24,4 +26,13 @@ */ class AjaxDataCollector extends BaseAjaxDataCollector { + public function collect(Request $request, Response $response, \Exception $exception = null) + { + // all collecting is done client side + } + + public function getName() + { + return 'ajax'; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php index b7289e793dc0a..1d48139e13e23 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php @@ -11,27 +11,18 @@ namespace Symfony\Bundle\FrameworkBundle\DataCollector; -use Symfony\Component\HttpKernel\DataCollector\RouterDataCollector as BaseRouterDataCollector; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; +use Symfony\Bundle\FrameworkBundle\Profiler\DataCollector\RouterDataCollector as BaseRouterDataCollector; + +@trigger_error('The '.__NAMESPACE__.'\RouterDataCollector class is deprecated since version 2.8 and will be removed in 3.0. Use Symfony\Bundle\FrameworkBundle\Profiler\DataCollector\RouterDataCollector instead.', E_USER_DEPRECATED); /** * RouterDataCollector. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Bundle\FrameworkBundle\Profiler\DataCollector\RouterDataCollector} instead. */ class RouterDataCollector extends BaseRouterDataCollector { - public function guessRoute(Request $request, $controller) - { - if (is_array($controller)) { - $controller = $controller[0]; - } - - if ($controller instanceof RedirectController) { - return $request->attributes->get('_route'); - } - - return parent::guessRoute($request, $controller); - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 6d9c5f892134b..5210590eb0187 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -309,7 +309,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $supported = array( 'sqlite' => 'Symfony\Component\HttpKernel\Profiler\SqliteProfilerStorage', 'mysql' => 'Symfony\Component\HttpKernel\Profiler\MysqlProfilerStorage', - 'file' => 'Symfony\Component\HttpKernel\Profiler\FileProfilerStorage', + 'file' => 'Symfony\Component\Profiler\Storage\FileProfilerStorage', 'mongodb' => 'Symfony\Component\HttpKernel\Profiler\MongoDbProfilerStorage', 'memcache' => 'Symfony\Component\HttpKernel\Profiler\MemcacheProfilerStorage', 'memcached' => 'Symfony\Component\HttpKernel\Profiler\MemcachedProfilerStorage', diff --git a/src/Symfony/Bundle/FrameworkBundle/Profiler/DataCollector/RouterDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/Profiler/DataCollector/RouterDataCollector.php new file mode 100644 index 0000000000000..b3d8811961f1d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Profiler/DataCollector/RouterDataCollector.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Profiler\DataCollector; + +use Symfony\Component\HttpKernel\Profiler\RouterDataCollector as BaseRouterDataCollector; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; + +/** + * RouterDataCollector. + * + * @author Fabien Potencier + */ +class RouterDataCollector extends BaseRouterDataCollector +{ + public function guessRoute(Request $request, $controller) + { + if (is_array($controller)) { + $controller = $controller[0]; + } + + if ($controller instanceof RedirectController) { + return $request->attributes->get('_route'); + } + + return parent::guessRoute($request, $controller); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index 429350ffcf8c3..016216ad179df 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -5,32 +5,34 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector - Symfony\Component\HttpKernel\DataCollector\RequestDataCollector + Symfony\Component\HttpKernel\Profiler\KernelDataCollector + Symfony\Component\HttpKernel\Profiler\RequestDataCollector Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector Symfony\Component\HttpKernel\DataCollector\EventDataCollector - Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector - Symfony\Component\HttpKernel\DataCollector\TimeDataCollector - Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector - Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector + Symfony\Bridge\Monolog\Profiler\LoggerDataCollector + Symfony\Component\HttpKernel\Profiler\TimeDataCollector + Symfony\Component\Profiler\DataCollector\MemoryDataCollector + Symfony\Bundle\FrameworkBundle\Profiler\DataCollector\RouterDataCollector - - + + + - + - + + @@ -47,6 +49,7 @@ + @@ -55,9 +58,10 @@ - - + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml index 167deb02fb8fa..87ee8d6717b2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml @@ -4,26 +4,22 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - Symfony\Component\HttpKernel\Profiler\Profiler - Symfony\Component\HttpKernel\EventListener\ProfilerListener - - - + - + + %profiler.storage.dsn% %profiler.storage.username% %profiler.storage.password% %profiler.storage.lifetime% - + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 67b1d40a494eb..4d1c68a48a901 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -26,6 +26,8 @@ "symfony/http-kernel": "~2.8", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "~2.3|~3.0.0", + "symfony/monolog-bridge": "~2.8.0", + "symfony/profiler": "~2.8.0", "symfony/routing": "~2.8|~3.0.0", "symfony/security-core": "~2.6|~3.0.0", "symfony/security-csrf": "~2.6|~3.0.0", @@ -40,6 +42,7 @@ "symfony/console": "~2.8|~3.0.0", "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0", "symfony/dom-crawler": "~2.0,>=2.0.5|~3.0.0", + "symfony/debug": "~2.8|~3.0.0", "symfony/finder": "~2.0,>=2.0.5|~3.0.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/security": "~2.6|~3.0.0", diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 7b3e111974ea5..6edc836fc275a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -23,6 +23,8 @@ * SecurityDataCollector. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\Security\Core\Profiler\SecurityDataCollector instead. */ class SecurityDataCollector extends DataCollector { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php index f4b3d27f21404..8acff1ed86e34 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php @@ -23,7 +23,7 @@ public function testSwitchUser($originalUser, $targetUser, $expectedUser, $expec $client->request('GET', '/profile?_switch_user='.$targetUser); $this->assertEquals($expectedStatus, $client->getResponse()->getStatusCode()); - $this->assertEquals($expectedUser, $client->getProfile()->getCollector('security')->getUser()); + $this->assertEquals($expectedUser, $client->getProfile()->get('security')->getUser()); } public function testSwitchedUserCannotSwitchToOther() @@ -34,7 +34,7 @@ public function testSwitchedUserCannotSwitchToOther() $client->request('GET', '/profile?_switch_user=user_cannot_switch_2'); $this->assertEquals(500, $client->getResponse()->getStatusCode()); - $this->assertEquals('user_cannot_switch_1', $client->getProfile()->getCollector('security')->getUser()); + $this->assertEquals('user_cannot_switch_1', $client->getProfile()->get('security')->getUser()); } public function testSwitchedUserExit() @@ -45,7 +45,7 @@ public function testSwitchedUserExit() $client->request('GET', '/profile?_switch_user=_exit'); $this->assertEquals(200, $client->getResponse()->getStatusCode()); - $this->assertEquals('user_can_switch', $client->getProfile()->getCollector('security')->getUser()); + $this->assertEquals('user_can_switch', $client->getProfile()->get('security')->getUser()); } public function getTestParameters() diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index d98f2b3611c59..3907775aa4e23 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -91,7 +91,7 @@ - + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php index fd0dbecd2bbec..5c4163db17ee9 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php @@ -11,10 +11,11 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; -use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\Profiler\Profiler; use Symfony\Component\Debug\ExceptionHandler; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface; /** * ExceptionController. @@ -27,9 +28,11 @@ class ExceptionController protected $debug; protected $profiler; - public function __construct(Profiler $profiler = null, \Twig_Environment $twig, $debug) + public function __construct(Profiler $profiler = null, ProfilerStorageInterface $profilerStorage = null, + \Twig_Environment $twig, $debug) { $this->profiler = $profiler; + $this->profilerStorage = $profilerStorage; $this->twig = $twig; $this->debug = $debug; } @@ -45,13 +48,13 @@ public function __construct(Profiler $profiler = null, \Twig_Environment $twig, */ public function showAction($token) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } $this->profiler->disable(); - $exception = $this->profiler->loadProfile($token)->getCollector('exception')->getException(); + $exception = $this->profilerStorage->read($token)->get('exception')->getException(); $template = $this->getTemplate(); if (!$this->twig->getLoader()->exists($template)) { @@ -85,13 +88,13 @@ public function showAction($token) */ public function cssAction($token) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } $this->profiler->disable(); - $exception = $this->profiler->loadProfile($token)->getCollector('exception')->getException(); + $exception = $this->profilerStorage->read($token)->get('exception')->getException(); $template = $this->getTemplate(); if (!$this->templateExists($template)) { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index 51a52099b7d73..2c1ccb2f426db 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -17,7 +17,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Profiler\Profiler; +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; /** @@ -30,6 +32,7 @@ class ProfilerController private $templateManager; private $generator; private $profiler; + private $profilerStorage; private $twig; private $templates; private $toolbarPosition; @@ -37,16 +40,19 @@ class ProfilerController /** * Constructor. * - * @param UrlGeneratorInterface $generator The URL Generator - * @param Profiler $profiler The profiler - * @param \Twig_Environment $twig The twig environment - * @param array $templates The templates - * @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration) + * @param UrlGeneratorInterface $generator The URL Generator + * @param Profiler $profiler The profiler + * @param ProfilerStorageInterface $profilerStorage The profiler storage + * @param \Twig_Environment $twig The twig environment + * @param array $templates The templates + * @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration) */ - public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, \Twig_Environment $twig, array $templates, $toolbarPosition = 'normal') + public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, ProfilerStorageInterface $profilerStorage = null, + \Twig_Environment $twig, array $templates, $toolbarPosition = 'normal') { $this->generator = $generator; $this->profiler = $profiler; + $this->profilerStorage = $profilerStorage; $this->twig = $twig; $this->templates = $templates; $this->toolbarPosition = $toolbarPosition; @@ -61,7 +67,7 @@ public function __construct(UrlGeneratorInterface $generator, Profiler $profiler */ public function homeAction() { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } @@ -82,7 +88,7 @@ public function homeAction() */ public function panelAction(Request $request, $token) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } @@ -91,22 +97,24 @@ public function panelAction(Request $request, $token) $panel = $request->query->get('panel', 'request'); $page = $request->query->get('page', 'home'); - if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null))) { + if ('latest' === $token && $latest = current($this->profilerStorage->findBy(array(), 1, null, null))) { $token = $latest['token']; } - if (!$profile = $this->profiler->loadProfile($token)) { - return new Response($this->twig->render('@WebProfiler/Profiler/info.html.twig', array('about' => 'no_token', 'token' => $token, 'request' => $request)), 200, array('Content-Type' => 'text/html')); + if (!$profile = $this->profilerStorage->read($token)) { + return new Response($this->twig->render('@WebProfiler/Profiler/info.html.twig', array('about' => 'no_token', 'token' => $token)), 200, array('Content-Type' => 'text/html')); } - if (!$profile->hasCollector($panel)) { + $panel = $request->query->get('panel', $profile instanceof Profile?'request':'config'); + + if (!$profile->has($panel)) { throw new NotFoundHttpException(sprintf('Panel "%s" is not available for token "%s".', $panel, $token)); } return new Response($this->twig->render($this->getTemplateManager()->getName($profile, $panel), array( 'token' => $token, 'profile' => $profile, - 'collector' => $profile->getCollector($panel), + 'collector' => $profile->get($panel), 'panel' => $panel, 'page' => $page, 'request' => $request, @@ -127,12 +135,12 @@ public function purgeAction() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } $this->profiler->disable(); - $this->profiler->purge(); + $this->profilerStorage->purge(); return new RedirectResponse($this->generator->generate('_profiler_info', array('about' => 'purge')), 302, array('Content-Type' => 'text/html')); } @@ -149,7 +157,7 @@ public function purgeAction() */ public function infoAction(Request $request, $about) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } @@ -173,7 +181,7 @@ public function infoAction(Request $request, $about) */ public function toolbarAction(Request $request, $token) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } @@ -190,7 +198,7 @@ public function toolbarAction(Request $request, $token) $this->profiler->disable(); - if (!$profile = $this->profiler->loadProfile($token)) { + if (!$profile = $this->profilerStorage->read($token)) { return new Response('', 404, array('Content-Type' => 'text/html')); } @@ -228,36 +236,27 @@ public function toolbarAction(Request $request, $token) */ public function searchBarAction(Request $request) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } $this->profiler->disable(); if (null === $session = $request->getSession()) { - $ip = - $method = - $url = + $filters = array(); $start = $end = - $limit = - $token = null; + $limit = null; } else { - $ip = $request->query->get('ip', $session->get('_profiler_search_ip')); - $method = $request->query->get('method', $session->get('_profiler_search_method')); - $url = $request->query->get('url', $session->get('_profiler_search_url')); - $start = $request->query->get('start', $session->get('_profiler_search_start')); - $end = $request->query->get('end', $session->get('_profiler_search_end')); + $filters = $request->query->get('filters', $session->get('_profiler_search_filters')); + $start = $request->query->get('limit', $session->get('_profiler_search_limit')); + $end = $request->query->get('limit', $session->get('_profiler_search_limit')); $limit = $request->query->get('limit', $session->get('_profiler_search_limit')); - $token = $request->query->get('token', $session->get('_profiler_search_token')); } return new Response( $this->twig->render('@WebProfiler/Profiler/search.html.twig', array( - 'token' => $token, - 'ip' => $ip, - 'method' => $method, - 'url' => $url, + 'filters' => $filters, 'start' => $start, 'end' => $end, 'limit' => $limit, @@ -280,17 +279,15 @@ public function searchBarAction(Request $request) */ public function searchResultsAction(Request $request, $token) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } $this->profiler->disable(); - $profile = $this->profiler->loadProfile($token); + $profile = $this->profilerStorage->read($token); - $ip = $request->query->get('ip'); - $method = $request->query->get('method'); - $url = $request->query->get('url'); + $filters = $request->query->get('filters', array()); $start = $request->query->get('start', null); $end = $request->query->get('end', null); $limit = $request->query->get('limit'); @@ -299,15 +296,12 @@ public function searchResultsAction(Request $request, $token) 'request' => $request, 'token' => $token, 'profile' => $profile, - 'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end), - 'ip' => $ip, - 'method' => $method, - 'url' => $url, + 'tokens' => $this->profilerStorage->findBy(array_replace($filters, array('profile_type' => 'http')), $limit, $start, $end), + 'filters' => $filters, 'start' => $start, 'end' => $end, 'limit' => $limit, 'panel' => null, - 'request' => $request, )), 200, array('Content-Type' => 'text/html')); } @@ -322,42 +316,37 @@ public function searchResultsAction(Request $request, $token) */ public function searchAction(Request $request) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } $this->profiler->disable(); - $ip = preg_replace('/[^:\d\.]/', '', $request->query->get('ip')); - $method = $request->query->get('method'); - $url = $request->query->get('url'); + $filters = $request->query->get('filters', array()); + if ( isset($filters['id']) ) { + $filters['ip'] = preg_replace('/[^:\d\.]/', '', $filters['ip']); + } $start = $request->query->get('start', null); $end = $request->query->get('end', null); $limit = $request->query->get('limit'); - $token = $request->query->get('token'); if (null !== $session = $request->getSession()) { - $session->set('_profiler_search_ip', $ip); - $session->set('_profiler_search_method', $method); - $session->set('_profiler_search_url', $url); + $session->set('_profiler_search_filters', $filters); $session->set('_profiler_search_start', $start); $session->set('_profiler_search_end', $end); $session->set('_profiler_search_limit', $limit); - $session->set('_profiler_search_token', $token); } - if (!empty($token)) { - return new RedirectResponse($this->generator->generate('_profiler', array('token' => $token)), 302, array('Content-Type' => 'text/html')); + if (!empty($filters['token'])) { + return new RedirectResponse($this->generator->generate('_profiler', array('token' => $filters['token'])), 302, array('Content-Type' => 'text/html')); } - $tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end); + $tokens = $this->profilerStorage->findBy(array_replace($filters, array('profile_type' => 'http')), $limit, $start, $end); return new RedirectResponse($this->generator->generate('_profiler_search_results', array( 'request' => $request, 'token' => $tokens ? $tokens[0]['token'] : 'empty', - 'ip' => $ip, - 'method' => $method, - 'url' => $url, + 'filters' => $filters, 'start' => $start, 'end' => $end, 'limit' => $limit, @@ -373,7 +362,7 @@ public function searchAction(Request $request) */ public function phpinfoAction() { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } @@ -394,7 +383,7 @@ public function phpinfoAction() protected function getTemplateManager() { if (null === $this->templateManager) { - $this->templateManager = new TemplateManager($this->profiler, $this->twig, $this->templates); + $this->templateManager = new TemplateManager($this->twig, $this->templates); } return $this->templateManager; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php index f4a84bf568730..a296312257e84 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php @@ -12,12 +12,13 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\Matcher\TraceableUrlMatcher; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\Profiler\Profiler; /** * RouterController. @@ -27,13 +28,16 @@ class RouterController { private $profiler; + private $profilerStorage; private $twig; private $matcher; private $routes; - public function __construct(Profiler $profiler = null, \Twig_Environment $twig, UrlMatcherInterface $matcher = null, RouteCollection $routes = null) + public function __construct(Profiler $profiler = null, ProfilerStorageInterface $profilerStorage = null, \Twig_Environment $twig, + UrlMatcherInterface $matcher = null, RouteCollection $routes = null) { $this->profiler = $profiler; + $this->profilerStorage = $profilerStorage; $this->twig = $twig; $this->matcher = $matcher; $this->routes = (null === $routes && $matcher instanceof RouterInterface) ? $matcher->getRouteCollection() : $routes; @@ -50,7 +54,7 @@ public function __construct(Profiler $profiler = null, \Twig_Environment $twig, */ public function panelAction($token) { - if (null === $this->profiler) { + if (null === $this->profiler || null === $this->profilerStorage) { throw new NotFoundHttpException('The profiler must be enabled.'); } @@ -60,17 +64,17 @@ public function panelAction($token) return new Response('The Router is not enabled.', 200, array('Content-Type' => 'text/html')); } - $profile = $this->profiler->loadProfile($token); + $profile = $this->profilerStorage->read($token); $context = $this->matcher->getContext(); $context->setMethod($profile->getMethod()); $matcher = new TraceableUrlMatcher($this->routes, $context); - $request = $profile->getCollector('request'); + $request = $profile->get('request'); return new Response($this->twig->render('@WebProfiler/Router/panel.html.twig', array( 'request' => $request, - 'router' => $profile->getCollector('router'), + 'router' => $profile->get('router'), 'traces' => $matcher->getTraces($request->getPathInfo()), )), 200, array('Content-Type' => 'text/html')); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php index 44a3ba79d744d..3793d095440cc 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php @@ -12,8 +12,8 @@ namespace Symfony\Bundle\WebProfilerBundle\Profiler; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\Profiler\Profiler; -use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Profiler\Profiler; /** * Profiler Templates Manager. @@ -25,18 +25,15 @@ class TemplateManager { protected $twig; protected $templates; - protected $profiler; /** * Constructor. * - * @param Profiler $profiler * @param \Twig_Environment $twig * @param array $templates */ - public function __construct(Profiler $profiler, \Twig_Environment $twig, array $templates) + public function __construct(\Twig_Environment $twig, array $templates) { - $this->profiler = $profiler; $this->twig = $twig; $this->templates = $templates; } @@ -44,8 +41,8 @@ public function __construct(Profiler $profiler, \Twig_Environment $twig, array $ /** * Gets the template name for a given panel. * - * @param Profile $profile - * @param string $panel + * @param Profile $profile + * @param string $panel * * @return mixed * @@ -100,7 +97,7 @@ protected function getNames(Profile $profile) list($name, $template) = $arguments; - if (!$this->profiler->has($name) || !$profile->hasCollector($name)) { + if (!$profile->has($name)) { continue; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml index d85b54d38db70..e2ca1140c2ace 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml @@ -11,12 +11,12 @@ - + - + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml index ed7e923f0d05d..a3be6f8bb35f4 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml @@ -15,6 +15,7 @@ + %data_collector.templates% %web_profiler.debug_toolbar.position% @@ -22,12 +23,14 @@ + + %kernel.debug% diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index 07dd6bb23e777..ad754f145c88a 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -3,23 +3,23 @@ {% from _self import form_tree_entry, form_tree_details %} {% block toolbar %} - {% if collector.data.nb_errors > 0 or collector.data.forms|length %} - {% set status_color = collector.data.nb_errors ? 'red' : '' %} + {% if collector.nbErrors > 0 or collector.forms|length %} + {% set status_color = collector.nbErrors ? 'red' : '' %} {% set icon %} {{ include('@WebProfiler/Icon/form.svg') }} - {{ collector.data.nb_errors ?: collector.data.forms|length }} + {{ collector.nb_errors ?: collector.forms|length }} {% endset %} {% set text %}
Number of forms - {{ collector.data.forms|length }} + {{ collector.forms|length }}
Number of errors - {{ collector.data.nb_errors }} + {{ collector.nbErrors }}
{% endset %} @@ -28,12 +28,12 @@ {% endblock %} {% block menu %} - + {{ include('@WebProfiler/Icon/form.svg') }} Forms - {% if collector.data.nb_errors > 0 %} + {% if collector.nb_errors > 0 %} - {{ collector.data.nb_errors }} + {{ collector.nb_errors }} {% endif %} @@ -188,18 +188,18 @@ {% block panel %}

Forms

- {% if collector.data.forms|length %} + {% if collector.forms|length %}
    - {% for formName, formData in collector.data.forms %} - {{ form_tree_entry(formName, formData, true) }} + {% for formHash, formData in collector.forms %} + {{ form_tree_entry(formHash, formData, true) }} {% endfor %}
- {% for formName, formData in collector.data.forms %} - {{ form_tree_details(formName, formData, collector.data.forms_by_hash) }} + {% for formHash, formData in collector.forms %} + {{ form_tree_details(formHash, formData) }} {% endfor %}
{% else %} @@ -437,7 +437,7 @@
{% endif %} - {{ name|default('(no name)') }} {% if data.type_class is defined and data.type is defined %}[{{ data.type }}]{% endif %} + {{ data.name|default('(no name)') }} {% if data.type_class is defined and data.type is defined %}[{{ data.type }}]{% endif %} {% if data.errors is defined and data.errors|length > 0 %}
{{ data.errors|length }}
@@ -458,7 +458,7 @@ {% import _self as tree %}

- {{ name|default('(no name)') }} + {{ data.name|default('(no name)') }} {% if data.type_class is defined and data.type is defined %} [{{ data.type }}] {% endif %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig index db842c5e0e64d..29c3c203f3a79 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig @@ -199,7 +199,7 @@ (token = {{ profile.parent.token }})

- {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: profile.parent.getcollector('request').requestattributes }, with_context = false) }} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: profile.parent.get('request').requestattributes }, with_context = false) }}
{% endif %} @@ -212,12 +212,12 @@ {% for child in profile.children %}

- {{- child.getcollector('request').requestattributes.get('_controller') -}} + {{- child.get('request').requestattributes.get('_controller') -}} (token = {{ child.token }})

- {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: child.getcollector('request').requestattributes }, with_context = false) }} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: child.fet('request').requestattributes }, with_context = false) }} {% endfor %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig index 1c065ed5fc295..61ea0821cdeda 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig @@ -68,7 +68,7 @@ {% set subrequests_time = 0 %} {% for child in profile.children %} - {% set subrequests_time = subrequests_time + child.getcollector('time').events.__section__.duration %} + {% set subrequests_time = subrequests_time + child.get('time').events.__section__.duration %} {% endfor %}
@@ -106,7 +106,7 @@ {% if profile.parent %}

- Sub-Request {{ profile.getcollector('request').requestattributes.get('_controller') }} + Sub-Request {{ profile.get('request').requestattributes.get('_controller') }} {{ collector.events.__section__.duration }} ms Return to parent request @@ -126,9 +126,9 @@

Sub-requests ({{ profile.children|length }})

{% for child in profile.children %} - {% set events = child.getcollector('time').events %} + {% set events = child.get('time').events %}

- {{ child.getcollector('request').requestattributes.get('_controller') }} + {{ child.get('request').requestattributes.get('_controller') }} {{ events.__section__.duration }} ms

@@ -458,7 +458,7 @@ {% if profile.children|length %} , {% for child in profile.children %} -{{ helper.dump_request_data(child.token, child, child.getcollector('time').events, collector.events.__section__.origin) }}{{ loop.last ? '' : ',' }} +{{ helper.dump_request_data(child.token, child, child.get('time').events, collector.events.__section__.origin) }}{{ loop.last ? '' : ',' }} {% endfor %} {% endif %} ] diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/search.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/search.html.twig index 3661c8abc6468..aee09025cfe85 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/search.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/search.html.twig @@ -2,27 +2,27 @@
- +
- +
- +
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig index 53ca5591d3a27..4598c19b0eabf 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.html.twig @@ -27,7 +27,7 @@
{% for name, template in templates %} {{ template.renderblock('toolbar', { - 'collector': profile.getcollector(name), + 'collector': profile.get(name), 'profiler_url': profiler_url, 'token': profile.token, 'name': name, diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php index f8cec7a9c8b41..8f9efa52ab696 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php @@ -12,7 +12,8 @@ namespace Symfony\Bundle\WebProfilerBundle\Tests\Controller; use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController; -use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\Profiler\HttpProfile; +use Symfony\Component\Profiler\Profile; use Symfony\Component\HttpFoundation\Request; class ProfilerControllerTest extends \PHPUnit_Framework_TestCase @@ -25,11 +26,12 @@ public function testEmptyToken($token) $urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); $twig = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->getMockBuilder('Symfony\Component\Profiler\Profiler') ->disableOriginalConstructor() ->getMock(); + $profilerStorage = $this->getMockBuilder('Symfony\Component\Profiler\Storage\FileProfilerStorage')->disableOriginalConstructor()->getMock(); - $controller = new ProfilerController($urlGenerator, $profiler, $twig, array()); + $controller = new ProfilerController($urlGenerator, $profiler, $profilerStorage, $twig, array()); $response = $controller->toolbarAction(Request::create('/_wdt/empty'), $token); $this->assertEquals(200, $response->getStatusCode()); @@ -49,18 +51,19 @@ public function testReturns404onTokenNotFound() $urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); $twig = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->getMockBuilder('Symfony\Component\Profiler\Profiler') ->disableOriginalConstructor() ->getMock(); + $profilerStorage = $this->getMockBuilder('Symfony\Component\Profiler\Storage\FileProfilerStorage')->disableOriginalConstructor()->getMock(); - $controller = new ProfilerController($urlGenerator, $profiler, $twig, array()); + $controller = new ProfilerController($urlGenerator, $profiler, $profilerStorage, $twig, array()); - $profiler + $profilerStorage ->expects($this->exactly(2)) - ->method('loadProfile') + ->method('read') ->will($this->returnCallback(function ($token) { if ('found' == $token) { - return new Profile($token); + return new Profile('TOKEN'); } return; @@ -79,11 +82,12 @@ public function testSearchResult() $urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); $twig = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->getMockBuilder('Symfony\Component\Profiler\Profiler') ->disableOriginalConstructor() ->getMock(); + $profilerStorage = $this->getMockBuilder('Symfony\Component\Profiler\Storage\FileProfilerStorage')->disableOriginalConstructor()->getMock(); - $controller = new ProfilerController($urlGenerator, $profiler, $twig, array()); + $controller = new ProfilerController($urlGenerator, $profiler, $profilerStorage, $twig, array()); $tokens = array( array( @@ -105,9 +109,13 @@ public function testSearchResult() 'status_code' => 404, ), ); - $profiler + $profilerStorage ->expects($this->once()) - ->method('find') + ->method('read') + ->will($this->returnValue(null)); + $profilerStorage + ->expects($this->once()) + ->method('findBy') ->will($this->returnValue($tokens)); $request = Request::create('/_profiler/empty/search/results', 'GET', array( @@ -120,17 +128,15 @@ public function testSearchResult() $twig->expects($this->once()) ->method('render') ->with($this->stringEndsWith('results.html.twig'), $this->equalTo(array( - 'token' => 'empty', 'profile' => null, 'tokens' => $tokens, - 'ip' => '127.0.0.1', - 'method' => 'GET', - 'url' => 'http://example.com/', 'start' => null, 'end' => null, 'limit' => 2, 'panel' => null, 'request' => $request, + 'filters' => array(), + 'token' => 'empty' ))); $response = $controller->searchResultsAction($request, 'empty'); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php index a3732fde425b1..ff5deff541c40 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php @@ -56,9 +56,10 @@ protected function setUp() $this->container->setParameter('kernel.cache_dir', __DIR__); $this->container->setParameter('kernel.debug', false); $this->container->setParameter('kernel.root_dir', __DIR__); - $this->container->setParameter('profiler.class', array('Symfony\\Component\\HttpKernel\\Profiler\\Profiler')); - $this->container->register('profiler', $this->getMockClass('Symfony\\Component\\HttpKernel\\Profiler\\Profiler')) - ->addArgument(new Definition($this->getMockClass('Symfony\\Component\\HttpKernel\\Profiler\\ProfilerStorageInterface'))); + $this->container->setParameter('profiler.class', array('Symfony\\Component\\Profiler\\Profiler')); + $this->container->register('profiler.storage', $this->getMockClass('Symfony\\Component\\Profiler\\Storage\\ProfilerStorageInterface')); + $this->container->register('profiler', $this->getMockClass('Symfony\\Component\\Profiler\\Profiler')) + ->addArgument(new Reference('profiler.storage')); $this->container->setParameter('data_collector.templates', array()); $this->container->set('kernel', $this->kernel); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/profile.data b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/profile.data index ab76cea63de6d..c28458f1c11b5 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/profile.data +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Fixtures/profile.data @@ -1 +1 @@ -Tzo0NToiU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlIjo4OntzOjUyOiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAHRva2VuIjtzOjU6IlRPS0VOIjtzOjUzOiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAHBhcmVudCI7TjtzOjU1OiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAGNoaWxkcmVuIjthOjA6e31zOjU3OiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAGNvbGxlY3RvcnMiO2E6MDp7fXM6NDk6IgBTeW1mb255XENvbXBvbmVudFxIdHRwS2VybmVsXFByb2ZpbGVyXFByb2ZpbGUAaXAiO047czo1MzoiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQBtZXRob2QiO047czo1MDoiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQB1cmwiO047czo1MToiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQB0aW1lIjtOO30= \ No newline at end of file +Tzo0NToiU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlIjoxMzp7czo1MjoiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQB0b2tlbiI7TjtzOjU3OiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAGNvbGxlY3RvcnMiO2E6MDp7fXM6NTE6IgBTeW1mb255XENvbXBvbmVudFxIdHRwS2VybmVsXFByb2ZpbGVyXFByb2ZpbGUAdGltZSI7TjtzOjU3OiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAHN0YXR1c0NvZGUiO047czo0OToiAFN5bWZvbnlcQ29tcG9uZW50XEh0dHBLZXJuZWxcUHJvZmlsZXJcUHJvZmlsZQBpcCI7TjtzOjUzOiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAG1ldGhvZCI7TjtzOjUwOiIAU3ltZm9ueVxDb21wb25lbnRcSHR0cEtlcm5lbFxQcm9maWxlclxQcm9maWxlAHVybCI7TjtzOjQwOiIAU3ltZm9ueVxDb21wb25lbnRcUHJvZmlsZXJcUHJvZmlsZQBkYXRhIjthOjA6e31zOjQyOiIAU3ltZm9ueVxDb21wb25lbnRcUHJvZmlsZXJcUHJvZmlsZQBwYXJlbnQiO047czo0NDoiAFN5bWZvbnlcQ29tcG9uZW50XFByb2ZpbGVyXFByb2ZpbGUAY2hpbGRyZW4iO2E6MDp7fXM6NDM6IgBTeW1mb255XENvbXBvbmVudFxQcm9maWxlclxQcm9maWxlAGluZGV4ZXMiO2E6MDp7fXM6NDE6IgBTeW1mb255XENvbXBvbmVudFxQcm9maWxlclxQcm9maWxlAHRva2VuIjtzOjU6IlRPS0VOIjtzOjQwOiIAU3ltZm9ueVxDb21wb25lbnRcUHJvZmlsZXJcUHJvZmlsZQB0aW1lIjtpOjE0NDYxMTk1NjM7fQ== \ No newline at end of file diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php index 5c8a3becb8326..c6cab73e451ac 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php @@ -26,11 +26,6 @@ class TemplateManagerTest extends TestCase */ protected $twigEnvironment; - /** - * @var \Symfony\Component\HttpKernel\Profiler\Profiler - */ - protected $profiler; - /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -45,7 +40,6 @@ protected function setUp() { parent::setUp(); - $profiler = $this->mockProfiler(); $twigEnvironment = $this->mockTwigEnvironment(); $templates = array( 'data_collector.foo' => array('foo','FooBundle:Collector:foo'), @@ -53,7 +47,7 @@ protected function setUp() 'data_collector.baz' => array('baz','FooBundle:Collector:baz'), ); - $this->templateManager = new TemplateManager($profiler, $twigEnvironment, $templates); + $this->templateManager = new TemplateManager($twigEnvironment, $templates); } /** @@ -70,15 +64,10 @@ public function testGetNameOfInvalidTemplate() */ public function testGetNameValidTemplate() { - $this->profiler->expects($this->any()) - ->method('has') - ->withAnyParameters() - ->will($this->returnCallback(array($this, 'profilerHasCallback'))); - $profile = $this->mockProfile(); $profile->expects($this->any()) - ->method('hasCollector') - ->will($this->returnCallback(array($this, 'profileHasCollectorCallback'))); + ->method('has') + ->will($this->returnCallback(array($this, 'profileHasCallback'))); $this->assertEquals('FooBundle:Collector:foo.html.twig', $this->templateManager->getName($profile, 'foo')); } @@ -91,21 +80,16 @@ public function testGetTemplates() { $profile = $this->mockProfile(); $profile->expects($this->any()) - ->method('hasCollector') - ->will($this->returnCallback(array($this, 'profilerHasCallback'))); - - $this->profiler->expects($this->any()) ->method('has') - ->withAnyParameters() - ->will($this->returnCallback(array($this, 'profileHasCollectorCallback'))); + ->will($this->returnCallback(array($this, 'profileHasCallback'))); $result = $this->templateManager->getTemplates($profile); $this->assertArrayHasKey('foo', $result); - $this->assertArrayNotHasKey('bar', $result); + $this->assertArrayHasKey('bar', $result); $this->assertArrayNotHasKey('baz', $result); } - public function profilerHasCallback($panel) + public function profileHasCallback($panel) { switch ($panel) { case 'foo': @@ -116,27 +100,16 @@ public function profilerHasCallback($panel) } } - public function profileHasCollectorCallback($panel) - { - switch ($panel) { - case 'foo': - case 'baz': - return true; - default: - return false; - } - } - protected function mockProfile() { - $this->profile = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profile') + $this->profile = $this->getMockBuilder('Symfony\Component\Profiler\Profile') ->disableOriginalConstructor() ->getMock(); return $this->profile; } - protected function mockTwigEnvironment() + protected function mockTwigEnvironment( ) { $this->twigEnvironment = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock(); @@ -150,13 +123,4 @@ protected function mockTwigEnvironment() return $this->twigEnvironment; } - - protected function mockProfiler() - { - $this->profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock(); - - return $this->profiler; - } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index edea43b002b26..a7e2fd33775a8 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -17,7 +17,8 @@ ], "require": { "php": ">=5.3.9", - "symfony/http-kernel": "~2.4|~3.0.0", + "symfony/http-kernel": "~2.8|~3.0.0", + "symfony/profiler": "~2.8|~3.0.0", "symfony/routing": "~2.2|~3.0.0", "symfony/twig-bridge": "~2.7|~3.0.0" }, diff --git a/src/Symfony/Component/Debug/Profiler/ExceptionData.php b/src/Symfony/Component/Debug/Profiler/ExceptionData.php new file mode 100644 index 0000000000000..586fc3d788a27 --- /dev/null +++ b/src/Symfony/Component/Debug/Profiler/ExceptionData.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Profiler; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +/** + * Class ExceptionData. + * + * @author Jelte Steijaert + */ +class ExceptionData implements ProfileDataInterface +{ + private $exception; + + public function __construct(FlattenException $exception = null) + { + $this->exception = $exception; + } + + /** + * Checks if the exception is not null. + * + * @return bool true if the exception is not null, false otherwise + */ + public function hasException() + { + return isset($this->exception); + } + + /** + * Gets the exception. + * + * @return \Exception The exception + */ + public function getException() + { + return $this->exception; + } + + /** + * Gets the exception message. + * + * @return string The exception message + */ + public function getMessage() + { + return $this->exception->getMessage(); + } + + /** + * Gets the exception code. + * + * @return int The exception code + */ + public function getCode() + { + return $this->exception->getCode(); + } + + /** + * Gets the status code. + * + * @return int The status code + */ + public function getStatusCode() + { + return $this->exception->getStatusCode(); + } + + /** + * Gets the exception trace. + * + * @return array The exception trace + */ + public function getTrace() + { + return $this->exception->getTrace(); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'exception'; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return serialize($this->exception); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $this->exception = unserialize($serialized); + } +} diff --git a/src/Symfony/Component/Debug/Profiler/ExceptionDataCollector.php b/src/Symfony/Component/Debug/Profiler/ExceptionDataCollector.php new file mode 100644 index 0000000000000..6cd4b4ead4b27 --- /dev/null +++ b/src/Symfony/Component/Debug/Profiler/ExceptionDataCollector.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Profiler; + +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; + +/** + * ExceptionDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class ExceptionDataCollector implements DataCollectorInterface, EventSubscriberInterface +{ + private $exception; + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + if (null === $this->exception) { + return; + } + + $exception = FlattenException::create($this->exception); + + return new ExceptionData($exception); + } + + /** + * Handles the onKernelTerminate event. + * + * @param Event $event + */ + public function onException(Event $event) + { + if (method_exists($event, 'getException')) { + $this->exception = $event->getException(); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + $events = array(); + + if (defined('Symfony\Component\HttpKernel\KernelEvents::EXCEPTION')) { + $events[KernelEvents::EXCEPTION] = array('onException'); + } + if (defined('Symfony\Component\Console\ConsoleEvents::EXCEPTION')) { + $events[ConsoleEvents::EXCEPTION] = array('onException'); + } + + return $events; + } +} diff --git a/src/Symfony/Component/Debug/Tests/Profiler/ExceptionDataCollectorTest.php b/src/Symfony/Component/Debug/Tests/Profiler/ExceptionDataCollectorTest.php new file mode 100644 index 0000000000000..749103df660a1 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Profiler/ExceptionDataCollectorTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests\Profiler; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\Profiler\ExceptionDataCollector; +use Symfony\Component\EventDispatcher\Event; + +class ExceptionDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $e = new \Exception('foo', 500); + $c = new ExceptionDataCollector(); + $flattened = FlattenException::create($e); + $trace = $flattened->getTrace(); + + $data = $c->getCollectedData(); + $this->assertNull($data); + + $c->onException(new CustomExceptionEvent($e)); + $data = $c->getCollectedData(); + + $this->assertInstanceOf('Symfony\Component\Debug\Profiler\ExceptionData', $data); + + $this->assertTrue($data->hasException()); + $this->assertEquals($flattened, $data->getException()); + $this->assertSame('foo', $data->getMessage()); + $this->assertSame(500, $data->getCode()); + $this->assertSame(500, $data->getStatusCode()); + $this->assertSame($trace, $data->getTrace()); + } +} + +class CustomExceptionEvent extends Event +{ + private $exception; + + public function __construct(\Exception $exception) + { + $this->exception = $exception; + } + + public function getException() + { + return $this->exception; + } +} diff --git a/src/Symfony/Component/Debug/composer.json b/src/Symfony/Component/Debug/composer.json index e739f7560390d..940516088da8e 100644 --- a/src/Symfony/Component/Debug/composer.json +++ b/src/Symfony/Component/Debug/composer.json @@ -24,7 +24,11 @@ }, "require-dev": { "symfony/class-loader": "~2.2|~3.0.0", - "symfony/http-kernel": "~2.3.24|~2.5.9|~2.6,>=2.6.2|~3.0.0" + "symfony/http-kernel": "~2.3.24|~2.5.9|~2.6,>=2.6.2|~3.0.0", + "symfony/profiler": "~2.8|~3.0.0" + }, + "suggest": { + "symfony/profiler": "For Profile DataCollection." }, "autoload": { "psr-4": { "Symfony\\Component\\Debug\\": "" }, diff --git a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php index e16627d6ad91b..0554de76ececf 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php +++ b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php @@ -25,14 +25,16 @@ class WrappedListener private $called; private $stoppedPropagation; private $stopwatch; + private $priority; private $dispatcher; - public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatcherInterface $dispatcher = null) + public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatcherInterface $dispatcher = null, $priority = 0) { $this->listener = $listener; $this->name = $name; $this->stopwatch = $stopwatch; $this->dispatcher = $dispatcher; + $this->priority = $priority; $this->called = false; $this->stoppedPropagation = false; } @@ -47,6 +49,11 @@ public function wasCalled() return $this->called; } + public function priority() + { + return $this->priority; + } + public function stoppedPropagation() { return $this->stoppedPropagation; diff --git a/src/Symfony/Component/EventDispatcher/Profiler/EventData.php b/src/Symfony/Component/EventDispatcher/Profiler/EventData.php new file mode 100644 index 0000000000000..e8bc4e8a14ee9 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Profiler/EventData.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Profiler; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +/** + * Class EventData. + * + * @author Jelte Steijaert + */ +class EventData implements ProfileDataInterface +{ + private $calledListeners; + private $notCalledListeners; + + public function __construct(array $calledListeners = array(), array $notCalledListeners = array()) + { + $this->calledListeners = $calledListeners; + $this->notCalledListeners = $notCalledListeners; + } + + /** + * Gets the called listeners. + * + * @return array An array of called listeners + * + * @see TraceableEventDispatcherInterface + */ + public function getCalledListeners() + { + return $this->calledListeners; + } + + /** + * Gets the not called listeners. + * + * @return array An array of not called listeners + * + * @see TraceableEventDispatcherInterface + */ + public function getNotCalledListeners() + { + return $this->notCalledListeners; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'events'; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return serialize(array('called' => $this->calledListeners, 'notCalled' => $this->notCalledListeners)); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $data = unserialize($serialized); + $this->calledListeners = $data['called']; + $this->notCalledListeners = $data['notCalled']; + } +} diff --git a/src/Symfony/Component/EventDispatcher/Profiler/EventDataCollector.php b/src/Symfony/Component/EventDispatcher/Profiler/EventDataCollector.php new file mode 100644 index 0000000000000..8a4a71eb84370 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Profiler/EventDataCollector.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\EventDispatcher\Profiler; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface; +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface; + +/** + * EventDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class EventDataCollector implements LateDataCollectorInterface +{ + private $dispatcher; + + public function __construct(EventDispatcherInterface $dispatcher = null) + { + $this->dispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + if ($this->dispatcher instanceof TraceableEventDispatcherInterface) { + return new EventData($this->dispatcher->getCalledListeners(), $this->dispatcher->getNotCalledListeners()); + } + } +} diff --git a/src/Symfony/Component/EventDispatcher/Tests/Profiler/EventDataCollectorTest.php b/src/Symfony/Component/EventDispatcher/Tests/Profiler/EventDataCollectorTest.php new file mode 100644 index 0000000000000..a4030d3d4a9c9 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Tests/Profiler/EventDataCollectorTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Tests\Profiler; + +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\Profiler\EventData; +use Symfony\Component\EventDispatcher\Profiler\EventDataCollector; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\Stopwatch\Stopwatch; + +class EventDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + + $dispatcher->addListener('test', function () { }); + + $c = new EventDataCollector($dispatcher); + + /** @var EventData $data */ + $data = $c->getCollectedData(); + $this->assertInstanceof('Symfony\Component\EventDispatcher\Profiler\EventData', $data); + $this->assertCount(0, $data->getCalledListeners()); + $this->assertCount(1, $data->getNotCalledListeners()); + + $dispatcher->dispatch('test'); + + $data = $c->getCollectedData(); + $this->assertCount(1, $data->getCalledListeners()); + $this->assertCount(0, $data->getNotCalledListeners()); + } + + public function testCollectWithoutEventDispatcher() + { + $c = new EventDataCollector(null); + + $data = $c->getCollectedData(); + $this->assertNull($data); + } +} diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index 282b770c8bcd8..3ba5ba0889842 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -22,12 +22,14 @@ "symfony/dependency-injection": "~2.6|~3.0.0", "symfony/expression-language": "~2.6|~3.0.0", "symfony/config": "~2.0,>=2.0.5|~3.0.0", + "symfony/profiler": "~2.8|~3.0.0", "symfony/stopwatch": "~2.3|~3.0.0", "psr/log": "~1.0" }, "suggest": { "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/http-kernel": "", + "symfony/profiler": "" }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" }, diff --git a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php index 70b347fa9af3a..4bd5a59f79cc2 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php @@ -21,6 +21,8 @@ * * @author Robert Schönthal * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Profiler\DataCollectorExtension instead. */ class DataCollectorExtension extends AbstractExtension { diff --git a/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php index 8fd70f09b1796..78a0c2a9c41ea 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php @@ -23,6 +23,8 @@ * @since 2.4 * * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. */ class DataCollectorListener implements EventSubscriberInterface { diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php index a57693a995241..f961808599ad9 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php @@ -21,6 +21,8 @@ * @since 2.4 * * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Profiler\FormDataCollectorInterface instead. */ interface FormDataCollectorInterface extends DataCollectorInterface { diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php index 1c678ac665d09..ae80b44a0b0e7 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php @@ -22,6 +22,8 @@ * @since 2.4 * * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Profiler\FormDataExtractor instead. */ class FormDataExtractor implements FormDataExtractorInterface { diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php index a5bb7d0ef30a1..6f8e8f1cab614 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php @@ -20,6 +20,8 @@ * @since 2.4 * * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Profiler\FormDataExtractorInterface instead. */ interface FormDataExtractorInterface { diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php index 65430f1d222b0..caf5c16d21cea 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php @@ -24,6 +24,8 @@ * @since 2.4 * * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Profiler\Proxy\ResolvedTypeDataCollectorProxy instead. */ class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface { diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php index f5f4ed2e2095c..01429b5b69077 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php @@ -23,6 +23,8 @@ * @since 2.4 * * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Profiler\Proxy\ResolvedTypeFactoryDataCollectorProxy instead. */ class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface { diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php index e124031422964..c09142bcf0721 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php @@ -23,6 +23,8 @@ * * @author Robert Schönthal * @author Bernhard Schussek + * + * @deprecated since 2.8 and will be removed in 3.0. Use Symfony\Component\Form\Extension\Profiler\Type\DataCollectorTypeExtension instead. */ class DataCollectorTypeExtension extends AbstractTypeExtension { diff --git a/src/Symfony/Component/Form/Extension/Profiler/DataCollectorExtension.php b/src/Symfony/Component/Form/Extension/Profiler/DataCollectorExtension.php new file mode 100644 index 0000000000000..ab5d76b7c6439 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/DataCollectorExtension.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Profiler; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\AbstractExtension; + +/** + * Extension for collecting data of the forms on a page. + * + * @since 2.4 + * @author Robert Schönthal + * @author Bernhard Schussek + */ +class DataCollectorExtension extends AbstractExtension +{ + /** + * @var EventSubscriberInterface + */ + private $dataCollector; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + protected function loadTypeExtensions() + { + return array( + new Type\DataCollectorTypeExtension($this->dataCollector), + ); + } +} diff --git a/src/Symfony/Component/Form/Extension/Profiler/FormData.php b/src/Symfony/Component/Form/Extension/Profiler/FormData.php new file mode 100644 index 0000000000000..7e2b601224fc8 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/FormData.php @@ -0,0 +1,57 @@ +forms = $forms; + $this->nbErrors = $nbErrors; + } + + public function getForms() + { + return $this->forms; + } + + public function getNbErrors() + { + return $this->nbErrors; + } + + /** + * Returns the name of the collector. + * + * @return string The collector name + * + * @api + */ + public function getName() + { + return 'form'; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return serialize(array('forms' => $this->forms, 'nbErrors' => $this->nbErrors)); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $unserialized = unserialize($serialized); + $this->forms = $unserialized['forms']; + $this->nbErrors = $unserialized['nbErrors']; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/Extension/Profiler/FormDataCollector.php b/src/Symfony/Component/Form/Extension/Profiler/FormDataCollector.php new file mode 100644 index 0000000000000..1e1b50b4a2293 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/FormDataCollector.php @@ -0,0 +1,249 @@ +dataExtractor = $dataExtractor; + } + + /** + * Collects data as late as possible. + * + * @return ProfileDataInterface + */ + public function getCollectedData() + { + return new FormData($this->forms, $this->nbErrors); + } + + /** + * Listener for the {@link FormEvents::POST_SET_DATA} event. + * + * @param FormEvent $event The event object + */ + public function postSetData(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect basic information about each form + $this->collectConfiguration($event->getForm()); + + // Collect the default data + $this->collectDefaultData($event->getForm()); + } + } + + /** + * Listener for the {@link FormEvents::POST_SUBMIT} event. + * + * @param FormEvent $event The event object + */ + public function postSubmit(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect the submitted data of each form + $this->collectSubmittedData($event->getForm()); + + // Assemble a form tree + // This is done again after the view is built, but we need it here as the view is not always created. + $this->buildPreliminaryFormTree($event->getForm()); + } + } + + /** + * {@inheritdoc} + */ + protected function collectConfiguration(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractConfiguration($form) + ); + + foreach ($form as $child) { + $this->collectConfiguration($child); + } + } + + /** + * {@inheritdoc} + */ + protected function collectDefaultData(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractDefaultData($form) + ); + + foreach ($form as $child) { + $this->collectDefaultData($child); + } + } + + + /** + * {@inheritdoc} + */ + protected function collectSubmittedData(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + // field was created by form event + $this->collectConfiguration($form); + $this->collectDefaultData($form); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractSubmittedData($form) + ); + + // Count errors + if (isset($this->dataByForm[$hash]['errors'])) { + $this->nbErrors += count($this->dataByForm[$hash]['errors']); + } + + foreach ($form as $child) { + $this->collectSubmittedData($child); + } + } + + /** + * {@inheritdoc} + */ + public function associateFormWithView(FormInterface $form, FormView $view) + { + $this->formsByView[spl_object_hash($view)] = spl_object_hash($form); + } + + /** + * {@inheritdoc} + */ + public function collectViewVariables(FormView $view) + { + $hash = spl_object_hash($view); + + if (!isset($this->dataByView[$hash])) { + $this->dataByView[$hash] = array(); + } + + $this->dataByView[$hash] = array_replace( + $this->dataByView[$hash], + $this->dataExtractor->extractViewVariables($view) + ); + + foreach ($view->children as $child) { + $this->collectViewVariables($child); + } + } + + /** + * {@inheritdoc} + */ + private function buildPreliminaryFormTree(FormInterface $form) + { + $this->forms[spl_object_hash($form)] = $this->recursiveBuildPreliminaryFormTree($form); + } + + /** + * {@inheritdoc} + */ + public function buildFinalFormTree(FormInterface $form, FormView $view) + { + $this->forms[spl_object_hash($form)] = $this->recursiveBuildFinalFormTree($form, $view); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + // High priority in order to be called as soon as possible + FormEvents::POST_SET_DATA => array('postSetData', 255), + // Low priority in order to be called as late as possible + FormEvents::POST_SUBMIT => array('postSubmit', -255), + ); + } + + private function recursiveBuildPreliminaryFormTree(FormInterface $form) + { + $hash = spl_object_hash($form); + + $output = array_replace( + array('name' => $form->getName(), 'children' => array()), + isset($this->dataByForm[$hash]) ? $this->dataByForm[$hash] : array() + ); + + foreach ($form as $name => $child) { + $output['children'][spl_object_hash($child)] = $this->recursiveBuildPreliminaryFormTree($child); + } + return $output; + } + + private function recursiveBuildFinalFormTree(FormInterface $form = null, FormView $view) + { + $viewHash = spl_object_hash($view); + $formHash = null; + $formName = null; + + if (null !== $form) { + $formHash = spl_object_hash($form); + $formName = $form->getName(); + } elseif (isset($this->formsByView[$viewHash])) { + // The FormInterface instance of the CSRF token is never contained in + // the FormInterface tree of the form, so we need to get the + // corresponding FormInterface instance for its view in a different way + $formHash = $this->formsByView[$viewHash]; + } + + $output = array_replace( + array('name' => $formName, 'children' => array()), + isset($this->dataByView[$viewHash]) ? $this->dataByView[$viewHash] : array(), + null !== $formHash && isset($this->dataByForm[$formHash]) ? $this->dataByForm[$formHash] : array() + ); + + foreach ($view->children as $name => $childView) { + // The CSRF token, for example, is never added to the form tree. + // It is only present in the view. + $childForm = null !== $form && $form->has($name) ? $form->get($name) : null; + + $childHash = null !== $childForm?spl_object_hash($childForm):spl_object_hash($childView); + $output['children'][$childHash] = array_replace($this->recursiveBuildFinalFormTree($childForm, $childView), array('name' => $name)); + } + + return $output; + } + +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/Extension/Profiler/FormDataCollectorInterface.php b/src/Symfony/Component/Form/Extension/Profiler/FormDataCollectorInterface.php new file mode 100644 index 0000000000000..812ebaaa2de9f --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/FormDataCollectorInterface.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Profiler; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; + +/** + * Collects and structures information about forms. + * + * @since 2.8 + * @author Bernhard Schussek + */ +interface FormDataCollectorInterface extends DataCollectorInterface, EventSubscriberInterface +{ + /** + * Stores the view variables of the given form view and its children. + * + * @param FormView $view A root form view + */ + public function collectViewVariables(FormView $view); + + /** + * Specifies that the given objects represent the same conceptual form. + * + * @param FormInterface $form A form object + * @param FormView $view A view object + */ + public function associateFormWithView(FormInterface $form, FormView $view); + + /** + * Assembles the data collected about the given form and its children as + * a tree-like data structure. + * + * The result can be queried using {@link getData()}. + * + * Contrary to {@link buildPreliminaryFormTree()}, a {@link FormView} + * object has to be passed. The tree structure of this view object will be + * used for structuring the resulting data. That means, if a child is + * present in the view, but not in the form, it will be present in the final + * data array anyway. + * + * When {@link FormView} instances are present in the view tree, for which + * no corresponding {@link FormInterface} objects can be found in the form + * tree, only the view data will be included in the result. If a + * corresponding {@link FormInterface} exists otherwise, call + * {@link associateFormWithView()} before calling this method. + * + * @param FormInterface $form A root form + * @param FormView $view A root view + */ + public function buildFinalFormTree(FormInterface $form, FormView $view); +} diff --git a/src/Symfony/Component/Form/Extension/Profiler/FormDataExtractor.php b/src/Symfony/Component/Form/Extension/Profiler/FormDataExtractor.php new file mode 100644 index 0000000000000..20f28e39c9e87 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/FormDataExtractor.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Profiler; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Profiler\ProfileData\Util\ValueExporter; +use Symfony\Component\Validator\ConstraintViolationInterface; + +/** + * Default implementation of {@link FormDataExtractorInterface}. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class FormDataExtractor implements FormDataExtractorInterface +{ + /** + * @var ValueExporter + */ + private $valueExporter; + + /** + * Constructs a new data extractor. + * + * @param ValueExporter|null $valueExporter + */ + public function __construct(ValueExporter $valueExporter = null) + { + $this->valueExporter = $valueExporter ?: new ValueExporter(); + } + + /** + * {@inheritdoc} + */ + public function extractConfiguration(FormInterface $form) + { + $data = array( + 'id' => $this->buildId($form), + 'name' => $form->getName(), + 'type' => $form->getConfig()->getType()->getName(), + 'type_class' => get_class($form->getConfig()->getType()->getInnerType()), + 'synchronized' => $this->valueExporter->exportValue($form->isSynchronized()), + 'passed_options' => array(), + 'resolved_options' => array(), + ); + + foreach ($form->getConfig()->getAttribute('data_collector/passed_options', array()) as $option => $value) { + $data['passed_options'][$option] = $this->valueExporter->exportValue($value); + } + + foreach ($form->getConfig()->getOptions() as $option => $value) { + $data['resolved_options'][$option] = $this->valueExporter->exportValue($value); + } + + ksort($data['passed_options']); + ksort($data['resolved_options']); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractDefaultData(FormInterface $form) + { + $data = array( + 'default_data' => array( + 'norm' => $this->valueExporter->exportValue($form->getNormData()), + ), + 'submitted_data' => array(), + ); + + if ($form->getData() !== $form->getNormData()) { + $data['default_data']['model'] = $this->valueExporter->exportValue($form->getData()); + } + + if ($form->getViewData() !== $form->getNormData()) { + $data['default_data']['view'] = $this->valueExporter->exportValue($form->getViewData()); + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractSubmittedData(FormInterface $form) + { + $data = array( + 'submitted_data' => array( + 'norm' => $this->valueExporter->exportValue($form->getNormData()), + ), + 'errors' => array(), + ); + + if ($form->getViewData() !== $form->getNormData()) { + $data['submitted_data']['view'] = $this->valueExporter->exportValue($form->getViewData()); + } + + if ($form->getData() !== $form->getNormData()) { + $data['submitted_data']['model'] = $this->valueExporter->exportValue($form->getData()); + } + + foreach ($form->getErrors() as $error) { + $errorData = array( + 'message' => $error->getMessage(), + 'origin' => is_object($error->getOrigin()) + ? spl_object_hash($error->getOrigin()) + : null, + 'trace' => array(), + ); + + $cause = $error->getCause(); + + while (null !== $cause) { + if ($cause instanceof ConstraintViolationInterface) { + $errorData['trace'][] = array( + 'class' => $this->valueExporter->exportValue(get_class($cause)), + 'root' => $this->valueExporter->exportValue($cause->getRoot()), + 'path' => $this->valueExporter->exportValue($cause->getPropertyPath()), + 'value' => $this->valueExporter->exportValue($cause->getInvalidValue()), + ); + + $cause = method_exists($cause, 'getCause') ? $cause->getCause() : null; + + continue; + } + + if ($cause instanceof \Exception) { + $errorData['trace'][] = array( + 'class' => $this->valueExporter->exportValue(get_class($cause)), + 'message' => $this->valueExporter->exportValue($cause->getMessage()), + ); + + $cause = $cause->getPrevious(); + + continue; + } + + $errorData['trace'][] = $cause; + + break; + } + + $data['errors'][] = $errorData; + } + + $data['synchronized'] = $this->valueExporter->exportValue($form->isSynchronized()); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractViewVariables(FormView $view) + { + $data = array(); + + // Set the ID in case no FormInterface object was collected for this + // view + if (!isset($data['id'])) { + $data['id'] = isset($view->vars['id']) ? $view->vars['id'] : null; + } + + if (!isset($data['name'])) { + $data['name'] = isset($view->vars['name']) ? $view->vars['name'] : null; + } + + foreach ($view->vars as $varName => $value) { + $data['view_vars'][$varName] = $this->valueExporter->exportValue($value); + } + + ksort($data['view_vars']); + + return $data; + } + + /** + * Recursively builds an HTML ID for a form. + * + * @param FormInterface $form The form + * + * @return string The HTML ID + */ + private function buildId(FormInterface $form) + { + $id = $form->getName(); + + if (null !== $form->getParent()) { + $id = $this->buildId($form->getParent()).'_'.$id; + } + + return $id; + } +} diff --git a/src/Symfony/Component/Form/Extension/Profiler/FormDataExtractorInterface.php b/src/Symfony/Component/Form/Extension/Profiler/FormDataExtractorInterface.php new file mode 100644 index 0000000000000..ef2ce96db56d6 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/FormDataExtractorInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Profiler; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; + +/** + * Extracts arrays of information out of forms. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface FormDataExtractorInterface +{ + /** + * Extracts the configuration data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's configuration + */ + public function extractConfiguration(FormInterface $form); + + /** + * Extracts the default data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's default data + */ + public function extractDefaultData(FormInterface $form); + + /** + * Extracts the submitted data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's submitted data + */ + public function extractSubmittedData(FormInterface $form); + + /** + * Extracts the view variables of a form. + * + * @param FormView $view The form view + * + * @return array Information about the view's variables + */ + public function extractViewVariables(FormView $view); +} diff --git a/src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeDataCollectorProxy.php new file mode 100644 index 0000000000000..bd005af7904d3 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeDataCollectorProxy.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Profiler\Proxy; + +use Symfony\Component\Form\Extension\Profiler\FormDataCollectorInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ResolvedFormTypeInterface; + +/** + * Proxy that invokes a data collector when creating a form and its view. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface +{ + /** + * @var ResolvedFormTypeInterface + */ + private $proxiedType; + + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(ResolvedFormTypeInterface $proxiedType, FormDataCollectorInterface $dataCollector) + { + $this->proxiedType = $proxiedType; + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->proxiedType->getName(); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return $this->proxiedType->getParent(); + } + + /** + * {@inheritdoc} + */ + public function getInnerType() + { + return $this->proxiedType->getInnerType(); + } + + /** + * {@inheritdoc} + */ + public function getTypeExtensions() + { + return $this->proxiedType->getTypeExtensions(); + } + + /** + * {@inheritdoc} + */ + public function createBuilder(FormFactoryInterface $factory, $name, array $options = array()) + { + $builder = $this->proxiedType->createBuilder($factory, $name, $options); + + $builder->setAttribute('data_collector/passed_options', $options); + $builder->setType($this); + + return $builder; + } + + /** + * {@inheritdoc} + */ + public function createView(FormInterface $form, FormView $parent = null) + { + return $this->proxiedType->createView($form, $parent); + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $this->proxiedType->buildForm($builder, $options); + } + + /** + * {@inheritdoc} + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->buildView($view, $form, $options); + } + + /** + * {@inheritdoc} + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->finishView($view, $form, $options); + + // Remember which view belongs to which form instance, so that we can + // get the collected data for a view when its form instance is not + // available (e.g. CSRF token) + $this->dataCollector->associateFormWithView($form, $view); + + // Since the CSRF token is only present in the FormView tree, we also + // need to check the FormView tree instead of calling isRoot() on the + // FormInterface tree + if (null === $view->parent) { + $this->dataCollector->collectViewVariables($view); + + // Re-assemble data, in case FormView instances were added, for + // which no FormInterface instances were present (e.g. CSRF token). + // Since finishView() is called after finishing the views of all + // children, we can safely assume that information has been + // collected about the complete form tree. + $this->dataCollector->buildFinalFormTree($form, $view); + } + } + + /** + * {@inheritdoc} + */ + public function getOptionsResolver() + { + return $this->proxiedType->getOptionsResolver(); + } +} diff --git a/src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeFactoryDataCollectorProxy.php new file mode 100644 index 0000000000000..23b3529dd4dbc --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/Proxy/ResolvedTypeFactoryDataCollectorProxy.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\Form\Extension\Profiler\Proxy; + +use Symfony\Component\Form\Extension\Profiler\FormDataCollectorInterface; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\Form\ResolvedFormTypeFactoryInterface; +use Symfony\Component\Form\ResolvedFormTypeInterface; + +/** + * Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy} + * instances. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface +{ + /** + * @var ResolvedFormTypeFactoryInterface + */ + private $proxiedFactory; + + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector) + { + $this->proxiedFactory = $proxiedFactory; + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null) + { + return new ResolvedTypeDataCollectorProxy( + $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent), + $this->dataCollector + ); + } +} diff --git a/src/Symfony/Component/Form/Extension/Profiler/Type/DataCollectorTypeExtension.php b/src/Symfony/Component/Form/Extension/Profiler/Type/DataCollectorTypeExtension.php new file mode 100644 index 0000000000000..a6c900a45874b --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Profiler/Type/DataCollectorTypeExtension.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\Form\Extension\Profiler\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Profiler\FormDataCollectorInterface; +use Symfony\Component\Form\FormBuilderInterface; + +/** + * Type extension for collecting data of a form with this type. + * + * @since 2.4 + * @author Robert Schönthal + * @author Bernhard Schussek + */ +class DataCollectorTypeExtension extends AbstractTypeExtension +{ + /** + * @var \Symfony\Component\EventDispatcher\EventSubscriberInterface + */ + private $dataCollector; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber($this->dataCollector); + } + + /** + * {@inheritdoc} + */ + public function getExtendedType() + { + return 'form'; + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Profiler/DataCollectorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Profiler/DataCollectorExtensionTest.php new file mode 100644 index 0000000000000..f9353edefdc67 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Profiler/DataCollectorExtensionTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Profiler; + +use Symfony\Component\Form\Extension\Profiler\DataCollectorExtension; + +/** + * @covers Symfony\Component\Form\Extension\DataCollector\DataCollectorExtension + */ +class DataCollectorExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DataCollectorExtension + */ + private $extension; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataCollector; + + protected function setUp() + { + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\Profiler\FormDataCollectorInterface'); + $this->extension = new DataCollectorExtension($this->dataCollector); + } + + public function testLoadTypeExtensions() + { + $typeExtensions = $this->extension->getTypeExtensions('form'); + + $this->assertInternalType('array', $typeExtensions); + $this->assertCount(1, $typeExtensions); + $this->assertInstanceOf('Symfony\Component\Form\Extension\Profiler\Type\DataCollectorTypeExtension', array_shift($typeExtensions)); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataCollectorTest.php new file mode 100644 index 0000000000000..713e0633cc275 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataCollectorTest.php @@ -0,0 +1,642 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Profiler; + +use Symfony\Component\Form\Extension\Profiler\FormDataCollector; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Extension\Profiler\FormData; + +class FormDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataExtractor; + + /** + * @var FormDataCollector + */ + private $dataCollector; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataMapper; + + /** + * @var Form + */ + private $form; + + /** + * @var Form + */ + private $childForm; + + /** + * @var FormView + */ + private $view; + + /** + * @var FormView + */ + private $childView; + + protected function setUp() + { + $this->dataExtractor = $this->getMock('Symfony\Component\Form\Extension\Profiler\FormDataExtractorInterface'); + $this->dataCollector = new FormDataCollector($this->dataExtractor); + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + $this->dataMapper = $this->getMock('Symfony\Component\Form\DataMapperInterface'); + $this->form = $this->createForm('name'); + $this->childForm = $this->createForm('child'); + $this->view = new FormView(); + $this->childView = new FormView(); + } + + public function testBuildPreliminaryFormTree() + { + $this->form->add($this->childForm); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($this->childForm) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + + $this->dataCollector->postSetData(new FormEvent($this->form, array())); + $this->dataCollector->postSubmit(new FormEvent($this->form, array())); + + $childFormData = array( + 'name' => 'child', + 'children' => array(), + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + ); + + $formData = array( + 'name' => 'name', + 'children' => array( + spl_object_hash($this->childForm) => $childFormData, + ), + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + ); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + + $this->assertSame(array( + spl_object_hash($this->form) => $formData, + ), $profileData->getForms()); + $this->assertSame(0, $profileData->getNbErrors()); + } + + public function testBuildMultiplePreliminaryFormTrees() + { + $form1 = $this->createForm('form1'); + $form2 = $this->createForm('form2'); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($form1) + ->will($this->returnValue(array('config' => 'foo'))); + + $this->dataExtractor->expects($this->at(1)) + ->method('extractDefaultData') + ->with($form1) + ->will($this->returnValue(array('default_data' => 'foo'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractConfiguration') + ->with($form2) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($form2) + ->will($this->returnValue(array('default_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($form1) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($form2) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + + $this->dataCollector->postSetData(new FormEvent($form1, array())); + $this->dataCollector->postSetData(new FormEvent($form2, array())); + $this->dataCollector->postSubmit(new FormEvent($form1, array())); + + $form1Data = array( + 'name' => 'form1', + 'children' => array(), + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + ); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + $this->assertSame( + array( + spl_object_hash($form1) => $form1Data, + ), + $profileData->getForms() + ); + + $this->dataCollector->postSubmit(new FormEvent($form2, array())); + + $form2Data = array( + 'name' => 'form2', + 'children' => array(), + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + ); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + $this->assertSame( + array( + spl_object_hash($form1) => $form1Data, + spl_object_hash($form2) => $form2Data, + ), + $profileData->getForms() + ); + + $this->assertSame(0, $profileData->getNbErrors()); + } + + public function testBuildSamePreliminaryFormTreeMultipleTimes() + { + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + + $this->dataExtractor->expects($this->at(1)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + + $this->dataExtractor->expects($this->at(3)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + + + $this->dataCollector->postSetData(new FormEvent($this->form, array())); + $this->dataCollector->postSubmit(new FormEvent($this->form, array())); + + $formData = array( + 'name' => 'name', + 'children' => array(), + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + ); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + + $this->assertSame( + array( + spl_object_hash($this->form) => $formData, + ), + $profileData->getForms() + ); + + $this->dataCollector->postSubmit(new FormEvent($this->form, array())); + + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + + $formData = array( + 'name' => 'name', + 'children' => array(), + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + ); + + $this->assertSame( + array( + spl_object_hash($this->form) => $formData, + ), + $profileData->getForms() + ); + } + + public function testBuildFinalFormTree() + { + $this->form->add($this->childForm); + $this->view->children['child'] = $this->childView; + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($this->childForm) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(6)) + ->method('extractViewVariables') + ->with($this->view) + ->will($this->returnValue(array('view_vars' => 'foo'))); + + $this->dataExtractor->expects($this->at(7)) + ->method('extractViewVariables') + ->with($this->childView) + ->will($this->returnValue(array('view_vars' => 'bar'))); + + $this->dataCollector->postSetData(new FormEvent($this->form, array())); + $this->dataCollector->postSubmit(new FormEvent($this->form, array())); + $this->dataCollector->collectViewVariables($this->view); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $childFormData = array( + 'name' => 'child', + 'children' => array(), + 'view_vars' => 'bar', + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + ); + + $formData = array( + 'name' => 'name', + 'children' => array( + spl_object_hash($this->childForm) => $childFormData, + ), + 'view_vars' => 'foo', + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + ); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + + $this->assertSame( + array( + spl_object_hash($this->form) => $formData, + ), + $profileData->getForms() + ); + } + + public function testFinalFormReliesOnFormViewStructure() + { + $this->form->add($child1 = $this->createForm('first')); + $this->form->add($child2 = $this->createForm('second')); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($child1) + ->will($this->returnValue(array('config' => 'bar'))); + $this->dataExtractor->expects($this->at(2)) + ->method('extractConfiguration') + ->with($child2) + ->will($this->returnValue(array('config' => 'saa'))); + + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + $this->dataExtractor->expects($this->at(4)) + ->method('extractDefaultData') + ->with($child1) + ->will($this->returnValue(array('default_data' => 'bar'))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractDefaultData') + ->with($child2) + ->will($this->returnValue(array('default_data' => 'saa'))); + + $this->dataExtractor->expects($this->at(6)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + $this->dataExtractor->expects($this->at(7)) + ->method('extractSubmittedData') + ->with($child1) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + $this->dataExtractor->expects($this->at(8)) + ->method('extractSubmittedData') + ->with($child2) + ->will($this->returnValue(array('submitted_data' => 'saa'))); + + $this->view->children['second'] = $this->childView; + + $this->dataCollector->postSubmit(new FormEvent($this->form, array())); + + $child1Data = array( + 'name' => 'first', + 'children' => array(), + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar' + ); + + $child2Data = array( + 'name' => 'second', + 'children' => array(), + 'config' => 'saa', + 'default_data' => 'saa', + 'submitted_data' => 'saa' + ); + + $formData = array( + 'name' => 'name', + 'children' => array(), + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo' + ); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + $this->assertSame( + array( + spl_object_hash($this->form) => array_replace( + $formData, + array( + 'children' => array( + spl_object_hash($child1) => $child1Data, + spl_object_hash($child2) => $child2Data, + ) + ) + ), + ), + $profileData->getForms() + ); + + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + $this->assertSame( + array( + spl_object_hash($this->form) => array_replace( + $formData, + array( + 'children' => array( + // "first" not present in FormView + spl_object_hash($child2) => $child2Data, + ) + ) + ), + ), + $profileData->getForms() + ); + } + + public function testChildViewsCanBeWithoutCorrespondingChildForms() + { + // don't add $this->childForm to $this->form! + + $this->view->children['child'] = $this->childView; + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + + $this->dataExtractor->expects($this->at(1)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->postSetData(new FormEvent($this->form, array())); + $this->dataCollector->postSetData(new FormEvent($this->childForm, array())); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + + $childFormData = array( + 'name' => 'child', + // no "config" key + 'children' => array(), + ); + + $formData = array( + 'name' => 'name', + 'children' => array( + spl_object_hash($this->childView) => $childFormData, + ), + 'config' => 'foo', + 'default_data' => 'foo' + ); + + $this->assertSame( + array( + spl_object_hash($this->form) => $formData, + ), + $profileData->getForms() + ); + } + + public function testChildViewsWithoutCorrespondingChildFormsMayBeExplicitlyAssociated() + { + // don't add $this->childForm to $this->form! + + $this->view->children['child'] = $this->childView; + + // but associate the two + $this->dataCollector->associateFormWithView($this->childForm, $this->childView); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + + $this->dataExtractor->expects($this->at(1)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->postSetData(new FormEvent($this->form, array())); + $this->dataCollector->postSetData(new FormEvent($this->childForm, array())); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $childFormData = array( + 'name' => 'child', + 'children' => array(), + 'config' => 'bar', + 'default_data' => 'bar', + ); + + $formData = array( + 'name' => 'name', + 'children' => array( + spl_object_hash($this->childView) => $childFormData, + ), + 'config' => 'foo', + 'default_data' => 'foo', + ); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + + $this->assertSame( + array( + spl_object_hash($this->form) => $formData, + ), + $profileData->getForms() + ); + } + + public function testCollectSubmittedDataCountsErrors() + { + $form1 = $this->createForm('form1'); + $childForm1 = $this->createForm('child1'); + $form2 = $this->createForm('form2'); + + $form1->add($childForm1); + $this->dataExtractor + ->method('extractConfiguration') + ->will($this->returnValue(array())); + $this->dataExtractor + ->method('extractDefaultData') + ->will($this->returnValue(array())); + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($form1) + ->will($this->returnValue(array('errors' => array('foo')))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($childForm1) + ->will($this->returnValue(array('errors' => array('bar', 'bam')))); + $this->dataExtractor->expects($this->at(8)) + ->method('extractSubmittedData') + ->with($form2) + ->will($this->returnValue(array('errors' => array('baz')))); + + $this->dataCollector->postSubmit(new FormEvent($form1, array())); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + $this->assertSame(3, $profileData->getNbErrors()); + + $this->dataCollector->postSubmit(new FormEvent($form2, array())); + + /** @var FormData $profileData */ + $profileData = $this->dataCollector->getCollectedData(); + $this->assertSame(4, $profileData->getNbErrors()); + + } + + private function createForm($name) + { + $builder = new FormBuilder($name, null, $this->dispatcher, $this->factory); + $builder->setCompound(true); + $builder->setDataMapper($this->dataMapper); + + return $builder->getForm(); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataExtractorTest.php b/src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataExtractorTest.php new file mode 100644 index 0000000000000..f08f4cffe8b96 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Profiler/FormDataExtractorTest.php @@ -0,0 +1,432 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Profiler; + +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\Profiler\FormDataExtractor; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; +use Symfony\Component\Profiler\ProfileData\Util\ValueExporter; + +class FormDataExtractorTest_SimpleValueExporter extends ValueExporter +{ + /** + * {@inheritdoc} + */ + public function exportValue($value, $depth = 1, $deep = false) + { + return is_object($value) ? sprintf('object(%s)', get_class($value)) : var_export($value, true); + } +} + +/** + * @author Bernhard Schussek + */ +class FormDataExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var FormDataExtractorTest_SimpleValueExporter + */ + private $valueExporter; + + /** + * @var FormDataExtractor + */ + private $dataExtractor; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + protected function setUp() + { + $this->valueExporter = new FormDataExtractorTest_SimpleValueExporter(); + $this->dataExtractor = new FormDataExtractor($this->valueExporter); + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + } + + public function testExtractConfiguration() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $form = $this->createBuilder('name') + ->setType($type) + ->getForm(); + + $this->assertSame(array( + 'id' => 'name', + 'name' => 'name', + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array(), + 'resolved_options' => array(), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractConfigurationSortsPassedOptions() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $options = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $form = $this->createBuilder('name') + ->setType($type) + // passed options are stored in an attribute by + // ResolvedTypeDataCollectorProxy + ->setAttribute('data_collector/passed_options', $options) + ->getForm(); + + $this->assertSame(array( + 'id' => 'name', + 'name' => 'name', + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + 'resolved_options' => array(), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractConfigurationSortsResolvedOptions() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $options = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $form = $this->createBuilder('name', $options) + ->setType($type) + ->getForm(); + + $this->assertSame(array( + 'id' => 'name', + 'name' => 'name', + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array(), + 'resolved_options' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractConfigurationBuildsIdRecursively() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $grandParent = $this->createBuilder('grandParent') + ->setCompound(true) + ->setDataMapper($this->getMock('Symfony\Component\Form\DataMapperInterface')) + ->getForm(); + $parent = $this->createBuilder('parent') + ->setCompound(true) + ->setDataMapper($this->getMock('Symfony\Component\Form\DataMapperInterface')) + ->getForm(); + $form = $this->createBuilder('name') + ->setType($type) + ->getForm(); + + $grandParent->add($parent); + $parent->add($form); + + $this->assertSame(array( + 'id' => 'grandParent_parent_name', + 'name' => 'name', + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array(), + 'resolved_options' => array(), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractDefaultData() + { + $form = $this->createBuilder('name')->getForm(); + + $form->setData('Foobar'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Foobar'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractDefaultDataStoresModelDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + ))) + ->getForm(); + + $form->setData('Foo'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Bar'", + 'model' => "'Foo'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractDefaultDataStoresViewDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addViewTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + ))) + ->getForm(); + + $form->setData('Foo'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Foo'", + 'view' => "'Bar'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractSubmittedData() + { + $form = $this->createBuilder('name')->getForm(); + + $form->submit('Foobar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresModelDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + '' => '', + ))) + ->getForm(); + + $form->submit('Bar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Bar'", + 'model' => "'Foo'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresViewDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addViewTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + '' => '', + ))) + ->getForm(); + + $form->submit('Bar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foo'", + 'view' => "'Bar'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresErrors() + { + $form = $this->createBuilder('name')->getForm(); + + $form->submit('Foobar'); + $form->addError(new FormError('Invalid!')); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array( + array('message' => 'Invalid!', 'origin' => spl_object_hash($form), 'trace' => array()), + ), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresErrorOrigin() + { + $form = $this->createBuilder('name')->getForm(); + + $error = new FormError('Invalid!'); + $error->setOrigin($form); + + $form->submit('Foobar'); + $form->addError($error); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array( + array('message' => 'Invalid!', 'origin' => spl_object_hash($form), 'trace' => array()), + ), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresErrorCause() + { + $form = $this->createBuilder('name')->getForm(); + + $exception = new \Exception(); + + $form->submit('Foobar'); + $form->addError(new FormError('Invalid!', null, array(), null, $exception)); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array( + array('message' => 'Invalid!', 'origin' => spl_object_hash($form), 'trace' => array( + array( + 'class' => "'Exception'", + 'message' => "''", + ), + )), + ), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataRemembersIfNonSynchronized() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new CallbackTransformer( + function () {}, + function () { + throw new TransformationFailedException('Fail!'); + } + )) + ->getForm(); + + $form->submit('Foobar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + 'model' => 'NULL', + ), + 'errors' => array(), + 'synchronized' => 'false', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractViewVariables() + { + $view = new FormView(); + + $view->vars = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + 'id' => 'foo_bar', + 'name' => 'bar', + ); + + $this->assertSame(array( + 'id' => 'foo_bar', + 'name' => 'bar', + 'view_vars' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + 'id' => "'foo_bar'", + 'name' => "'bar'", + ), + ), $this->dataExtractor->extractViewVariables($view)); + } + + /** + * @param string $name + * @param array $options + * + * @return FormBuilder + */ + private function createBuilder($name, array $options = array()) + { + return new FormBuilder($name, null, $this->dispatcher, $this->factory, $options); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Profiler/Type/DataCollectorTypeExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Profiler/Type/DataCollectorTypeExtensionTest.php new file mode 100644 index 0000000000000..91cee0d3629b7 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Profiler/Type/DataCollectorTypeExtensionTest.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\Form\Tests\Extension\Profiler\Type; + +use Symfony\Component\Form\Extension\Profiler\Type\DataCollectorTypeExtension; + +class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DataCollectorTypeExtension + */ + private $extension; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataCollector; + + protected function setUp() + { + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\Profiler\FormDataCollectorInterface'); + $this->extension = new DataCollectorTypeExtension($this->dataCollector); + } + + public function testGetExtendedType() + { + $this->assertEquals('form', $this->extension->getExtendedType()); + } + + public function testBuildForm() + { + $builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); + $builder->expects($this->atLeastOnce()) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Symfony\Component\Form\Extension\Profiler\FormDataCollectorInterface')); + + $this->extension->buildForm($builder, array()); + } +} diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 8c882b4f02922..a30a204fbbfc6 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -30,7 +30,8 @@ "symfony/http-foundation": "~2.2|~3.0.0", "symfony/http-kernel": "~2.4|~3.0.0", "symfony/security-csrf": "~2.4|~3.0.0", - "symfony/translation": "~2.0,>=2.0.5|~3.0.0" + "symfony/translation": "~2.0,>=2.0.5|~3.0.0", + "symfony/profiler": "~2.8|~3.0.0" }, "conflict": { "symfony/doctrine-bridge": "<2.7", @@ -41,7 +42,8 @@ "symfony/validator": "For form validation.", "symfony/security-csrf": "For protecting forms against CSRF attacks.", "symfony/twig-bridge": "For templating with Twig.", - "symfony/framework-bundle": "For templating with PHP." + "symfony/framework-bundle": "For templating with PHP.", + "symfony/profiler": "For Profile DataCollection." }, "autoload": { "psr-4": { "Symfony\\Component\\Form\\": "" }, diff --git a/src/Symfony/Component/HttpKernel/DataCollector/AjaxDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/AjaxDataCollector.php deleted file mode 100644 index b8405d5945af0..0000000000000 --- a/src/Symfony/Component/HttpKernel/DataCollector/AjaxDataCollector.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\DataCollector; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -/** - * AjaxDataCollector. - * - * @author Bart van den Burg - */ -class AjaxDataCollector extends DataCollector -{ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - // all collecting is done client side - } - - public function getName() - { - return 'ajax'; - } -} diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php index 853adcb300a2c..9221079fad13d 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php @@ -15,11 +15,14 @@ use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; /** * ConfigDataCollector. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\Profiler\DataCollector\ConfigDataCollector instead. */ class ConfigDataCollector extends DataCollector { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php index 5dca65298d059..77743ec7c4022 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php @@ -20,6 +20,8 @@ * * @author Fabien Potencier * @author Bernhard Schussek + * + * @deprecated since 2.8, to be removed in 3.0. */ abstract class DataCollector implements DataCollectorInterface, \Serializable { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php b/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php index 2820ad5b289b5..365a2906e0cf0 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php @@ -13,13 +13,16 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface as BaseDataCollectorInterface; /** * DataCollectorInterface. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\Profiler\DataCollector\DataCollectorInterface instead. */ -interface DataCollectorInterface +interface DataCollectorInterface extends BaseDataCollectorInterface { /** * Collects data for the given Request and Response. diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php index c50bf7a135b39..0b29bbf46ae3e 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php @@ -23,6 +23,8 @@ /** * @author Nicolas Grekas + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\VarDumper\Profiler\DumpDataCollector instead. */ class DumpDataCollector extends DataCollector implements DataDumperInterface { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php index 0a87bc38926de..1eced4dd7206f 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php @@ -20,6 +20,8 @@ * EventDataCollector. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\EventDispatcher\Profiler\EventDataCollector instead. */ class EventDataCollector extends DataCollector implements LateDataCollectorInterface { @@ -104,4 +106,4 @@ public function getName() { return 'events'; } -} +} \ No newline at end of file diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php index 9fe826446b195..b0ab616cfd21c 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php @@ -19,6 +19,8 @@ * ExceptionDataCollector. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\Debug\Profiler\ExceptionDataCollector instead. */ class ExceptionDataCollector extends DataCollector { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LateDataCollectorInterface.php b/src/Symfony/Component/HttpKernel/DataCollector/LateDataCollectorInterface.php index 012332de479f7..f9c2a0db415e6 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LateDataCollectorInterface.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LateDataCollectorInterface.php @@ -11,12 +11,16 @@ namespace Symfony\Component\HttpKernel\DataCollector; +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface as BaseLateDataCollectorInterface; + /** * LateDataCollectorInterface. * * @author Fabien Potencier + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface instead. */ -interface LateDataCollectorInterface +interface LateDataCollectorInterface extends BaseLateDataCollectorInterface { /** * Collects data as late as possible. diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index 12584345a248f..0e61ad041de23 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DataCollector; +@trigger_error('The '.__NAMESPACE__.'\LoggerDataCollector class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Bridge\Monolog\Profiler\LoggerDataCollector instead.', E_USER_DEPRECATED); + use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; @@ -19,6 +21,9 @@ * LogDataCollector. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Bridge\Monolog\Profiler\LoggerDataCollector} instead. */ class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php index 93850108444a0..b912b4492013f 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DataCollector; +@trigger_error('The '.__NAMESPACE__.'\MemoryDataCollector class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\Profiler\DataCollector\MemoryDataCollector instead.', E_USER_DEPRECATED); + use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -18,6 +20,9 @@ * MemoryDataCollector. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\Profiler\DataCollector\MemoryDataCollector} instead. */ class MemoryDataCollector extends DataCollector implements LateDataCollectorInterface { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php index 9a499a737ad02..045b2326a32fd 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DataCollector; +@trigger_error('The '.__NAMESPACE__.'\RequestDataCollector class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\HttpKernel\Profiler\RequestDataCollector instead.', E_USER_DEPRECATED); + use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\Request; @@ -24,6 +26,9 @@ * RequestDataCollector. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\HttpKernel\Profiler\RequestDataCollector} instead. */ class RequestDataCollector extends DataCollector implements EventSubscriberInterface { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/RouterDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/RouterDataCollector.php index 76d962346175e..651b7ffdb0923 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/RouterDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/RouterDataCollector.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DataCollector; +@trigger_error('The '.__NAMESPACE__.'\RouterDataCollector class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\HttpKernel\Profiler\RouterDataCollector instead.', E_USER_DEPRECATED); + use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -20,6 +22,9 @@ * RouterDataCollector. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\HttpKernel\Profiler\RouterDataCollector} instead. */ class RouterDataCollector extends DataCollector { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php index 4ccaafa311ee1..82a7f7d773c04 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DataCollector; +@trigger_error('The '.__NAMESPACE__.'\TimeDataCollector class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\HttpKernel\Profiler\TimeDataCollector instead.', E_USER_DEPRECATED); + use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\KernelInterface; @@ -19,6 +21,9 @@ * TimeDataCollector. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\HttpKernel\Profiler\TimeDataCollector} instead. */ class TimeDataCollector extends DataCollector implements LateDataCollectorInterface { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php index d2f0898605968..a9d907e2158a5 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php @@ -11,10 +11,14 @@ namespace Symfony\Component\HttpKernel\DataCollector\Util; +use Symfony\Component\Profiler\ProfileData\Util\ValueExporter as BaseValueExporter; + /** * @author Bernhard Schussek + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\Profiler\ProfileData\Util\ValueExporter instead. */ -class ValueExporter +class ValueExporter extends BaseValueExporter { /** * Converts a PHP value to a string. diff --git a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php index f73f325241471..6bf6b985d6ab0 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php @@ -11,33 +11,39 @@ namespace Symfony\Component\HttpKernel\EventListener; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\PostResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\HttpKernel\Profiler\Profiler; -use Symfony\Component\HttpFoundation\RequestMatcherInterface; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Profiler\ProfileData\GenericProfileData; +use Symfony\Component\Profiler\Profiler; /** * ProfilerListener collects data for the current request by listening to the kernel events. * * @author Fabien Potencier + * @author Jelte Steijaert + * */ class ProfilerListener implements EventSubscriberInterface { + /** @var Profiler */ protected $profiler; + protected $requestStack; protected $matcher; protected $onlyException; protected $onlyMasterRequests; + /** @var \Exception */ protected $exception; - protected $requests = array(); protected $profiles; - protected $requestStack; protected $parents; - + protected $requests = array(); + /** * Constructor. * @@ -47,7 +53,7 @@ class ProfilerListener implements EventSubscriberInterface * @param bool $onlyException true if the profiler only collects data when an exception occurs, false otherwise * @param bool $onlyMasterRequests true if the profiler only collects data when the request is a master request, false otherwise */ - public function __construct(Profiler $profiler, $requestStack = null, $matcher = null, $onlyException = false, $onlyMasterRequests = false) + public function __construct($profiler, $requestStack = null, $matcher = null, $onlyException = false, $onlyMasterRequests = false) { if ($requestStack instanceof RequestMatcherInterface || (null !== $matcher && !$matcher instanceof RequestMatcherInterface) || $onlyMasterRequests instanceof RequestStack) { $tmp = $onlyMasterRequests; @@ -67,38 +73,40 @@ public function __construct(Profiler $profiler, $requestStack = null, $matcher = if (null !== $matcher && !$matcher instanceof RequestMatcherInterface) { throw new \InvalidArgumentException('Matcher must implement RequestMatcherInterface.'); } + if ( null === $requestStack ) { + $requestStack = new RequestStack(); + } $this->profiler = $profiler; + $this->requestStack = $requestStack; $this->matcher = $matcher; $this->onlyException = (bool) $onlyException; $this->onlyMasterRequests = (bool) $onlyMasterRequests; $this->profiles = new \SplObjectStorage(); $this->parents = new \SplObjectStorage(); - $this->requestStack = $requestStack; } /** - * Handles the onKernelException event. - * - * @param GetResponseForExceptionEvent $event A GetResponseForExceptionEvent instance + * @deprecated since version 2.4, to be removed in 3.0. */ - public function onKernelException(GetResponseForExceptionEvent $event) + public function onKernelRequest(GetResponseEvent $event) { - if ($this->onlyMasterRequests && !$event->isMasterRequest()) { - return; + if (null === $this->requestStack) { + $this->requests[] = $event->getRequest(); } - - $this->exception = $event->getException(); } /** - * @deprecated since version 2.4, to be removed in 3.0. + * Remebers the triggered exception on the onKernelException event. + * + * @param GetResponseForExceptionEvent $event A GetResponseForExceptionEvent instance */ - public function onKernelRequest(GetResponseEvent $event) + public function onKernelException(GetResponseForExceptionEvent $event) { - if (null === $this->requestStack) { - $this->requests[] = $event->getRequest(); + if ($this->onlyMasterRequests && !$event->isMasterRequest()) { + return; } + $this->exception = $event->getException(); } /** @@ -125,10 +133,24 @@ public function onKernelResponse(FilterResponseEvent $event) return; } - if (!$profile = $this->profiler->collect($request, $event->getResponse(), $exception)) { - return; + if (method_exists($this->profiler, 'collect')) { + if (!$profile = $this->profiler->collect($request, $event->getResponse(), $exception)) { + return; + } + } else { + if (!$profile = $this->profiler->profile()) { + return; + } + foreach ($this->profiler->getDeprecatedDataCollectors() as $deprecatedDataCollector) { + if ( !($deprecatedDataCollector instanceof LateDataCollectorInterface) ) { + $deprecatedDataCollector->collect($request, $event->getResponse(), $exception); + $profile->add(new GenericProfileData($deprecatedDataCollector)); + } + } } + $event->getResponse()->headers->set('X-Debug-Token', $profile->getToken()); + $this->profiles[$request] = $profile; if (null !== $this->requestStack) { @@ -155,7 +177,19 @@ public function onKernelTerminate(PostResponseEvent $event) // save profiles foreach ($this->profiles as $request) { - $this->profiler->saveProfile($this->profiles[$request]); + foreach ($this->profiler->getDeprecatedDataCollectors() as $deprecatedDataCollector) { + if ($deprecatedDataCollector instanceof LateDataCollectorInterface) { + $deprecatedDataCollector->lateCollect(); + $this->profiles[$request]->add(new GenericProfileData($deprecatedDataCollector)); + } + } + $this->profiler->save($this->profiles[$request], array( + 'url' => $event->getRequest()->getUri(), + 'method' => $event->getRequest()->getMethod(), + 'ip' => $event->getRequest()->getClientIp(), + 'status_code' => $event->getResponse()->getStatusCode(), + 'profile_type' => 'http', + )); } $this->profiles = new \SplObjectStorage(); diff --git a/src/Symfony/Component/HttpKernel/Profiler/AjaxData.php b/src/Symfony/Component/HttpKernel/Profiler/AjaxData.php new file mode 100644 index 0000000000000..65f9414b22bb8 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/AjaxData.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\HttpKernel\Profiler; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +/** + * AjaxData. + * + * @author Jelte Steijaert + */ +class AjaxData implements ProfileDataInterface +{ + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(null); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + } + + public function getName() + { + return 'ajax'; + } +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/AjaxDataCollector.php b/src/Symfony/Component/HttpKernel/Profiler/AjaxDataCollector.php new file mode 100644 index 0000000000000..3d03356bff79f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/AjaxDataCollector.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\HttpKernel\Profiler; + +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; + +/** + * AjaxDataCollector. + * + * @author Jelte Steijaert + */ +class AjaxDataCollector implements DataCollectorInterface +{ + public function getCollectedData() + { + return new AjaxData(); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php index 29da4abf32ccf..8e6fd98682e67 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php +++ b/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php @@ -11,10 +11,15 @@ namespace Symfony\Component\HttpKernel\Profiler; +@trigger_error('The '.__NAMESPACE__.'\FileProfilerStorage class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\Profiler\Storage\FileProfilerStorage instead.', E_USER_DEPRECATED); + /** * Storage for profiler using files. * * @author Alexandre Salomé + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\Profiler\Storage\FileProfilerStorage} instead. */ class FileProfilerStorage implements ProfilerStorageInterface { diff --git a/src/Symfony/Component/HttpKernel/Profiler/KernelData.php b/src/Symfony/Component/HttpKernel/Profiler/KernelData.php new file mode 100644 index 0000000000000..09e60943aa19f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/KernelData.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Profiler\ProfileData\ConfigData; + +/** + * KernelData. + * + * @author Jelte Steijaert + */ +class KernelData extends ConfigData +{ + private $name; + private $version; + private $state; + private $env; + private $debug; + private $bundles = array(); + + public function __construct(KernelInterface $kernel, array $data) + { + parent::__construct($data); + $this->version = Kernel::VERSION; + $this->state = $this->determineSymfonyState(); + $this->name = $kernel->getName(); + $this->env = $kernel->getEnvironment(); + $this->debug = $kernel->isDebug(); + + foreach ($kernel->getBundles() as $name => $bundle) { + $this->bundles[$name] = $bundle->getPath(); + } + } + + /** + * Gets the application name. + * + * @return string The application name + */ + public function getAppName() + { + return $this->name; + } + + /** + * Gets the Symfony version. + * + * @return string The Symfony version + */ + public function getSymfonyVersion() + { + return $this->version; + } + + /** + * Returns the state of the current Symfony release. + * + * @return string One of: unknown, dev, stable, eom, eol + */ + public function getSymfonyState() + { + return $this->state; + } + + /** + * Gets the environment. + * + * @return string The environment + */ + public function getEnv() + { + return $this->env; + } + + /** + * Returns true if the debug is enabled. + * + * @return bool true if debug is enabled, false otherwise + */ + public function isDebug() + { + return $this->debug; + } + + public function getBundles() + { + return $this->bundles; + } + + public function serialize() + { + return serialize(array( + 'data' => $this->data, + 'name' => $this->name, + 'version' => $this->version, + 'env' => $this->env, + 'debug' => $this->debug, + 'state' => $this->state, + 'bundles' => $this->bundles + )); + } + + public function unserialize($data) + { + $values = unserialize($data); + + $this->data = $values['data']; + $this->name = $values['name']; + $this->version = $values['version']; + $this->env = $values['env']; + $this->debug = $values['debug']; + $this->state = $values['state']; + $this->bundles = $values['bundles']; + } + + /** + * Tries to retrieve information about the current Symfony version. + * + * @return string One of: dev, stable, eom, eol + */ + private function determineSymfonyState() + { + $now = new \DateTime(); + $eom = \DateTime::createFromFormat('m/Y', Kernel::END_OF_MAINTENANCE)->modify('last day of this month'); + $eol = \DateTime::createFromFormat('m/Y', Kernel::END_OF_LIFE)->modify('last day of this month'); + + if ($now > $eol) { + $versionState = 'eol'; //@codeCoverageIgnore + } elseif ($now > $eom) { + $versionState = 'eom'; //@codeCoverageIgnore + } elseif ('' !== Kernel::EXTRA_VERSION) { + $versionState = strtolower(Kernel::EXTRA_VERSION); //@codeCoverageIgnore + } else { + $versionState = 'stable'; //@codeCoverageIgnore + } + + return $versionState; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/HttpKernel/Profiler/KernelDataCollector.php b/src/Symfony/Component/HttpKernel/Profiler/KernelDataCollector.php new file mode 100644 index 0000000000000..a10fd22cee410 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/KernelDataCollector.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Profiler\DataCollector\ConfigDataCollector; + +/** + * KernelDataCollector. + * + * @author Jelte Steijaert + */ +class KernelDataCollector extends ConfigDataCollector +{ + /** + * @var KernelInterface + */ + private $kernel; + + /** + * Constructor. + * + * @param KernelInterface $kernel A KernelInterface instance + */ + public function __construct(KernelInterface $kernel) + { + $this->kernel = $kernel; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + return new KernelData($this->kernel, $this->doCollect()); + } +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/PdoProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/PdoProfilerStorage.php index 4294c098fe40f..21a92e9039a4a 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/PdoProfilerStorage.php +++ b/src/Symfony/Component/HttpKernel/Profiler/PdoProfilerStorage.php @@ -209,7 +209,8 @@ protected function createProfileFromData($token, $data, $parent = null) $profile->setMethod($data['method']); $profile->setUrl($data['url']); $profile->setTime($data['time']); - $profile->setCollectors(unserialize(base64_decode($data['data']))); + $collectors = unserialize(base64_decode($data['data'])); + $profile->setCollectors($collectors); if (!$parent && !empty($data['parent'])) { $parent = $this->read($data['parent']); diff --git a/src/Symfony/Component/HttpKernel/Profiler/Profile.php b/src/Symfony/Component/HttpKernel/Profiler/Profile.php index a4e4ba6ad66b8..ea9369ce85789 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/Profile.php +++ b/src/Symfony/Component/HttpKernel/Profiler/Profile.php @@ -11,14 +11,20 @@ namespace Symfony\Component\HttpKernel\Profiler; +@trigger_error('The '.__NAMESPACE__.'\Profile class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\Profiler\Profile instead.', E_USER_DEPRECATED); + use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; +use Symfony\Component\Profiler\Profile as BaseProfile; /** * Profile. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\Profiler\Profile} instead. */ -class Profile +class Profile extends BaseProfile { private $token; @@ -27,86 +33,50 @@ class Profile */ private $collectors = array(); - private $ip; - private $method; - private $url; private $time; + private $statusCode; - /** - * @var Profile - */ - private $parent; + private $ip; - /** - * @var Profile[] - */ - private $children = array(); + private $method; - /** - * Constructor. - * - * @param string $token The token - */ - public function __construct($token) - { - $this->token = $token; - } + private $url; /** * Sets the token. * * @param string $token The token + * + * @deprecated since 2.8, Profile will be immutable in 3.0. */ public function setToken($token) { $this->token = $token; } - /** - * Gets the token. - * - * @return string The token - */ public function getToken() { + if ( null === $this->token ) { + return parent::getToken(); + } return $this->token; } /** - * Sets the parent token. - * - * @param Profile $parent The parent Profile - */ - public function setParent(Profile $parent) - { - $this->parent = $parent; - } - - /** - * Returns the parent profile. + * Sets the IP. * - * @return Profile The parent profile - */ - public function getParent() - { - return $this->parent; - } - - /** - * Returns the parent token. + * @param string $ip * - * @return null|string The parent token + * @deprecated since 2.8, Profile will be immutable in 3.0. */ - public function getParentToken() + public function setIp($ip) { - return $this->parent ? $this->parent->getToken() : null; + $this->ip = $ip; } /** - * Returns the IP. - * - * @return string The IP + * @deprecated since 2.8. */ public function getIp() { @@ -114,66 +84,71 @@ public function getIp() } /** - * Sets the IP. + * Sets the Method. * - * @param string $ip + * @param string $method + * + * @deprecated since 2.8, Profile will be immutable in 3.0. */ - public function setIp($ip) + public function setMethod($method) { - $this->ip = $ip; + $this->method = $method; } /** - * Returns the request method. - * - * @return string The request method + * @deprecated since 2.8. */ public function getMethod() { return $this->method; } - public function setMethod($method) + /** + * Sets the URL. + * + * @param string $url + * + * @deprecated since 2.8, Profile will be immutable in 3.0. + */ + public function setUrl($url) { - $this->method = $method; + $this->url = $url; } /** - * Returns the URL. - * - * @return string The URL + * @deprecated since 2.8. */ public function getUrl() { return $this->url; } - public function setUrl($url) - { - $this->url = $url; - } - /** - * Returns the time. + * Sets the time. * - * @return string The time + * @param int $time + * + * @deprecated since 2.8, Profile will be immutable in 3.0. */ - public function getTime() + public function setTime($time) { - if (null === $this->time) { - return 0; - } - - return $this->time; + $this->time = $time; } - public function setTime($time) + public function getTime() { - $this->time = $time; + if ( null === $this->time ) { + return parent::getTime(); + } + return $this->time; } /** + * Sets the StatusCode. + * * @param int $statusCode + * + * @deprecated since 2.8, Profile will be immutable in 3.0. */ public function setStatusCode($statusCode) { @@ -182,6 +157,8 @@ public function setStatusCode($statusCode) /** * @return int + * + * @deprecated since 2.8, use Profile::getIndex($name). */ public function getStatusCode() { @@ -189,37 +166,36 @@ public function getStatusCode() } /** - * Finds children profilers. + * Sets the Collectors associated with this profile. * - * @return Profile[] An array of Profile + * @param DataCollectorInterface[] $collectors */ - public function getChildren() + public function setCollectors(array $collectors) { - return $this->children; + $this->collectors = array(); + foreach ($collectors as $collector) { + $this->addCollector($collector); + } } /** - * Sets children profiler. + * Gets the Collectors associated with this profile. * - * @param Profile[] $children An array of Profile + * @return DataCollectorInterface[] */ - public function setChildren(array $children) + public function getCollectors() { - $this->children = array(); - foreach ($children as $child) { - $this->addChild($child); - } + return $this->collectors; } /** - * Adds the child token. + * Adds a Collector. * - * @param Profile $child The child Profile + * @param DataCollectorInterface $collector A DataCollectorInterface instance */ - public function addChild(Profile $child) + public function addCollector(DataCollectorInterface $dataCollector) { - $this->children[] = $child; - $child->setParent($this); + $this->collectors[$dataCollector->getName()] = $dataCollector; } /** @@ -240,39 +216,6 @@ public function getCollector($name) return $this->collectors[$name]; } - /** - * Gets the Collectors associated with this profile. - * - * @return DataCollectorInterface[] - */ - public function getCollectors() - { - return $this->collectors; - } - - /** - * Sets the Collectors associated with this profile. - * - * @param DataCollectorInterface[] $collectors - */ - public function setCollectors(array $collectors) - { - $this->collectors = array(); - foreach ($collectors as $collector) { - $this->addCollector($collector); - } - } - - /** - * Adds a Collector. - * - * @param DataCollectorInterface $collector A DataCollectorInterface instance - */ - public function addCollector(DataCollectorInterface $collector) - { - $this->collectors[$collector->getName()] = $collector; - } - /** * Returns true if a Collector for the given name exists. * @@ -285,8 +228,20 @@ public function hasCollector($name) return isset($this->collectors[$name]); } - public function __sleep() + public function getIndexes() { + return array( + 'ip' => $this->ip, + 'url' => $this->url, + 'status_code' => $this->statusCode, + 'method' => $this->method, + ); + } + + public function getIndex($name) { - return array('token', 'parent', 'children', 'collectors', 'ip', 'method', 'url', 'time'); + if ( !isset($this->$name) ) { + return; + } + return $this->$name; } } diff --git a/src/Symfony/Component/HttpKernel/Profiler/Profiler.php b/src/Symfony/Component/HttpKernel/Profiler/Profiler.php index 378bf5dac3e8a..d11cd502718b0 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/Profiler.php +++ b/src/Symfony/Component/HttpKernel/Profiler/Profiler.php @@ -11,67 +11,26 @@ namespace Symfony\Component\HttpKernel\Profiler; +@trigger_error('The '.__NAMESPACE__.'\Profiler class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\Profiler\Profiler instead.', E_USER_DEPRECATED); + use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; -use Psr\Log\LoggerInterface; +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface as BaseLateDataCollectorInterface; +use Symfony\Component\Profiler\ProfileData\GenericProfileData; +use Symfony\Component\Profiler\Profiler as BaseProfiler; /** * Profiler. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\Profiler\Profiler} instead. */ -class Profiler +class Profiler extends BaseProfiler { - /** - * @var ProfilerStorageInterface - */ - private $storage; - - /** - * @var DataCollectorInterface[] - */ - private $collectors = array(); - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var bool - */ - private $enabled = true; - - /** - * Constructor. - * - * @param ProfilerStorageInterface $storage A ProfilerStorageInterface instance - * @param LoggerInterface $logger A LoggerInterface instance - */ - public function __construct(ProfilerStorageInterface $storage, LoggerInterface $logger = null) - { - $this->storage = $storage; - $this->logger = $logger; - } - - /** - * Disables the profiler. - */ - public function disable() - { - $this->enabled = false; - } - - /** - * Enables the profiler. - */ - public function enable() - { - $this->enabled = true; - } - /** * Loads the Profile for the given Response. * @@ -84,10 +43,10 @@ public function loadProfileFromResponse(Response $response) if (!$token = $response->headers->get('X-Debug-Token')) { return false; } - + return $this->loadProfile($token); } - + /** * Loads the Profile for the given token. * @@ -109,99 +68,38 @@ public function loadProfile($token) */ public function saveProfile(Profile $profile) { - // late collect - foreach ($profile->getCollectors() as $collector) { + foreach ($this->collectors as $collector) { if ($collector instanceof LateDataCollectorInterface) { $collector->lateCollect(); } + if ($collector instanceof BaseLateDataCollectorInterface) { + if ( !method_exists($collector, 'getCollectedData') ) { + $profile->add(new GenericProfileData($collector)); + } else { + $profile->add($collector->getCollectedData()); + } + } } - if (!($ret = $this->storage->write($profile)) && null !== $this->logger) { + if (!($ret = $this->storage->write($profile, $profile->getIndexes())) && null !== $this->logger) { $this->logger->warning('Unable to store the profiler information.', array('configured_storage' => get_class($this->storage))); } return $ret; } - /** - * Purges all data from the storage. - */ - public function purge() - { - $this->storage->purge(); - } - - /** - * Exports the current profiler data. - * - * @param Profile $profile A Profile instance - * - * @return string The exported data - * - * @deprecated since Symfony 2.8, to be removed in 3.0. - */ - public function export(Profile $profile) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - - return base64_encode(serialize($profile)); - } - - /** - * Imports data into the profiler storage. - * - * @param string $data A data string as exported by the export() method - * - * @return Profile A Profile instance - * - * @deprecated since Symfony 2.8, to be removed in 3.0. - */ - public function import($data) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - - $profile = unserialize(base64_decode($data)); - - if ($this->storage->read($profile->getToken())) { - return false; - } - - $this->saveProfile($profile); - - return $profile; - } - - /** - * Finds profiler tokens for the given criteria. - * - * @param string $ip The IP - * @param string $url The URL - * @param string $limit The maximum number of tokens to return - * @param string $method The request method - * @param string $start The start date to search from - * @param string $end The end date to search to - * - * @return array An array of tokens - * - * @see http://php.net/manual/en/datetime.formats.php for the supported date/time formats - */ - public function find($ip, $url, $limit, $method, $start, $end) - { - return $this->storage->find($ip, $url, $limit, $method, $this->getTimestamp($start), $this->getTimestamp($end)); - } - /** * Collects data for the given Response. * - * @param Request $request A Request instance - * @param Response $response A Response instance + * @param Request $request A Request instance + * @param Response $response A Response instance * @param \Exception $exception An exception instance if the request threw one * * @return Profile|null A Profile instance or null if the profiler is disabled */ public function collect(Request $request, Response $response, \Exception $exception = null) { - if (false === $this->enabled) { + if (!$this->enabled) { return; } @@ -215,76 +113,84 @@ public function collect(Request $request, Response $response, \Exception $except $response->headers->set('X-Debug-Token', $profile->getToken()); foreach ($this->collectors as $collector) { - $collector->collect($request, $response, $exception); - - // we need to clone for sub-requests - $profile->addCollector(clone $collector); + if ( $collector instanceof DataCollectorInterface ) { + $collector->collect($request, $response, $exception); + } + if (!($collector instanceof \Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface)) { + if ( !method_exists($collector, 'getCollectedData') ) { + $profile->add(new GenericProfileData($collector)); + } else { + $profile->add($collector->getCollectedData()); + } + } } return $profile; } /** - * Gets the Collectors associated with this profiler. + * Finds profiler tokens for the given criteria. * - * @return array An array of collectors - */ - public function all() - { - return $this->collectors; - } - - /** - * Sets the Collectors associated with this profiler. + * @param string $ip The IP + * @param string $url The URL + * @param string $limit The maximum number of tokens to return + * @param string $method The request method + * @param string $start The start date to search from + * @param string $end The end date to search to + * + * @return array An array of tokens * - * @param DataCollectorInterface[] $collectors An array of collectors + * @see http://php.net/manual/en/datetime.formats.php for the supported date/time formats */ - public function set(array $collectors = array()) + public function find($ip, $url, $limit, $method, $start, $end) { - $this->collectors = array(); - foreach ($collectors as $collector) { - $this->add($collector); - } + return $this->storage->find($ip, $url, $limit, $method, $this->getTimestamp($start), $this->getTimestamp($end)); } /** - * Adds a Collector. - * - * @param DataCollectorInterface $collector A DataCollectorInterface instance + * Purges all data from the storage. */ - public function add(DataCollectorInterface $collector) + public function purge() { - $this->collectors[$collector->getName()] = $collector; + $this->storage->purge(); } - /** - * Returns true if a Collector for the given name exists. + * Exports the current profiler data. * - * @param string $name A collector name + * @param Profile $profile A Profile instance * - * @return bool + * @return string The exported data + * + * @deprecated since Symfony 2.8, to be removed in 3.0. */ - public function has($name) + public function export(Profile $profile) { - return isset($this->collectors[$name]); - } + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); + return base64_encode(serialize($profile)); + } /** - * Gets a Collector by name. + * Imports data into the profiler storage. * - * @param string $name A collector name + * @param string $data A data string as exported by the export() method * - * @return DataCollectorInterface A DataCollectorInterface instance + * @return Profile A Profile instance * - * @throws \InvalidArgumentException if the collector does not exist + * @deprecated since Symfony 2.8, to be removed in 3.0. */ - public function get($name) + public function import($data) { - if (!isset($this->collectors[$name])) { - throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name)); + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); + + $profile = unserialize(base64_decode($data)); + + if ($this->storage->read($profile->getToken())) { + return false; } - - return $this->collectors[$name]; + + $this->saveProfile($profile); + + return $profile; } private function getTimestamp($value) @@ -292,13 +198,11 @@ private function getTimestamp($value) if (null === $value || '' == $value) { return; } - try { $value = new \DateTime(is_numeric($value) ? '@'.$value : $value); } catch (\Exception $e) { return; } - return $value->getTimestamp(); } } diff --git a/src/Symfony/Component/HttpKernel/Profiler/ProfilerStorageInterface.php b/src/Symfony/Component/HttpKernel/Profiler/ProfilerStorageInterface.php index ea72af2314f6f..2680a514540cf 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/ProfilerStorageInterface.php +++ b/src/Symfony/Component/HttpKernel/Profiler/ProfilerStorageInterface.php @@ -11,12 +11,19 @@ namespace Symfony\Component\HttpKernel\Profiler; +@trigger_error('The '.__NAMESPACE__.'\ProfilerStorageInterface class is deprecated since Symfony 2.8 and will be removed in 3.0. Use Symfony\Component\Profiler\Storage\ProfilerStorageInterface instead.', E_USER_DEPRECATED); + +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface as BaseProfilerStorageInterface; + /** * ProfilerStorageInterface. * * @author Fabien Potencier + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\Profiler\Storage\ProfilerStorageInterface} instead. */ -interface ProfilerStorageInterface +interface ProfilerStorageInterface extends BaseProfilerStorageInterface { /** * Finds profiler tokens for the given criteria. @@ -29,31 +36,15 @@ interface ProfilerStorageInterface * @param int|null $end The end date to search to * * @return array An array of tokens - */ - public function find($ip, $url, $limit, $method, $start = null, $end = null); - - /** - * Reads data associated with the given token. - * - * The method returns false if the token does not exist in the storage. * - * @param string $token A token - * - * @return Profile The profile associated with token + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Use {@link Symfony\Component\Profiler\Storage\ProfilerStorageInterface::findBy} instead. */ - public function read($token); + public function find($ip, $url, $limit, $method, $start = null, $end = null); /** - * Saves a Profile. - * - * @param Profile $profile A Profile instance - * - * @return bool Write operation successful + * @param Profile $profile + * @return mixed */ public function write(Profile $profile); - - /** - * Purges all data from the database. - */ - public function purge(); } diff --git a/src/Symfony/Component/HttpKernel/Profiler/RequestData.php b/src/Symfony/Component/HttpKernel/Profiler/RequestData.php new file mode 100644 index 0000000000000..68d55436460e5 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/RequestData.php @@ -0,0 +1,323 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpFoundation\HeaderBag; +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\Profiler\ProfileData\AbstractProfileData; + +/** + * RequestData. + * + * @author Jelte Steijaert + */ +class RequestData extends AbstractProfileData +{ + /** + * Constructor. + * + * @param Request $request The Request. + * @param Response $response The Response. + * @param callable|null $controller The Controller if one is associated with the Request. + */ + public function __construct(Request $request, Response $response, $controller = null) + { + $responseHeaders = $response->headers->all(); + $cookies = array(); + foreach ($response->headers->getCookies() as $cookie) { + $cookies[] = $this->getCookieHeader($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); + } + if (count($cookies) > 0) { + $responseHeaders['Set-Cookie'] = $cookies; + } + + // attributes are serialized and as they can be anything, they need to be converted to strings. + $attributes = array(); + foreach ($request->attributes->all() as $key => $value) { + if ('_route' === $key && is_object($value) && method_exists($value, 'getPath')) { + $attributes[$key] = $this->varToString($value->getPath()); + } elseif ('_route_params' === $key) { + // we need to keep route params as an array (see getRouteParams()) + foreach ($value as $k => $v) { + $value[$k] = $this->varToString($v); + } + $attributes[$key] = $value; + } else { + $attributes[$key] = $this->varToString($value); + } + } + + $content = null; + try { + $content = $request->getContent(); + } catch (\LogicException $e) { + // the user already got the request content as a resource + $content = false; + } + + $sessionMetadata = array(); + $sessionAttributes = array(); + $flashes = array(); + if ($request->hasSession()) { + $session = $request->getSession(); + if ($session->isStarted()) { + $sessionMetadata['Created'] = date(DATE_RFC822, $session->getMetadataBag()->getCreated()); + $sessionMetadata['Last used'] = date(DATE_RFC822, $session->getMetadataBag()->getLastUsed()); + $sessionMetadata['Lifetime'] = $session->getMetadataBag()->getLifetime(); + $sessionAttributes = $session->all(); + $flashes = $session->getBag('flashes')->peekAll(); + } + } + + $statusCode = $response->getStatusCode(); + + $data = array( + 'format' => $request->getRequestFormat(), + 'content' => $content, + 'content_type' => $response->headers->get('Content-Type', 'text/html'), + 'status_text' => isset(Response::$statusTexts[$statusCode]) ? Response::$statusTexts[$statusCode] : '', + 'status_code' => $statusCode, + 'request_query' => $request->query->all(), + 'request_request' => $request->request->all(), + 'request_headers' => $request->headers->all(), + 'request_server' => $request->server->all(), + 'request_cookies' => $request->cookies->all(), + 'request_attributes' => $attributes, + 'response_headers' => $responseHeaders, + 'session_metadata' => $sessionMetadata, + 'session_attributes' => $sessionAttributes, + 'flashes' => $flashes, + 'path_info' => $request->getPathInfo(), + 'controller' => 'n/a', + 'locale' => $request->getLocale(), + ); + + if (isset($data['request_headers']['php-auth-pw'])) { + $data['request_headers']['php-auth-pw'] = '******'; + } + + if (isset($data['request_server']['PHP_AUTH_PW'])) { + $data['request_server']['PHP_AUTH_PW'] = '******'; + } + + if (isset($data['request_request']['_password'])) { + $data['request_request']['_password'] = '******'; + } + + if (null !== $controller) { + if (is_array($controller)) { + try { + $r = new \ReflectionMethod($controller[0], $controller[1]); + $data['controller'] = array( + 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], + 'method' => $controller[1], + 'file' => $r->getFilename(), + 'line' => $r->getStartLine(), + ); + } catch (\ReflectionException $re) { + if (is_callable($controller)) { + // using __call or __callStatic + $data['controller'] = array( + 'class' => is_object($controller[0]) ? get_class($controller[0]) : $controller[0], + 'method' => $controller[1], + 'file' => 'n/a', + 'line' => 'n/a', + ); + } + } + } elseif ($controller instanceof \Closure) { + $r = new \ReflectionFunction($controller); + $data['controller'] = array( + 'class' => $r->getName(), + 'method' => null, + 'file' => $r->getFilename(), + 'line' => $r->getStartLine(), + ); + } elseif (is_object($controller)) { + $r = new \ReflectionClass($controller); + $data['controller'] = array( + 'class' => $r->getName(), + 'method' => null, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ); + } else { + $data['controller'] = (string) $controller ?: 'n/a'; + } + } + parent::__construct($data); + } + + public function getPathInfo() + { + return $this->data['path_info']; + } + + public function getRequestRequest() + { + return new ParameterBag($this->data['request_request']); + } + + public function getRequestQuery() + { + return new ParameterBag($this->data['request_query']); + } + + public function getRequestHeaders() + { + return new HeaderBag($this->data['request_headers']); + } + + public function getRequestServer() + { + return new ParameterBag($this->data['request_server']); + } + + public function getRequestCookies() + { + return new ParameterBag($this->data['request_cookies']); + } + + public function getRequestAttributes() + { + return new ParameterBag($this->data['request_attributes']); + } + + public function getResponseHeaders() + { + return new ResponseHeaderBag($this->data['response_headers']); + } + + public function getSessionMetadata() + { + return $this->data['session_metadata']; + } + + public function getSessionAttributes() + { + return $this->data['session_attributes']; + } + + public function getFlashes() + { + return $this->data['flashes']; + } + + public function getContent() + { + return $this->data['content']; + } + + public function getContentType() + { + return $this->data['content_type']; + } + + public function getStatusText() + { + return $this->data['status_text']; + } + + public function getStatusCode() + { + return $this->data['status_code']; + } + + public function getFormat() + { + return $this->data['format']; + } + + public function getLocale() + { + return $this->data['locale']; + } + + /** + * Gets the route name. + * + * The _route request attributes is automatically set by the Router Matcher. + * + * @return string The route + */ + public function getRoute() + { + return isset($this->data['request_attributes']['_route']) ? $this->data['request_attributes']['_route'] : ''; + } + + /** + * Gets the route parameters. + * + * The _route_params request attributes is automatically set by the RouterListener. + * + * @return array The parameters + */ + public function getRouteParams() + { + return isset($this->data['request_attributes']['_route_params']) ? $this->data['request_attributes']['_route_params'] : array(); + } + + /** + * Gets the controller. + * + * @return string The controller as a string + */ + public function getController() + { + return $this->data['controller']; + } + + private function getCookieHeader($name, $value, $expires, $path, $domain, $secure, $httponly) + { + $cookie = sprintf('%s=%s', $name, urlencode($value)); + + if (0 !== $expires) { + if (is_numeric($expires)) { + $expires = (int) $expires; + } elseif ($expires instanceof \DateTime) { + $expires = $expires->getTimestamp(); + } else { + $tmp = strtotime($expires); + if (false === $tmp || -1 == $tmp) { + throw new \InvalidArgumentException(sprintf('The "expires" cookie parameter is not valid (%s).', $expires)); + } + $expires = $tmp; + } + + $cookie .= '; expires='.str_replace('+0000', '', \DateTime::createFromFormat('U', $expires, new \DateTimeZone('GMT'))->format('D, d-M-Y H:i:s T')); + } + + if ($domain) { + $cookie .= '; domain='.$domain; + } + + $cookie .= '; path='.$path; + + if ($secure) { + $cookie .= '; secure'; + } + + if ($httponly) { + $cookie .= '; httponly'; + } + + return $cookie; + } + + public function getName() + { + return 'request'; + } +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/RequestDataCollector.php b/src/Symfony/Component/HttpKernel/Profiler/RequestDataCollector.php new file mode 100644 index 0000000000000..1291ea61a3778 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/RequestDataCollector.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\FilterControllerEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; + +/** + * RequestDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class RequestDataCollector implements DataCollectorInterface, EventSubscriberInterface +{ + private $requestStack; + private $responses; + private $controllers; + + /** + * Constructor. + * + * @param RequestStack $requestStack The RequestStack. + */ + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + $this->controllers = new \SplObjectStorage(); + $this->responses = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + $request = $this->requestStack->getCurrentRequest(); + + if (null === $request) { + return; + } + + if (!isset($this->responses[$request])) { + return; + } + + $controller = null; + if (isset($this->controllers[$request])) { + $controller = $this->controllers[$request]; + unset($this->controllers[$request]); + } + + return new RequestData($request, $this->responses[$request], $controller); + } + + /** + * Remembers the controller associated to each request. + * + * @param FilterControllerEvent $event The filter controller event + */ + public function onKernelController(FilterControllerEvent $event) + { + $this->controllers[$event->getRequest()] = $event->getController(); + } + + /** + * Remembers the response associated to each request. + * + * @param FilterResponseEvent $event The filter response event + */ + public function onKernelResponse(FilterResponseEvent $event) + { + $this->responses[$event->getRequest()] = $event->getResponse(); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + KernelEvents::CONTROLLER => 'onKernelController', + KernelEvents::RESPONSE => 'onKernelResponse', + ); + } +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/RouterData.php b/src/Symfony/Component/HttpKernel/Profiler/RouterData.php new file mode 100644 index 0000000000000..76a7fe1b9b8f5 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/RouterData.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\HttpKernel\Profiler; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +/** + * RouterData. + * + * @author Jelte Steijaert + */ +class RouterData implements ProfileDataInterface +{ + private $redirect = false; + private $url; + private $route; + + /** + * Constructor. + * + * @param Response $response The Response. + * @param string|null $route The Route. + */ + public function __construct(Response $response, $route = null) + { + if ($response instanceof RedirectResponse) { + $this->redirect = true; + $this->url = $response->getTargetUrl(); + } + $this->route = $route; + } + + /** + * @return bool Whether this request will result in a redirect + */ + public function getRedirect() + { + return $this->redirect; + } + + /** + * @return string|null The target URL + */ + public function getTargetUrl() + { + return $this->url; + } + + /** + * @return string|null The target route + */ + public function getTargetRoute() + { + return $this->route; + } + + /** + * (PHP 5 >= 5.1.0)
+ * String representation of object + * @link http://php.net/manual/en/serializable.serialize.php + * @return string the string representation of the object or null + */ + public function serialize() + { + return serialize(array($this->redirect, $this->route, $this->url)); + } + + /** + * (PHP 5 >= 5.1.0)
+ * Constructs the object + * @link http://php.net/manual/en/serializable.unserialize.php + * @param string $serialized

+ * The string representation of the object. + *

+ * @return void + */ + public function unserialize($serialized) + { + list($this->redirect, $this->route, $this->url) = unserialize($serialized); + } + + public function getName() + { + return "router"; + } +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/RouterDataCollector.php b/src/Symfony/Component/HttpKernel/Profiler/RouterDataCollector.php new file mode 100644 index 0000000000000..e1116b1c349e2 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/RouterDataCollector.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpKernel\Event\FilterControllerEvent; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; + +/** + * RouterDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class RouterDataCollector implements EventSubscriberInterface, DataCollectorInterface +{ + private $requestStack; + private $controllers; + private $responses; + + /** + * @param RequestStack $requestStack The RequestStack. + */ + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + $this->controllers = new \SplObjectStorage(); + $this->responses = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + $request = $this->requestStack->getCurrentRequest(); + + if (null === $request) { + return; + } + + if (!isset($this->responses[$request])) { + return; + } + $response = $this->responses[$request]; + + $route = null; + if ($response instanceof RedirectResponse) { + if ($this->controllers->contains($request)) { + $route = $this->guessRoute($request, $this->controllers[$request]); + } + } + + unset($this->controllers[$request]); + + return new RouterData($response, $route); + } + + /** + * {@inheritdoc} + */ + protected function guessRoute(Request $request, $controller) + { + return 'n/a'; + } + + /** + * Remembers the controller associated to each request. + * + * @param FilterControllerEvent $event The filter controller event + */ + public function onKernelController(FilterControllerEvent $event) + { + $this->controllers[$event->getRequest()] = $event->getController(); + } + + /** + * Remembers the response associated to each request. + * + * @param FilterResponseEvent $event The filter response event + */ + public function onKernelResponse(FilterResponseEvent $event) + { + $this->responses[$event->getRequest()] = $event->getResponse(); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + KernelEvents::CONTROLLER => 'onKernelController', + KernelEvents::RESPONSE => 'onKernelResponse', + ); + } +} diff --git a/src/Symfony/Component/HttpKernel/Profiler/TimeDataCollector.php b/src/Symfony/Component/HttpKernel/Profiler/TimeDataCollector.php new file mode 100644 index 0000000000000..495b1d7fb446f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Profiler/TimeDataCollector.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Profiler\DataCollector\TimeDataCollector as BaseTimeDataCollector; + +/** + * TimeDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class TimeDataCollector extends BaseTimeDataCollector +{ + private $requestStack; + private $kernel; + + /** + * Constructor. + * + * @param RequestStack $requestStack + * @param KernelInterface|null $kernel + * @param Stopwatch|null $stopwatch + */ + public function __construct(RequestStack $requestStack, KernelInterface $kernel = null, Stopwatch $stopwatch = null) + { + parent::__construct($stopwatch); + $this->requestStack = $requestStack; + $this->kernel = $kernel; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + $request = $this->requestStack->getCurrentRequest(); + + if (null !== $this->kernel) { + $this->startTime = $this->kernel->getStartTime(); + } elseif (null !== $request) { + $this->startTime = $request->server->get('REQUEST_TIME_FLOAT', $request->server->get('REQUEST_TIME')); + } + + return parent::getCollectedData(); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php index 4a0dc263b7d8a..b2a0d973f650a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php @@ -17,6 +17,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @group legacy + */ class ConfigDataCollectorTest extends \PHPUnit_Framework_TestCase { public function testCollect() diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/DumpDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/DumpDataCollectorTest.php index e9b8433c46ef5..8c196a4a48c00 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/DumpDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/DumpDataCollectorTest.php @@ -18,6 +18,7 @@ /** * @author Nicolas Grekas + * @group legacy */ class DumpDataCollectorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php index 6c71f4c9ebdd5..a4df85122ec2d 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php @@ -16,6 +16,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @group legacy + */ class ExceptionDataCollectorTest extends \PHPUnit_Framework_TestCase { public function testCollect() diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php index dd1608ce7f3a2..c713de7c49740 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php @@ -13,6 +13,9 @@ use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; +/** + * @group legacy + */ class LoggerDataCollectorTest extends \PHPUnit_Framework_TestCase { /** diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php index 340b428816882..14e221a0a3aa9 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php @@ -15,6 +15,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @group legacy + */ class MemoryDataCollectorTest extends \PHPUnit_Framework_TestCase { public function testCollect() diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php index 2eb1c41e8dda8..42fc5a092b517 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php @@ -20,6 +20,9 @@ use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\EventDispatcher\EventDispatcher; +/** + * @group legacy + */ class RequestDataCollectorTest extends \PHPUnit_Framework_TestCase { public function testCollect() diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php index 0bdcccd53d0cb..4fd3e6a12ea5d 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php @@ -17,6 +17,7 @@ /** * @group time-sensitive + * @group legacy */ class TimeDataCollectorTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/Util/ValueExporterTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/Util/ValueExporterTest.php index 2f2bb972da921..81e1a3f1b1604 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/Util/ValueExporterTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/Util/ValueExporterTest.php @@ -13,6 +13,9 @@ use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; +/** + * @group legacy + */ class ValueExporterTest extends \PHPUnit_Framework_TestCase { /** diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php index ce6fe8e5c108b..4babb5343daa9 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php @@ -11,7 +11,9 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\EventListener\ProfilerListener; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -19,6 +21,7 @@ use Symfony\Component\HttpKernel\Event\PostResponseEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\HttpKernel\KernelEvents; class ProfilerListenerTest extends \PHPUnit_Framework_TestCase { @@ -36,19 +39,22 @@ public function testLegacyEventsWithoutRequestStack() $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') ->disableOriginalConstructor() ->getMock(); + $profiler->expects($this->once()) ->method('collect') ->will($this->returnValue($profile)); + $profiler->expects($this->any()) + ->method('getDeprecatedDataCollectors') + ->will($this->returnValue(array())); + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Symfony\Component\HttpFoundation\Response') - ->disableOriginalConstructor() - ->getMock(); + $response = new Response(); $listener = new ProfilerListener($profiler); $listener->onKernelRequest(new GetResponseEvent($kernel, $request, Kernel::MASTER_REQUEST)); @@ -57,9 +63,13 @@ public function testLegacyEventsWithoutRequestStack() } /** + * Test to ensure BC with Symfony\Component\HttpKernel\Profiler\Profiler + * * Test a master and sub request with an exception and `onlyException` profiler option enabled. + * + * @group legacy */ - public function testKernelTerminate() + public function testLegacyKernelTerminate() { $profile = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profile') ->disableOriginalConstructor() @@ -73,6 +83,10 @@ public function testKernelTerminate() ->method('collect') ->will($this->returnValue($profile)); + $profiler->expects($this->any()) + ->method('getDeprecatedDataCollectors') + ->will($this->returnValue(array())); + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); $masterRequest = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') @@ -83,9 +97,7 @@ public function testKernelTerminate() ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Symfony\Component\HttpFoundation\Response') - ->disableOriginalConstructor() - ->getMock(); + $response = new Response(); $requestStack = new RequestStack(); $requestStack->push($masterRequest); @@ -102,4 +114,168 @@ public function testKernelTerminate() $listener->onKernelTerminate(new PostResponseEvent($kernel, $masterRequest, $response)); } + + /** + * Test a master and sub request with an exception and `onlyException` profiler option enabled. + */ + public function testKernelTerminate() + { + $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock(); + + $profile = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profile') + ->disableOriginalConstructor() + ->getMock(); + + $profiler->expects($this->once()) + ->method('collect') + ->will($this->returnValue($profile)); + + $profiler->expects($this->any()) + ->method('getDeprecatedDataCollectors') + ->will($this->returnValue(array())); + + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $masterRequest = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + + $subRequest = new Request(); + $subRequest2 = new Request(); + $response = new Response(); + + $requestStack = new RequestStack(); + $requestStack->push($masterRequest); + $requestStack->push($subRequest); + $requestStack->push($subRequest2); + + $listener = new ProfilerListener($profiler, $requestStack, null, true, false); + + // master request + $listener->onKernelResponse(new FilterResponseEvent($kernel, $masterRequest, Kernel::MASTER_REQUEST, $response)); + + // sub request + $listener->onKernelException(new GetResponseForExceptionEvent($kernel, $subRequest, Kernel::SUB_REQUEST, new HttpException(404))); + $listener->onKernelResponse(new FilterResponseEvent($kernel, $subRequest, Kernel::SUB_REQUEST, $response)); + + $listener->onKernelTerminate(new PostResponseEvent($kernel, $masterRequest, $response)); + } + + /** + * Test a master and sub request with an exception and `onlyException` profiler option enabled. + */ + public function testKernelExceptionOnlyMasterWithSub() + { + $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock(); + + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $subRequest = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + + $requestStack = new RequestStack(); + + $onlyException = true; + $listener = new ProfilerListener($profiler, $requestStack, null, $onlyException, true); + + // sub request + $listener->onKernelException(new GetResponseForExceptionEvent($kernel, $subRequest, Kernel::SUB_REQUEST, new HttpException(404))); + } + + /** + * Test a master and sub request with an exception and `onlyException` profiler option enabled. + */ + public function testKernelResponseOnlyMasterWithSub() + { + $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock(); + + $profiler->expects($this->never()) + ->method('collect'); + + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $subRequest = new Request(); + + $response = new Response(); + + $requestStack = new RequestStack(); + + $listener = new ProfilerListener($profiler, $requestStack, null, false, true); + + // sub request + $listener->onKernelResponse(new FilterResponseEvent($kernel, $subRequest, Kernel::SUB_REQUEST, $response)); + } + + /** + * Test a master and sub request with an exception and `onlyException` profiler option enabled. + */ + public function testKernelResponseNoProfileReturned() + { + $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock(); + + $profiler->expects($this->once()) + ->method('collect') + ->will($this->returnValue(null)); + + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $masterRequest = new Request(); + $response = new Response(); + + $requestStack = new RequestStack(); + $requestStack->push($masterRequest); + + $listener = new ProfilerListener($profiler, $requestStack, null, false, false); + + // master request + $listener->onKernelResponse(new FilterResponseEvent($kernel, $masterRequest, Kernel::MASTER_REQUEST, $response)); + } + + /** + * Test a master and sub request with an exception and `onlyException` profiler option enabled. + */ + public function testKernelResponseWithMatcher() + { + $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock(); + + $profiler->expects($this->never()) + ->method('collect'); + + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + + $matcher = $this->getMock('Symfony\Component\HttpFoundation\RequestMatcherInterface'); + $matcher->expects($this->once()) + ->method('matches') + ->willReturn(false); + + $masterRequest = new Request(); + $response = new Response(); + + $requestStack = new RequestStack(); + $requestStack->push($masterRequest); + + $listener = new ProfilerListener($profiler, $requestStack, $matcher, false, false); + + // master request + $listener->onKernelResponse(new FilterResponseEvent($kernel, $masterRequest, Kernel::MASTER_REQUEST, $response)); + } + + public function testSubscribedEvents() + { + $events = ProfilerListener::getSubscribedEvents(); + $this->assertArrayHasKey(KernelEvents::TERMINATE, $events); + $this->assertArrayHasKey(KernelEvents::RESPONSE, $events); + $this->assertArrayHasKey(KernelEvents::EXCEPTION, $events); + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php index 084ce1120d880..574b93ae10106 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php @@ -14,6 +14,9 @@ use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; use Symfony\Component\HttpKernel\Profiler\Profile; +/** + * @group legacy + */ class FileProfilerStorageTest extends AbstractProfilerStorageTest { private $tmpDir; diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/KernelDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/KernelDataCollectorTest.php new file mode 100644 index 0000000000000..b97866eb31523 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/KernelDataCollectorTest.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Profiler; + +use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Profiler\KernelDataCollector; + +class KernelDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $kernel = new KernelForTest('test', true); + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + $c = new KernelDataCollector($kernel); + $profileData = $c->getCollectedData(); + $this->assertInstanceOf('Symfony\Component\Profiler\ProfileData\ConfigData', $profileData); + + $this->assertNull($profileData->getApplicationName()); + $this->assertNull($profileData->getApplicationVersion()); + $this->assertSame('test', $profileData->getEnv()); + $this->assertTrue($profileData->isDebug()); + $this->assertSame('testkernel', $profileData->getAppName()); + $this->assertSame(PHP_VERSION, $profileData->getPhpVersion()); + $this->assertSame(Kernel::VERSION, $profileData->getSymfonyVersion()); + $this->assertSame($this->currentSymfonyState(), $profileData->getSymfonyState()); + if ('' !== Kernel::EXTRA_VERSION) { + $this->assertSame(strtolower(Kernel::EXTRA_VERSION), $profileData->getSymfonyState()); + } + + $this->assertCount(1, $profileData->getBundles()); + + $unserializedProfileData = unserialize(serialize($profileData)); + + $this->assertCount(1, $unserializedProfileData->getBundles()); + } + + private function currentSymfonyState() + { + $now = new \DateTime(); + $eom = \DateTime::createFromFormat('m/Y', Kernel::END_OF_MAINTENANCE)->modify('last day of this month'); + $eol = \DateTime::createFromFormat('m/Y', Kernel::END_OF_LIFE)->modify('last day of this month'); + + if ($now > $eol) { + $versionState = 'eol'; + } elseif ($now > $eom) { + $versionState = 'eom'; + } elseif ('' !== Kernel::EXTRA_VERSION) { + $versionState = strtolower(Kernel::EXTRA_VERSION); + } else { + $versionState = 'stable'; + } + + return $versionState; + } +} + +class KernelForTest extends Kernel +{ + public function getName() + { + return 'testkernel'; + } + + public function registerBundles() + { + } + + public function getBundles() + { + return array( + new BundleForTest(), + ); + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + } +} + +class BundleForTest extends Bundle +{ +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php index 6e56f8bcf5c33..4e7ff0cc083b9 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php @@ -17,12 +17,15 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +/** + * @group legacy + */ class ProfilerTest extends \PHPUnit_Framework_TestCase { private $tmp; private $storage; - public function testCollect() + public function testLegacyCollect() { $request = new Request(); $request->query->set('foo', 'bar'); @@ -35,7 +38,7 @@ public function testCollect() $this->assertSame(204, $profile->getStatusCode()); $this->assertSame('GET', $profile->getMethod()); - $this->assertEquals(array('foo' => 'bar'), $profiler->get('request')->getRequestQuery()->all()); + $this->assertEquals(array('foo' => 'bar'), $profile->get('request')->getRequestQuery()->all()); } public function testFindWorksWithDates() @@ -68,6 +71,7 @@ protected function setUp() $this->storage = new FileProfilerStorage('file:'.$this->tmp); $this->storage->purge(); + } protected function tearDown() diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataCollectorTest.php new file mode 100644 index 0000000000000..b2da33b380900 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataCollectorTest.php @@ -0,0 +1,266 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Profiler; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\HttpKernel; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Profiler\RequestDataCollector; +use Symfony\Component\HttpKernel\Event\FilterControllerEvent; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\EventDispatcher\EventDispatcher; + +class RequestDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $requestStack = new RequestStack(); + $c = new RequestDataCollector($requestStack); + + $requestStack->push($this->createRequest()); + + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $this->createResponse() + ) + ); + $data = $c->getCollectedData(); + + $attributes = $data->getRequestAttributes(); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag', $data->getRequestHeaders()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $data->getRequestServer()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $data->getRequestCookies()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $attributes); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $data->getRequestRequest()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $data->getRequestQuery()); + $this->assertSame('html', $data->getFormat()); + $this->assertSame('foobar', $data->getRoute()); + $this->assertSame($requestStack->getMasterRequest()->getPathInfo(), $data->getPathInfo()); + $this->assertSame(array('name' => 'foo'), $data->getRouteParams()); + $this->assertSame(array(), $data->getSessionAttributes()); + $this->assertSame(array(), $data->getSessionMetadata()); + $this->assertSame(array(), $data->getFlashes()); + $this->assertSame('en', $data->getLocale()); + $this->assertRegExp('/Resource\(stream#\d+\)/', $attributes->get('resource')); + $this->assertSame('Object(stdClass)', $attributes->get('object')); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag', $data->getResponseHeaders()); + $this->assertSame('OK', $data->getStatusText()); + $this->assertSame(200, $data->getStatusCode()); + $this->assertSame('application/json', $data->getContentType()); + $this->assertSame('', $data->getContent()); + } + + public function testCollectNoResponseForRequest() + { + $requestStack = new RequestStack(); + $c = new RequestDataCollector($requestStack); + + $requestStack->push($this->createRequest()); + + $this->assertNull($c->getCollectedData()); + } + + public function testSubscribedEvents() + { + $events = RequestDataCollector::getSubscribedEvents(); + $this->assertArrayHasKey(KernelEvents::CONTROLLER, $events); + $this->assertArrayHasKey(KernelEvents::RESPONSE, $events); + } + + /** + * Test various types of controller callables. + */ + public function testControllerInspection() + { + // make sure we always match the line number + $r1 = new \ReflectionMethod($this, 'testControllerInspection'); + $r2 = new \ReflectionMethod($this, 'staticControllerMethod'); + $r3 = new \ReflectionClass($this); + // test name, callable, expected + $controllerTests = array( + array( + '"Regular" callable', + array($this, 'testControllerInspection'), + array( + 'class' => 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', + 'method' => 'testControllerInspection', + 'file' => __FILE__, + 'line' => $r1->getStartLine(), + ), + ), + + array( + 'Closure', + function () { + return 'foo'; + }, + array( + 'class' => __NAMESPACE__ . '\{closure}', + 'method' => null, + 'file' => __FILE__, + 'line' => __LINE__ - 7, + ), + ), + + array( + 'Static callback as string', + 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest::staticControllerMethod', + 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest::staticControllerMethod', + ), + + array( + 'Static callable with instance', + array($this, 'staticControllerMethod'), + array( + 'class' => 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', + 'method' => 'staticControllerMethod', + 'file' => __FILE__, + 'line' => $r2->getStartLine(), + ), + ), + + array( + 'Static callable with class name', + array('Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', 'staticControllerMethod'), + array( + 'class' => 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', + 'method' => 'staticControllerMethod', + 'file' => __FILE__, + 'line' => $r2->getStartLine(), + ), + ), + + array( + 'Callable with instance depending on __call()', + array($this, 'magicMethod'), + array( + 'class' => 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', + 'method' => 'magicMethod', + 'file' => 'n/a', + 'line' => 'n/a', + ), + ), + + array( + 'Callable with class name depending on __callStatic()', + array('Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', 'magicMethod'), + array( + 'class' => 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', + 'method' => 'magicMethod', + 'file' => 'n/a', + 'line' => 'n/a', + ), + ), + + array( + 'Invokable controller', + $this, + array( + 'class' => 'Symfony\Component\HttpKernel\Tests\Profiler\RequestDataCollectorTest', + 'method' => null, + 'file' => __FILE__, + 'line' => $r3->getStartLine(), + ), + ), + ); + + $requestStack = new RequestStack(); + $c = new RequestDataCollector($requestStack); + $request = $this->createRequest(); + $requestStack->push($request); + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $this->createResponse() + ) + ); + foreach ($controllerTests as $controllerTest) { + $this->injectController($c, $controllerTest[1], $request); + $data = $c->getCollectedData(); + $this->assertSame($controllerTest[2], $data->getController(), sprintf('Testing: %s', $controllerTest[0])); + } + } + + protected function createRequest() + { + $request = Request::create('http://test.com/foo?bar=baz'); + $request->attributes->set('foo', 'bar'); + $request->attributes->set('_route', 'foobar'); + $request->attributes->set('_route_params', array('name' => 'foo')); + $request->attributes->set('resource', fopen(__FILE__, 'r')); + $request->attributes->set('object', new \stdClass()); + + return $request; + } + + protected function createResponse() + { + $response = new Response(); + $response->setStatusCode(200); + $response->headers->set('Content-Type', 'application/json'); + $response->headers->setCookie(new Cookie('foo', 'bar', 1, '/foo', 'localhost', true, true)); + $response->headers->setCookie(new Cookie('bar', 'foo', new \DateTime('@946684800'))); + $response->headers->setCookie(new Cookie('bazz', 'foo', '2000-12-12')); + + return $response; + } + + /** + * Inject the given controller callable into the data collector. + */ + protected function injectController($collector, $controller, $request) + { + $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); + $httpKernel = new HttpKernel(new EventDispatcher(), $resolver); + $event = new FilterControllerEvent($httpKernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + $collector->onKernelController($event); + } + + /** + * Dummy method used as controller callable. + */ + public static function staticControllerMethod() + { + throw new \LogicException('Unexpected method call'); + } + + /** + * Magic method to allow non existing methods to be called and delegated. + */ + public function __call($method, $args) + { + throw new \LogicException('Unexpected method call'); + } + + /** + * Magic method to allow non existing methods to be called and delegated. + */ + public static function __callStatic($method, $args) + { + throw new \LogicException('Unexpected method call'); + } + + public function __invoke() + { + throw new \LogicException('Unexpected method call'); + } + + protected function getKernel() + { + return $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataTest.php new file mode 100644 index 0000000000000..99c5e11d8ae9e --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/RequestDataTest.php @@ -0,0 +1,143 @@ + 'test'), + array(), + array(), + array(), + array( + 'PHP_AUTH_PW' => 'test', + 'HTTP_php-auth-pw' => 'test', + ) + ); + $requestStack->push($request); + + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $this->createResponse() + ) + ); + $data = $c->getCollectedData(); + $this->assertNotSame('test', $data->getRequestHeaders()->get('php-auth-pw')); + $this->assertNotSame('test', $data->getRequestServer()->get('PHP_AUTH_PW')); + $this->assertNotSame('test', $data->getRequestRequest()->get('_password')); + } + + public function testSessions() + { + $requestStack = new RequestStack(); + $c = new RequestDataCollector($requestStack); + + /** @var \Symfony\Component\HttpFoundation\Session\Session $session */ + $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session') + ->disableOriginalConstructor() + ->getMock(); + + $session->expects($this->once())->method('isStarted')->willReturn(true); + $sessionMetadataBag = new MetadataBag(); + $session->expects($this->any())->method('getMetadataBag')->willReturn($sessionMetadataBag); + $flashBag = new FlashBag(); + $session->expects($this->any())->method('getBag')->with('flashes')->willReturn($flashBag); + $flashBag->add('test', 'Testing'); + + $session->start(); + $request = new Request(); + $request->setSession($session); + $requestStack->push($request); + + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $this->createResponse() + ) + ); + $data = $c->getCollectedData(); + $this->assertCount(1, $data->getFlashes()); + } + + public function testRouteAttribute() + { + $requestStack = new RequestStack(); + $c = new RequestDataCollector($requestStack); + + /** @var \Symfony\Component\Routing\Route $route */ + $route = $this->getMockBuilder('Symfony\Component\Routing\Route') + ->disableOriginalConstructor() + ->getMock(); + $route->expects($this->once())->method('getPath')->willReturn('/test'); + + $request = new Request( + array(), + array(), + array('_route' => $route) + ); + $requestStack->push($request); + + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $this->createResponse() + ) + ); + $data = $c->getCollectedData(); + } + + /** + * @expectsException \LogicException + */ + public function testInvalidContent() + { + $requestStack = new RequestStack(); + $c = new RequestDataCollector($requestStack); + + $request = new Request(array(), array(), array(), array(), array(), array(), false); + $requestStack->push($request); + + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $this->createResponse() + ) + ); + $data = $c->getCollectedData(); + } + + protected function createResponse() + { + $response = new Response(); + $response->setStatusCode(200); + $response->headers->set('Content-Type', 'application/json'); + $response->headers->setCookie(new Cookie('foo', 'bar', 1, '/foo', 'localhost', true, true)); + $response->headers->setCookie(new Cookie('bar', 'foo', new \DateTime('@946684800'))); + $response->headers->setCookie(new Cookie('bazz', 'foo', '2000-12-12')); + + return $response; + } + + protected function getKernel() + { + return $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + } +} + +interface DummyRouteInterface +{ + public function getPath(); +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/RouterDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/RouterDataCollectorTest.php new file mode 100644 index 0000000000000..f15a3477238c1 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/RouterDataCollectorTest.php @@ -0,0 +1,118 @@ +push($this->createRequest()); + + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $this->createResponse() + ) + ); + + $data = $c->getCollectedData(); + + $this->assertInstanceof('Symfony\Component\HttpKernel\Profiler\RouterData', $data); + $this->assertFalse($data->getRedirect()); + } + + public function testCollectRedirectResponse() + { + $requestStack = new RequestStack(); + $c = new RouterDataCollector($requestStack); + + $request = $this->createRequest(); + $requestStack->push($request); + + $c->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, new RedirectResponse('dummy') + ) + ); + + $this->injectController($c, array($this, 'testCollectRedirectResponse'), $request); + + $data = $c->getCollectedData(); + $this->assertInstanceof('Symfony\Component\HttpKernel\Profiler\RouterData', $data); + $this->assertSame('n/a', $data->getTargetRoute()); + $this->assertSame('dummy', $data->getTargetUrl()); + $this->assertTrue($data->getRedirect()); + } + + public function testCollectNoResponseForRequest() + { + $requestStack = new RequestStack(); + $c = new RouterDataCollector($requestStack); + + $requestStack->push($this->createRequest()); + + $this->assertNull($c->getCollectedData()); + } + + public function testSubscribedEvents() + { + $events = RouterDataCollector::getSubscribedEvents(); + $this->assertArrayHasKey(KernelEvents::CONTROLLER, $events); + $this->assertArrayHasKey(KernelEvents::RESPONSE, $events); + } + + protected function createRequest() + { + $request = Request::create('http://test.com/foo?bar=baz'); + $request->attributes->set('foo', 'bar'); + $request->attributes->set('_route', 'foobar'); + $request->attributes->set('_route_params', array('name' => 'foo')); + $request->attributes->set('resource', fopen(__FILE__, 'r')); + $request->attributes->set('object', new \stdClass()); + + return $request; + } + + protected function createResponse() + { + $response = new Response(); + $response->setStatusCode(200); + $response->headers->set('Content-Type', 'application/json'); + $response->headers->setCookie(new Cookie('foo', 'bar', 1, '/foo', 'localhost', true, true)); + $response->headers->setCookie(new Cookie('bar', 'foo', new \DateTime('@946684800'))); + $response->headers->setCookie(new Cookie('bazz', 'foo', '2000-12-12')); + + return $response; + } + + /** + * Inject the given controller callable into the data collector. + */ + protected function injectController($collector, $controller, $request) + { + $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); + $httpKernel = new HttpKernel(new EventDispatcher(), $resolver); + $event = new FilterControllerEvent($httpKernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + $collector->onKernelController($event); + } + + protected function getKernel() + { + return $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/TimeDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/TimeDataCollectorTest.php new file mode 100644 index 0000000000000..8220c6a58d535 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/TimeDataCollectorTest.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Profiler; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Profiler\TimeDataCollector; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Stopwatch\Stopwatch; + +class TimeDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $requestStack = new RequestStack(); + $c = new TimeDataCollector($requestStack); + + $request = new Request(); + $requestStack->push($request); + + $data = $c->getCollectedData(); + $data->setToken('Mock-Test-Token'); + + $this->assertEquals(0, $data->getStartTime()); + $this->assertEquals(0, $data->getInitTime()); + $this->assertEquals(0, $data->getDuration()); + } + + public function testCollectServerRequestTime() + { + $requestStack = new RequestStack(); + $c = new TimeDataCollector($requestStack); + + $request = new Request(); + $request->server->set('REQUEST_TIME', 1); + $requestStack->push($request); + + $data = $c->getCollectedData(); + $this->assertEquals(1000, $data->getStartTime()); + } + + public function testCollectServerRequestTimeFloat() + { + $requestStack = new RequestStack(); + $c = new TimeDataCollector($requestStack); + + $request = new Request(); + $request->server->set('REQUEST_TIME_FLOAT', 2); + $requestStack->push($request); + + $data = $c->getCollectedData(); + $this->assertEquals(2000, $data->getStartTime()); + } + + public function testCollectWithKernel() + { + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + $kernel->expects($this->once())->method('getStartTime')->will($this->returnValue(123456)); + + $requestStack = new RequestStack(); + $c = new TimeDataCollector($requestStack, $kernel); + + $request = new Request(); + $request->server->set('REQUEST_TIME_FLOAT', 2); + $requestStack->push($request); + + $data = $c->getCollectedData(); + $this->assertEquals(123456000, $data->getStartTime()); + } + + public function testCollectWithStopwatch() + { + $requestStack = new RequestStack(); + $stopwatch = new Stopwatch(); + $startTime = microtime(true) - 10; + + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + $kernel->expects($this->once())->method('getStartTime')->will($this->returnValue($startTime)); + + $c = new TimeDataCollector($requestStack, $kernel, $stopwatch); + $token = 'Mock-Test-Token-Stopwatch'; + $stopwatch->openSection(); + $stopwatch->start('Kernel.Request', 'section'); + sleep(1); + $stopwatch->stop('Kernel.Request'); + $stopwatch->stopSection($token); + + $request = new Request(); + $requestStack->push($request); + + $data = $c->getCollectedData(); + $data->setToken($token); + + $this->assertGreaterThan(10, $data->getDuration() / 1000); + $this->assertInternalType('array', $data->getEvents()); + $this->assertGreaterThan(0, $data->getInitTime()); + } +} diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index d0e92d57a666e..8f8a5df7dff99 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -20,6 +20,7 @@ "symfony/event-dispatcher": "~2.6,>=2.6.7|~3.0.0", "symfony/http-foundation": "~2.5,>=2.5.4|~3.0.0", "symfony/debug": "~2.6,>=2.6.2", + "symfony/profiler": "~2.8|~3.0.0", "psr/log": "~1.0" }, "require-dev": { diff --git a/src/Symfony/Component/Profiler/DataCollector/ConfigDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/ConfigDataCollector.php new file mode 100644 index 0000000000000..aa411afa5ac19 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/ConfigDataCollector.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Profiler\ProfileData\ConfigData; + +/** + * ConfigDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class ConfigDataCollector implements DataCollectorInterface +{ + private $name; + private $version; + + /** + * Constructor. + * + * @param string $name The name of the application using the web profiler + * @param string $version The version of the application using the web profiler + */ + public function __construct($name = null, $version = null) + { + $this->name = $name; + $this->version = $version; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + return new ConfigData($this->doCollect()); + } + + /** + * Collect all config information. + * + * @return array + */ + protected function doCollect() + { + return array( + 'app_name' => $this->name, + 'app_version' => $this->version, + 'php_version' => PHP_VERSION, + 'xdebug_enabled' => extension_loaded('xdebug'), + 'eaccel_enabled' => extension_loaded('eaccelerator') && ini_get('eaccelerator.enable'), + 'apc_enabled' => extension_loaded('apc') && ini_get('apc.enabled'), + 'xcache_enabled' => extension_loaded('xcache') && ini_get('xcache.cacher'), + 'wincache_enabled' => extension_loaded('wincache') && ini_get('wincache.ocenabled'), + 'zend_opcache_enabled' => extension_loaded('Zend OPcache') && ini_get('opcache.enable'), + 'sapi_name' => php_sapi_name(), + ); + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/DataCollectorInterface.php b/src/Symfony/Component/Profiler/DataCollector/DataCollectorInterface.php new file mode 100644 index 0000000000000..7337237e12720 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/DataCollectorInterface.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\Profiler\DataCollector; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +/** + * DataCollectorInterface. + * + * @author Jelte Steijaert + */ +interface DataCollectorInterface +{ + /** + * Returns the collected data. + * + * @return ProfileDataInterface + * + * @todo public function getCollectedData(); //introduce in 3.0 + */ +} diff --git a/src/Symfony/Component/Profiler/DataCollector/LateDataCollectorInterface.php b/src/Symfony/Component/Profiler/DataCollector/LateDataCollectorInterface.php new file mode 100644 index 0000000000000..8d0e2039b43e3 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/LateDataCollectorInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +/** + * LateDataCollectorInterface. + * + * @author Fabien Potencier + */ +interface LateDataCollectorInterface extends DataCollectorInterface +{ +} diff --git a/src/Symfony/Component/Profiler/DataCollector/MemoryDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/MemoryDataCollector.php new file mode 100644 index 0000000000000..20e5bb1454e11 --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/MemoryDataCollector.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Profiler\ProfileData\MemoryData; + +/** + * MemoryDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class MemoryDataCollector implements LateDataCollectorInterface +{ + private $memoryLimit; + + /** + * Constructor. + */ + public function __construct() + { + $this->memoryLimit = ini_get('memory_limit'); + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + return new MemoryData(memory_get_peak_usage(true), $this->memoryLimit); + } +} diff --git a/src/Symfony/Component/Profiler/DataCollector/TimeDataCollector.php b/src/Symfony/Component/Profiler/DataCollector/TimeDataCollector.php new file mode 100644 index 0000000000000..7c5f3c137b4bf --- /dev/null +++ b/src/Symfony/Component/Profiler/DataCollector/TimeDataCollector.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\DataCollector; + +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Profiler\ProfileData\TimeData; + +/** + * TimeDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class TimeDataCollector implements LateDataCollectorInterface +{ + /** + * @var null|Stopwatch + */ + private $stopwatch; + + /** + * @var int + */ + protected $startTime; + + /** + * Constructor. + * + * @param Stopwatch|null $stopwatch + */ + public function __construct(Stopwatch $stopwatch = null) + { + $this->stopwatch = $stopwatch; + $this->startTime = 0; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + return new TimeData($this->startTime, $this->stopwatch); + } +} diff --git a/src/Symfony/Component/Profiler/Profile.php b/src/Symfony/Component/Profiler/Profile.php new file mode 100644 index 0000000000000..7fd2d266a605a --- /dev/null +++ b/src/Symfony/Component/Profiler/Profile.php @@ -0,0 +1,244 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; +use Symfony\Component\Profiler\ProfileData\TokenAwareProfileDataInterface; + +/** + * Profile. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class Profile +{ + /** + * @var string + */ + private $token; + + /** + * @var int + */ + private $time; + + /** + * @var ProfileDataInterface[] + */ + private $data = array(); + + /** + * @var Profile + */ + private $parent; + + /** + * @var Profile[] + */ + private $children = array(); + + /** + * @var array + */ + private $indexes; + + /** + * Constructor. + * + * @param string $token The token + * @param int $time The time + * @param array $indexes + */ + public function __construct($token, $time = null, $indexes = array()) + { + $this->token = $token; + $this->time = null === $time ? time() : $time; + $this->indexes = $indexes; + } + + /** + * Returns the token. + * + * @return string The token + */ + public function getToken() + { + return $this->token; + } + + /** + * Sets the parent token. + * + * @param Profile $parent The parent Profile + */ + public function setParent(Profile $parent) + { + $this->parent = $parent; + } + + /** + * Returns the parent profile. + * + * @return Profile The parent profile + */ + public function getParent() + { + return $this->parent; + } + + /** + * Returns the parent token. + * + * @return null|string The parent token + */ + public function getParentToken() + { + return $this->parent ? $this->parent->getToken() : null; + } + + /** + * Returns the time. + * + * @return string The time + */ + public function getTime() + { + return $this->time; + } + + /** + * Returns children profiles. + * + * @return Profile[] An array of Profile + */ + public function getChildren() + { + return $this->children; + } + + /** + * Sets children profiler. + * + * @param Profile[] $children An array of Profile + */ + public function setChildren(array $children) + { + $this->children = array(); + foreach ($children as $child) { + $this->addChild($child); + } + } + + /** + * Adds the child token. + * + * @param Profile $child The child Profile + */ + public function addChild(Profile $child) + { + $this->children[] = $child; + $child->setParent($this); + } + + /** + * Returns the collection of profile data. + * + * @return ProfileDataInterface[] + */ + public function getData() + { + return $this->data; + } + + /** + * Sets the ProfileData associated with this profile. + * + * @param ProfileDataInterface[] $data + */ + public function setData(array $data) + { + $this->data = $data; + } + + /** + * Adds a Collector. + * + * @param ProfileDataInterface $profileData A ProfileDataInterface instance + */ + public function add(ProfileDataInterface $profileData = null) + { + if ( null === $profileData ) { + return; + } + if ( $profileData instanceof TokenAwareProfileDataInterface ) { + $profileData->setToken($this->token); + } + $this->data[$profileData->getName()] = $profileData; + } + + /** + * Returns data for a specific section. + * + * @param $name + * + * @return ProfileDataInterface + */ + public function get($name) + { + if (!isset($this->data[$name])) { + throw new \InvalidArgumentException(sprintf('ProfileData "%s" does not exist.', $name)); + } + + return $this->data[$name]; + } + + /** + * Check of data exists for a specific section. + * + * @param $name + * + * @return bool + */ + public function has($name) + { + return isset($this->data[$name]); + } + + public function __call($name, $arguments) + { + if (substr($name, 0, 3) == 'get') { + $property = ltrim(strtolower(preg_replace('/[A-Z]/', '_$0', strtolower(substr($name, 3, 1)) . substr($name, 4))), '_'); + if ( isset($this->indexes[$property]) ) { + return $this->indexes[$property]; + } + } + } + + public function getIndex($name) + { + if ( !isset($this->indexes[$name]) ) { + return; + } + return $this->indexes[$name]; + } + + /** + * @return array + * + * @api + */ + public function getIndexes() + { + return $this->indexes; + } +} diff --git a/src/Symfony/Component/Profiler/ProfileData/AbstractProfileData.php b/src/Symfony/Component/Profiler/ProfileData/AbstractProfileData.php new file mode 100644 index 0000000000000..c4b27065fb9a9 --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/AbstractProfileData.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\Component\Profiler\ProfileData; + +use Symfony\Component\Profiler\ProfileData\Util\ValueExporter; + +/** + * AbstractProfileData. + * + * @author Jelte Steijaert + */ +abstract class AbstractProfileData implements ProfileDataInterface +{ + /** + * @var array + */ + protected $data; + + /** + * @var ValueExporter + */ + private $valueExporter; + + /** + * Constructor. + * + * @param array $data The Data. + */ + public function __construct(array $data) + { + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize($this->data); + } + + /** + * {@inheritdoc} + */ + public function unserialize($data) + { + $this->data = unserialize($data); + } + + /** + * Converts a PHP variable to a string. + * + * @param mixed $var A PHP variable + * + * @return string The string representation of the variable + */ + protected function varToString($var) + { + if (null === $this->valueExporter) { + $this->valueExporter = new ValueExporter(); + } + + return $this->valueExporter->exportValue($var); + } +} diff --git a/src/Symfony/Component/Profiler/ProfileData/ConfigData.php b/src/Symfony/Component/Profiler/ProfileData/ConfigData.php new file mode 100644 index 0000000000000..64131038163a2 --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/ConfigData.php @@ -0,0 +1,213 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\ProfileData; + +/** + * ConfigData. + * + * @author Jelte Steijaert + */ +class ConfigData extends AbstractProfileData implements TokenAwareProfileDataInterface +{ + /** + * Returns the Application name. + * + * @return string + */ + public function getApplicationName() + { + return $this->data['app_name']; + } + + /** + * Returns the Application version. + * + * @return string + */ + public function getApplicationVersion() + { + return $this->data['app_version']; + } + + /** + * Returns the token. + * + * @return string The token + */ + public function getToken() + { + return $this->data['token']; + } + + /** + * {@inheritdoc} + */ + public function setToken($token) + { + $this->data['token'] = $token; + } + + /** + * Returns the PHP version. + * + * @return string The PHP version + */ + public function getPhpVersion() + { + return $this->data['php_version']; + } + + /** + * Returns true if the XDebug is enabled. + * + * @return bool true if XDebug is enabled, false otherwise + */ + public function hasXDebug() + { + return $this->data['xdebug_enabled']; + } + + /** + * Returns true if EAccelerator is enabled. + * + * @return bool true if EAccelerator is enabled, false otherwise + */ + public function hasEAccelerator() + { + return $this->data['eaccel_enabled']; + } + + /** + * Returns true if APC is enabled. + * + * @return bool true if APC is enabled, false otherwise + */ + public function hasApc() + { + return $this->data['apc_enabled']; + } + + /** + * Returns true if Zend OPcache is enabled. + * + * @return bool true if Zend OPcache is enabled, false otherwise + */ + public function hasZendOpcache() + { + return $this->data['zend_opcache_enabled']; + } + + /** + * Returns true if XCache is enabled. + * + * @return bool true if XCache is enabled, false otherwise + */ + public function hasXCache() + { + return $this->data['xcache_enabled']; + } + + /** + * Returns true if WinCache is enabled. + * + * @return bool true if WinCache is enabled, false otherwise + */ + public function hasWinCache() + { + return $this->data['wincache_enabled']; + } + + /** + * Returns true if any accelerator is enabled. + * + * @return bool true if any accelerator is enabled, false otherwise + */ + public function hasAccelerator() + { + return $this->hasApc() || $this->hasZendOpcache() || $this->hasEAccelerator() || $this->hasXCache() || $this->hasWinCache(); + } + + /** + * Returns the PHP SAPI name. + * + * @return string The environment + */ + public function getSapiName() + { + return $this->data['sapi_name']; + } + + /** + * Returns the Symfony Application name. + * + * @return string The application name + */ + public function getAppName() + { + return 'n/a'; + } + + /** + * Returns the Symfony version. + * + * @return string The Symfony version + */ + public function getSymfonyVersion() + { + return 'n/a'; + } + + /** + * Returns the state of the current Symfony release. + * + * @return string One of: unknown, dev, stable, eom, eol + */ + public function getSymfonyState() + { + return 'n/a'; + } + + /** + * Returns the environment. + * + * @return string The environment + */ + public function getEnv() + { + return 'n/a'; + } + + /** + * Returns true if the debug is enabled. + * + * @return bool true if debug is enabled, false otherwise + */ + public function isDebug() + { + return 'n/a'; + } + + /** + * Return the collection of loaded bundles. + * + * @return array + */ + public function getBundles() + { + return array(); + } + + public function getName() + { + return 'config'; + } +} diff --git a/src/Symfony/Component/Profiler/ProfileData/GenericProfileData.php b/src/Symfony/Component/Profiler/ProfileData/GenericProfileData.php new file mode 100644 index 0000000000000..e21136849a578 --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/GenericProfileData.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\ProfileData; + +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; + +/** + * GenericProfileData + * + * @author Jelte Steijaert + * + * @deprecated Deprecated since Symfony 2.8, to be removed in Symfony 3.0. + * Add the method `getCollectedData` to your DataCollectors, + * see {@link Symfony\Component\Profiler\DataCollector\DataCollectorInterface} for more info. + */ +class GenericProfileData implements ProfileDataInterface +{ + private $dataCollector; + + public function __construct(DataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return serialize($this->dataCollector); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $this->dataCollector = unserialize($serialized); + } + + public function getName() + { + return $this->dataCollector->getName(); + } + + public function __call($name, $arguments) + { + if ( method_exists($this->dataCollector, $name)) { + return call_user_func_array(array($this->dataCollector, $name), $arguments); + } + if ( method_exists($this->dataCollector, 'get'.ucfirst($name))) { + return call_user_func_array(array($this->dataCollector, 'get'.ucfirst($name)), $arguments); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Profiler/ProfileData/MemoryData.php b/src/Symfony/Component/Profiler/ProfileData/MemoryData.php new file mode 100644 index 0000000000000..c00a4304db62f --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/MemoryData.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\ProfileData; + +/** + * MemoryData. + * + * @author Jelte Steijaert + */ +class MemoryData implements ProfileDataInterface +{ + private $memory; + private $memoryLimit; + + /** + * Constructor. + * + * @param int $memory The current used memory. + * @param int $memoryLimit The memory limit. + */ + public function __construct($memory, $memoryLimit) + { + $this->memory = $memory; + $this->memoryLimit = $this->convertToBytes($memoryLimit); + } + + /** + * Returns the memory. + * + * @return int The memory + */ + public function getMemory() + { + return $this->memory; + } + + /** + * Returns the PHP memory limit. + * + * @return int The memory limit + */ + public function getMemoryLimit() + { + return $this->memoryLimit; + } + + private function convertToBytes($memoryLimit) + { + if ('-1' === $memoryLimit) { + return -1; + } + + $memoryLimit = strtolower($memoryLimit); + $max = strtolower(ltrim($memoryLimit, '+')); + if (0 === strpos($max, '0x')) { + $max = intval($max, 16); + } elseif (0 === strpos($max, '0')) { + $max = intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($memoryLimit, -1)) { + case 't': $max *= 1024; + case 'g': $max *= 1024; + case 'm': $max *= 1024; + case 'k': $max *= 1024; + } + + return $max; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array('memory' => $this->memory, 'memoryLimit' => $this->memoryLimit)); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $data = unserialize($serialized); + $this->memory = $data['memory']; + $this->memoryLimit = $data['memoryLimit']; + } + + public function getName() + { + return 'memory'; + } +} diff --git a/src/Symfony/Component/Profiler/ProfileData/ProfileDataInterface.php b/src/Symfony/Component/Profiler/ProfileData/ProfileDataInterface.php new file mode 100644 index 0000000000000..ef9a9f7f07483 --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/ProfileDataInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\ProfileData; + +/** + * ProfileDataInterface. + * + * @author Jelte Steijaert + */ +interface ProfileDataInterface extends \Serializable +{ + public function getName(); +} diff --git a/src/Symfony/Component/Profiler/ProfileData/TimeData.php b/src/Symfony/Component/Profiler/ProfileData/TimeData.php new file mode 100644 index 0000000000000..f2ac56633dabd --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/TimeData.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\ProfileData; + +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * TimeData. + * + * @author Jelte Steijaert + */ +class TimeData implements ProfileDataInterface, TokenAwareProfileDataInterface +{ + private $startTime; + private $events = array(); + private $stopwatch; + + public function __construct($startTime, Stopwatch $stopwatch = null) + { + $this->startTime = $startTime * 1000; + $this->stopwatch = $stopwatch; + } + + /** + * Sets the request events. + * + * @param array $events The request events + */ + protected function setEvents(array $events) + { + foreach ($events as $event) { + $event->ensureStopped(); + } + + $this->events = $events; + } + + /** + * Returns the events. + * + * @return array The request events + */ + public function getEvents() + { + return $this->events; + } + + /** + * Returns the elapsed time. + * + * @return float The elapsed time + */ + public function getDuration() + { + if (!isset($this->events['__section__'])) { + return 0; + } + + $lastEvent = $this->events['__section__']; + + return $lastEvent->getOrigin() + $lastEvent->getDuration() - $this->getStartTime(); + } + + /** + * Returns the initialization time. + * + * This is the time spent until the beginning of the request handling. + * + * @return float The elapsed time + */ + public function getInitTime() + { + if (!isset($this->events['__section__'])) { + return 0; + } + + return $this->events['__section__']->getOrigin() - $this->getStartTime(); + } + + /** + * Returns the start time. + * + * @return int The time + */ + public function getStartTime() + { + return $this->startTime; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array('startTime' => $this->startTime, 'events' => $this->events)); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $data = unserialize($serialized); + $this->startTime = $data['startTime']; + $this->events = $data['events']; + } + + /** + * @inheritDoc + */ + public function setToken($token) + { + if (null !== $this->stopwatch) { + $this->setEvents($this->stopwatch->getSectionEvents($token)); + } + } + + public function getName() + { + return 'time'; + } +} diff --git a/src/Symfony/Component/Profiler/ProfileData/TokenAwareProfileDataInterface.php b/src/Symfony/Component/Profiler/ProfileData/TokenAwareProfileDataInterface.php new file mode 100644 index 0000000000000..5884b10ae9759 --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/TokenAwareProfileDataInterface.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\Component\Profiler\ProfileData; + +/** + * TokenAwareProfileDataInterface. + * + * @author Jelte Steijaert + */ +interface TokenAwareProfileDataInterface +{ + /** + * Set the Token of the active profile. + * + * @param $token + * + * @api + */ + public function setToken($token); +} \ No newline at end of file diff --git a/src/Symfony/Component/Profiler/ProfileData/Util/ValueExporter.php b/src/Symfony/Component/Profiler/ProfileData/Util/ValueExporter.php new file mode 100644 index 0000000000000..0e1dec06832e5 --- /dev/null +++ b/src/Symfony/Component/Profiler/ProfileData/Util/ValueExporter.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\Profiler\ProfileData\Util; + +/** + * @author Bernhard Schussek + */ +class ValueExporter +{ + /** + * Converts a PHP value to a string. + * + * @param mixed $value The PHP value + * @param int $depth only for internal usage + * @param bool $deep only for internal usage + * + * @return string The string representation of the given value + */ + public function exportValue($value, $depth = 1, $deep = false) + { + if (is_object($value)) { + if ($value instanceof \DateTime || $value instanceof \DateTimeInterface) { + return sprintf('Object(%s) - %s', get_class($value), $value->format(\DateTime::ISO8601)); + } + + return sprintf('Object(%s)', get_class($value)); + } + + if (is_array($value)) { + if (empty($value)) { + return '[]'; + } + + $indent = str_repeat(' ', $depth); + + $a = array(); + foreach ($value as $k => $v) { + if (is_array($v)) { + $deep = true; + } + $a[] = sprintf('%s => %s', $k, $this->exportValue($v, $depth + 1, $deep)); + } + + if ($deep) { + return sprintf("[\n%s%s\n%s]", $indent, implode(sprintf(", \n%s", $indent), $a), str_repeat(' ', $depth - 1)); + } + + return sprintf('[%s]', implode(', ', $a)); + } + + if (is_resource($value)) { + return sprintf('Resource(%s#%d)', get_resource_type($value), $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/Profiler/Profiler.php b/src/Symfony/Component/Profiler/Profiler.php new file mode 100644 index 0000000000000..8f319dbdff7a3 --- /dev/null +++ b/src/Symfony/Component/Profiler/Profiler.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler; + +use Symfony\Component\Profiler\ProfileData\GenericProfileData; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface; + +/** + * Profiler. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class Profiler +{ + /** + * @var DataCollectorInterface[] + */ + protected $collectors = array(); + + /** + * @var ProfilerStorageInterface + */ + protected $storage; + + /** + * @var LoggerInterface + */ + protected $logger; + + /** + * @var bool + */ + protected $enabled = true; + + /** + * Constructor. + * + * @param ProfilerStorageInterface $storage A ProfilerStorageInterface instance + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(ProfilerStorageInterface $storage, LoggerInterface $logger = null) + { + $this->storage = $storage; + $this->logger = $logger; + } + + /** + * Disables the profiler. + */ + public function disable() + { + $this->enabled = false; + } + + /** + * Enables the profiler. + */ + public function enable() + { + $this->enabled = true; + } + + /** + * Saves a Profile. + * + * @param Profile $profile A Profile instance + * + * @return bool + */ + public function save(Profile $profile, array $indexes) + { + $dataCollectors = array_filter($this->collectors, function(DataCollectorInterface $dataCollector) { + return ($dataCollector instanceof LateDataCollectorInterface); + }); + + foreach ( $dataCollectors as $collector) { + if (!method_exists($collector, 'getCollectedData')) { + $profile->add(new GenericProfileData($collector)); + } else { + $profile->add($collector->getCollectedData()); + } + } + + if (!($ret = $this->storage->write($profile, $indexes)) && null !== $this->logger) { + $this->logger->warning('Unable to store the profiler information.', array('configured_storage' => get_class($this->storage))); + } + + return $ret; + } + + /** + * Collects data. + * + * @return Profile|void + */ + public function profile() + { + if (!$this->enabled) { + return; + } + + $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + + $dataCollectors = array_filter($this->collectors, function(DataCollectorInterface $dataCollector) { + return !($dataCollector instanceof LateDataCollectorInterface); + }); + + foreach ( $dataCollectors as $collector) { + if (method_exists($collector, 'getCollectedData')) { + $profile->add($collector->getCollectedData()); + } + } + + return $profile; + } + + /** + * Adds a Collector. + * + * @param DataCollectorInterface $collector A DataCollectorInterface instance + */ + public function add(DataCollectorInterface $collector) + { + $this->collectors[] = $collector; + } + + /** + * @return \Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface[] + */ + public function getDeprecatedDataCollectors() + { + return array_map(function($collector) { + return clone $collector; + }, + array_filter($this->collectors, function($collector) { + return $collector instanceof \Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; + }) + ); + } +} diff --git a/src/Symfony/Component/Profiler/README.md b/src/Symfony/Component/Profiler/README.md new file mode 100644 index 0000000000000..4900e13e6f935 --- /dev/null +++ b/src/Symfony/Component/Profiler/README.md @@ -0,0 +1,50 @@ +Profiler Component +================== + +Profiler collects information about each run of your application and store them for later analysis. + +The profiler is mainly used in the development environment to help you debug +your code and enhance performance; use it in the production environment to +explore problems after the fact. + + use Symfony\Component\Profiler\Profiler; + use Symfony\Component\Profiler\DataCollector; + use Symfony\Component\Profiler\Storage\FileProfilerStorage; + + $storage = new FileProfilerStorage('file:/path/to/storage/profiles'); + $profiler = new Profiler($storage); + + // add some data collectors + $profiler->add(new DataCollector\RequestDataCollector($requestStack)); + $profiler->add(new DataCollector\MemoryDataCollector()); + // ... + + // gather runtime information and create a profile + $profile = $profiler->profile(); + + // profiles are uniquely identified by a token + $token = $profile->getToken(); + + // gather additional information and save to the Storage along with a collection of indexes. + $profiler->save($profile, array( + 'url' => $event->getRequest()->getUri(), + 'method' => $event->getRequest()->getMethod(), + 'ip' => $event->getRequest()->getClientIp(), + 'status_code' => $event->getResponse()->getStatusCode(), + 'profile_type' => 'http', + )); + + // in another process, get back a profile + $profile = $storage->read($token); + + // Searching profiles + $profiles = $storage->findBy(array('ip' => '127.0.0.1'), 10); + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Profiler/ + $ composer.phar install + $ phpunit \ No newline at end of file diff --git a/src/Symfony/Component/Profiler/Storage/AbstractProfilerStorage.php b/src/Symfony/Component/Profiler/Storage/AbstractProfilerStorage.php new file mode 100644 index 0000000000000..fb1e0955880a0 --- /dev/null +++ b/src/Symfony/Component/Profiler/Storage/AbstractProfilerStorage.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Storage; + +use Symfony\Component\Profiler\Profile; + +/** + * AbstractProfilerStorage. + * + * @author Jelte Steijaert + */ +abstract class AbstractProfilerStorage implements ProfilerStorageInterface +{ + /** + * {@inheritdoc} + */ + public function write(Profile $profile, array $indexes) + { + $res = $this->dowrite( + $profile->getToken(), + array_replace($indexes, array( + 'token' => $profile->getToken(), + 'parent_token' => $profile->getParentToken(), + 'time' => $profile->getTime(), + 'children' => base64_encode(serialize(array_map(function (Profile $p) { return $p->getToken(); }, $profile->getChildren()))), + 'data' => base64_encode(serialize($profile->getData())), + )), + array_replace($indexes, array('token' => $profile->getToken(), 'parent_token' => $profile->getParentToken(), 'time' => $profile->getTime())) + ); + foreach ($profile->getChildren() as $childProfile) { + $this->write($childProfile, $indexes); + } + + return $res; + } + + /** + * {@inheritdoc} + */ + public function read($token, Profile $parent = null) + { + $data = $this->doRead($token); + + if ( empty($data) ) { + return; + } + + $indexes = array_map(function($value, $key) { + return !in_array($key, array('token', 'parent_token', 'time', 'children', 'data'))?$value:null; + }, $data, array_keys($data)); + $indexes = array_combine(array_keys($data), $indexes); + $indexes = array_filter($indexes, function($value) { return !is_null($value); }); + + $profile = new Profile($token, $data['time'], $indexes); + + $profileData = unserialize(base64_decode($data['data'])); + $profile->setData($profileData); + + foreach (unserialize(base64_decode($data['children'])) as $childProfileToken) { + $childProfile = $this->read($childProfileToken, $profile); + $profile->addChild($childProfile); + } + + if (isset($data['parent_token']) && null !== $data['parent_token'] && null === $parent) { + $profile->setParent($this->read($data['parent_token'])); + } + + return $profile; + } + + /** + * Executes the actual write. + * + * @param $token + * @param array $data + * @param array $indexedData + * + * @return bool Write operation successful + */ + abstract protected function doWrite($token, array $data, array $indexedData); + + /** + * Executes the actual read. + * + * @param $token + * + * @return array + */ + abstract protected function doRead($token); +} diff --git a/src/Symfony/Component/Profiler/Storage/FileProfilerStorage.php b/src/Symfony/Component/Profiler/Storage/FileProfilerStorage.php new file mode 100644 index 0000000000000..5ff50265e4184 --- /dev/null +++ b/src/Symfony/Component/Profiler/Storage/FileProfilerStorage.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Storage; + +/** + * Storage for profiler using files. + * + * @author Alexandre Salomé + * @author Jelte Steijaert + */ +class FileProfilerStorage extends AbstractProfilerStorage +{ + /** + * Folder where profiler data are stored. + * + * @var string + */ + private $folder; + + /** + * Constructs the file storage using a "dsn-like" path. + * + * Example : "file:/path/to/the/storage/folder" + * + * @param string $dsn The DSN + * + * @throws \RuntimeException + */ + public function __construct($dsn) + { + if (0 !== strpos($dsn, 'file:')) { + throw new \RuntimeException(sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn)); + } + $this->folder = substr($dsn, 5); + + if (!is_dir($this->folder)) { + mkdir($this->folder, 0777, true); + } + } + + /** + * {@inheritdoc} + */ + public function findBy(array $criteria, $limit, $start = null, $end = null) + { + $file = $this->getIndexFilename(); + + if (!file_exists($file)) { + return array(); + } + + $file = fopen($file, 'r'); + fseek($file, 0, SEEK_END); + + $result = array(); + while (count($result) < $limit && $line = $this->readLineFromFile($file)) { + $values = json_decode($line, true); + $time = (int) $values['time']; + + if (!empty($start) && $time < $start) { + continue; + } + + if (!empty($end) && $time > $end) { + continue; + } + + if (!$this->validateCriteria($values, $criteria)) { + continue; + } + + $result[$values['token']] = $values; + } + + fclose($file); + + return array_values($result); + } + + private function validateCriteria($values, $criteria) + { + foreach ($criteria as $key => $value) { + if (null !== $value && false === strpos($values[$key], $value)) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function purge() + { + $flags = \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($iterator as $file) { + if (is_file($file)) { + unlink($file); + } else { + rmdir($file); + } + } + } + + /** + * {@inheritdoc} + */ + public function doRead($token) + { + if (!$token || !file_exists($file = $this->getFilename($token))) { + return; + } + + return unserialize(file_get_contents($file)); + } + + /** + * {@inheritdoc} + */ + public function doWrite($token, array $data, array $indexedData) + { + $file = $this->getFilename($token); + + $profileIndexed = is_file($file); + if (!$profileIndexed) { + // Create directory + $dir = dirname($file); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + } + + if (false === file_put_contents($file, serialize($data))) { + return false; + } + + if (!$profileIndexed) { + // Add to index + if (false === $file = fopen($this->getIndexFilename(), 'a')) { + return false; + } + + fputs($file, json_encode($indexedData)."\n"); + fclose($file); + } + + return true; + } + + /** + * Gets filename to store data, associated to the token. + * + * @param string $token + * + * @return string The profile filename + */ + protected function getFilename($token) + { + // Uses 4 last characters, because first are mostly the same. + $folderA = substr($token, -2, 2); + $folderB = substr($token, -4, 2); + + return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token; + } + + /** + * Gets the index filename. + * + * @return string The index filename + */ + protected function getIndexFilename() + { + return $this->folder.'/index.csv'; + } + + /** + * Reads a line in the file, backward. + * + * This function automatically skips the empty lines and do not include the line return in result value. + * + * @param resource $file The file resource, with the pointer placed at the end of the line to read + * + * @return mixed A string representing the line or null if beginning of file is reached + */ + protected function readLineFromFile($file) + { + $line = ''; + $position = ftell($file); + + if (0 === $position) { + return; + } + + while (true) { + $chunkSize = min($position, 1024); + $position -= $chunkSize; + fseek($file, $position); + + if (0 === $chunkSize) { + // bof reached + break; + } + + $buffer = fread($file, $chunkSize); + + if (false === ($upTo = strrpos($buffer, "\n"))) { + $line = $buffer.$line; + continue; + } + + $position += $upTo; + $line = substr($buffer, $upTo + 1).$line; + fseek($file, max(0, $position), SEEK_SET); + + if ('' !== $line) { + break; + } + } + + return '' === $line ? null : $line; + } +} diff --git a/src/Symfony/Component/Profiler/Storage/ProfilerStorageInterface.php b/src/Symfony/Component/Profiler/Storage/ProfilerStorageInterface.php new file mode 100644 index 0000000000000..6e58b576c9a83 --- /dev/null +++ b/src/Symfony/Component/Profiler/Storage/ProfilerStorageInterface.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Storage; + +use Symfony\Component\Profiler\Profile; + +/** + * ProfilerStorageInterface. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +interface ProfilerStorageInterface +{ + /** + * Finds profiler tokens for the given criteria. + * + * @param array $criteria The criteria to find profiles + * @param string $limit The maximum number of tokens to return + * @param int|null $start The start date to search from + * @param int|null $end The end date to search to + * + * @return array An array of tokens + * + * @todo public function findBy(array $criteria, $limit, $start = null, $end = null); //introduce in 3.0 + */ + + /** + * Reads data associated with the given token. + * + * The method returns false if the token does not exist in the storage. + * + * @param string $token A token + * + * @return Profile The profile associated with token + */ + public function read($token); + + /** + * Saves a Profile. + * + * @param Profile $profile A Profile instance. + * @param array $indexes Collection of indexed values. + * + * @return bool Write operation successful + * + * @todo public function write(Profile $profile, array $indexes); // introduce in 3.0 + */ + + /** + * Purges all data from the database. + */ + public function purge(); +} diff --git a/src/Symfony/Component/Profiler/Tests/DataCollector/ConfigDataCollectorTest.php b/src/Symfony/Component/Profiler/Tests/DataCollector/ConfigDataCollectorTest.php new file mode 100644 index 0000000000000..8f6baea828852 --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/DataCollector/ConfigDataCollectorTest.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\Component\Profiler\Tests\DataCollector; + +use Symfony\Component\Profiler\DataCollector\ConfigDataCollector; + +/** + * ConfigDataCollectorTest. + * + * @author Jelte Steijaert + */ +class ConfigDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $c = new ConfigDataCollector('Test Suite', 'test'); + + $profileData = $c->getCollectedData(); + $profileData->setToken('test'); + + $this->assertInstanceOf('Symfony\Component\Profiler\ProfileData\ConfigData', $profileData); + + $this->assertSame('Test Suite', $profileData->getApplicationName()); + $this->assertSame('test', $profileData->getApplicationVersion()); + $this->assertSame('n/a', $profileData->getEnv()); + $this->assertSame('n/a', $profileData->isDebug()); + $this->assertSame('n/a', $profileData->getAppName()); + $this->assertSame(PHP_VERSION, $profileData->getPhpVersion()); + $this->assertSame('n/a', $profileData->getSymfonyVersion()); + $this->assertSame('n/a', $profileData->getSymfonyState()); + + $this->assertSame('test', $profileData->getToken()); + + // if else clause because we don't know it + if (extension_loaded('xdebug')) { + $this->assertTrue($profileData->hasXdebug()); + } else { + $this->assertFalse($profileData->hasXdebug()); + } + + // if else clause because we don't know it + if (((extension_loaded('eaccelerator') && ini_get('eaccelerator.enable')) + || + (extension_loaded('apc') && ini_get('apc.enabled')) + || + (extension_loaded('Zend OPcache') && ini_get('opcache.enable')) + || + (extension_loaded('xcache') && ini_get('xcache.cacher')) + || + (extension_loaded('wincache') && ini_get('wincache.ocenabled')))) { + $this->assertTrue($profileData->hasAccelerator()); + } else { + $this->assertFalse($profileData->hasAccelerator()); + } + + $this->assertEquals(extension_loaded('eaccelerator') && ini_get('eaccelerator.enable'), $profileData->hasEAccelerator()); + $this->assertEquals(extension_loaded('apc') && ini_get('apc.enabled'), $profileData->hasApc()); + $this->assertEquals(extension_loaded('wincache') && ini_get('wincache.ocenabled'), $profileData->hasWinCache()); + $this->assertEquals(extension_loaded('xcache') && ini_get('xcache.cacher'), $profileData->hasXCache()); + $this->assertEquals(extension_loaded('Zend OPcache') && ini_get('opcache.enable'), $profileData->hasZendOpcache()); + $this->assertEquals(php_sapi_name(), $profileData->getSapiName()); + + $this->assertEmpty($profileData->getBundles()); + } +} diff --git a/src/Symfony/Component/Profiler/Tests/DataCollector/MemoryDataCollectorTest.php b/src/Symfony/Component/Profiler/Tests/DataCollector/MemoryDataCollectorTest.php new file mode 100644 index 0000000000000..2a95b6555adda --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/DataCollector/MemoryDataCollectorTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Tests\DataCollector; + +use Symfony\Component\Profiler\DataCollector\MemoryDataCollector; +use Symfony\Component\Profiler\ProfileData\MemoryData; + +/** + * MemoryDataCollectorTest. + * + * @author Jelte Steijaert + */ +class MemoryDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $collector = new MemoryDataCollector(); + + $data = $collector->getCollectedData(); + + $this->assertInstanceOf('Symfony\Component\Profiler\ProfileData\MemoryData', $data); + $this->assertInternalType('integer', $data->getMemory()); + $this->assertInternalType('integer', $data->getMemoryLimit()); + } + + /** @dataProvider getBytesConversionTestData */ + public function testBytesConversion($limit, $bytes) + { + $data = new MemoryData(1, 1); + $method = new \ReflectionMethod($data, 'convertToBytes'); + $method->setAccessible(true); + $this->assertEquals($bytes, $method->invoke($data, $limit)); + } + + public function getBytesConversionTestData() + { + return array( + array('2k', 2048), + array('2 k', 2048), + array('8m', 8 * 1024 * 1024), + array('+2 k', 2048), + array('+2???k', 2048), + array('0x10', 16), + array('0xf', 15), + array('010', 8), + array('+0x10 k', 16 * 1024), + array('1g', 1024 * 1024 * 1024), + array('1G', 1024 * 1024 * 1024), + array('-1', -1), + array('0', 0), + array('2mk', 2048), // the unit must be the last char, so in this case 'k', not 'm' + ); + } +} diff --git a/src/Symfony/Component/Profiler/Tests/DataCollector/TimeDataCollectorTest.php b/src/Symfony/Component/Profiler/Tests/DataCollector/TimeDataCollectorTest.php new file mode 100644 index 0000000000000..16ec653426536 --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/DataCollector/TimeDataCollectorTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Tests\DataCollector; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Profiler\DataCollector\TimeDataCollector; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * Class TimeDataCollectorTest + * + * @author Jelte Steijaert + */ +class TimeDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + public function testCollect() + { + $c = new TimeDataCollector(); + + + $data = $c->getCollectedData(); + $data->setToken('Mock-Test-Token'); + $this->assertEquals(0, $data->getStartTime()); + $this->assertEquals(0, $data->getInitTime()); + $this->assertEquals(0, $data->getDuration()); + } + + public function testCollectWithStopwatch() + { + $requestStack = new RequestStack(); + $stopwatch = new Stopwatch(); + + $c = new TimeDataCollector($stopwatch); + $token = 'Mock-Test-Token-Stopwatch'; + $stopwatch->openSection(); + $stopwatch->start('Kernel.Request', 'section'); + sleep(1); + $stopwatch->stop('Kernel.Request'); + $stopwatch->stopSection($token); + + $request = new Request(); + $requestStack->push($request); + + + $data = $c->getCollectedData(); + $data->setToken($token); + + $this->assertGreaterThan(1, $data->getDuration() / 1000); + $this->assertInternalType('array', $data->getEvents()); + $this->assertGreaterThan(0, $data->getInitTime()); + } +} diff --git a/src/Symfony/Component/Profiler/Tests/ProfileData/Util/ValueExporterTest.php b/src/Symfony/Component/Profiler/Tests/ProfileData/Util/ValueExporterTest.php new file mode 100644 index 0000000000000..397014c022061 --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/ProfileData/Util/ValueExporterTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Tests\ProfileData\Util; + +use Symfony\Component\Profiler\ProfileData\Util\ValueExporter; + +/** + * ValueExporterTest. + * + * @author Jelte Steijaert + */ +class ValueExporterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ValueExporter + */ + private $valueExporter; + + protected function setUp() + { + $this->valueExporter = new ValueExporter(); + } + + public function testNull() + { + $this->assertSame('null', $this->valueExporter->exportValue(null)); + } + + public function testBoolean() + { + $this->assertSame('false', $this->valueExporter->exportValue(false)); + $this->assertSame('true', $this->valueExporter->exportValue(true)); + } + + public function testArray() + { + $this->assertSame('[]', $this->valueExporter->exportValue(array())); + $this->assertSame('[0 => 1, 1 => 2, 2 => 3]', $this->valueExporter->exportValue(array(1, 2, 3))); + $deepArray = "[\n 0 => [\n 0 => 2\n ]\n]"; + $this->assertSame($deepArray, $this->valueExporter->exportValue(array(array(2)))); + } + + public function testDateTime() + { + $dateTime = new \DateTime('2014-06-10 07:35:40', new \DateTimeZone('UTC')); + $this->assertSame('Object(DateTime) - 2014-06-10T07:35:40+0000', $this->valueExporter->exportValue($dateTime)); + } + + public function testDateTimeImmutable() + { + if (!class_exists('DateTimeImmutable', false)) { + $this->markTestSkipped('Test skipped, class DateTimeImmutable does not exist.'); + } + + $dateTime = new \DateTimeImmutable('2014-06-10 07:35:40', new \DateTimeZone('UTC')); + $this->assertSame('Object(DateTimeImmutable) - 2014-06-10T07:35:40+0000', $this->valueExporter->exportValue($dateTime)); + } +} diff --git a/src/Symfony/Component/Profiler/Tests/ProfileTest.php b/src/Symfony/Component/Profiler/Tests/ProfileTest.php new file mode 100644 index 0000000000000..cca861decc5ef --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/ProfileTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Tests; + +use Symfony\Component\Profiler\DataCollector\MemoryDataCollector; +use Symfony\Component\Profiler\Profile; + +/** + * ProfileTest. + * + * @author Jelte Steijaert + */ +class ProfileTest extends \PHPUnit_Framework_TestCase +{ + public function testHoldsProfileData() + { + $profile = new Profile('test-profile'); + + $collector = new MemoryDataCollector(); + $profile->add($collector->getCollectedData()); + $this->assertTrue($profile->has('memory')); + $this->assertInstanceof('Symfony\Component\Profiler\ProfileData\ProfileDataInterface', $profile->get('memory')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage ProfileData "memory" does not exist. + */ + public function testProfileDataCollector() + { + $profile = new Profile('test-profile'); + + $profile->get('memory'); + } + + public function testNestable() + { + $profile = new Profile('test-profile'); + $childProfile = new Profile('test-child'); + + $profile->setChildren(array($childProfile)); + $this->assertEquals($profile, $childProfile->getParent()); + } +} diff --git a/src/Symfony/Component/Profiler/Tests/ProfilerTest.php b/src/Symfony/Component/Profiler/Tests/ProfilerTest.php new file mode 100644 index 0000000000000..1ebea0aae7d6a --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/ProfilerTest.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Tests; + +use Symfony\Component\Profiler\DataCollector\ConfigDataCollector; +use Symfony\Component\Profiler\DataCollector\MemoryDataCollector; +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Profiler\Storage\FileProfilerStorage; +use Symfony\Component\Profiler\Storage\ProfilerStorageInterface; +use Symfony\Component\Profiler\Profiler; + +/** + * ProfilerTest. + * + * @author Jelte Steijaert + */ +class ProfilerTest extends \PHPUnit_Framework_TestCase +{ + private $tmp; + /** @var FileProfileStorage */ + private $storage; + + /** @var Profiler */ + private $profiler; + + public function testCollect() + { + $this->profiler->add(new ConfigDataCollector()); + $this->profiler->add(new MemoryDataCollector()); + + $profile = $this->profiler->profile(); + + $this->assertTrue($profile->has('config')); + + $this->assertFalse($profile->has('memory')); + } + + public function testDisabledProfiler() + { + $this->profiler->add(new ConfigDataCollector()); + $this->profiler->add(new MemoryDataCollector()); + + $this->profiler->disable(); + $this->assertNull($this->profiler->profile()); + + $this->profiler->enable(); + $profile = $this->profiler->profile(); + + $this->assertTrue($profile->has('config')); + } + + public function testSave() + { + $storage = new DummyStorage(); + $profiler = new Profiler($storage); + + $profiler->add(new MemoryDataCollector()); + + $profile = $profiler->profile(); + + $this->assertTrue($profiler->save($profile, array())); + + $this->assertTrue($profile->has('memory')); + } + + protected function setUp() + { + $this->tmp = tempnam(sys_get_temp_dir(), 'sf2_profiler'); + if (file_exists($this->tmp)) { + @unlink($this->tmp); + } + + $this->storage = new FileProfilerStorage('file:'.$this->tmp); + $this->storage->purge(); + + $this->profiler = new Profiler($this->storage); + } + + protected function tearDown() + { + if (null !== $this->storage) { + $this->storage->purge(); + $this->storage = null; + + @unlink($this->tmp); + } + } +} + +class DummyStorage implements ProfilerStorageInterface +{ + protected $profiles = array(); + + public function findBy(array $criteria = array(), $limit = null, $start = null, $end = null) + { + return $this->profiles; + } + + public function read($token) + { + if (!isset($this->profiles[$token])) { + return false; + } + + return $this->profiles[$token]; + } + + public function write(Profile $profile, array $indexes) + { + if (isset($this->profiles[$profile->getToken()])) { + return false; + } + $this->profiles[$profile->getToken()] = $profile; + + return true; + } + + public function purge() + { + $this->profiles = array(); + } +} diff --git a/src/Symfony/Component/Profiler/Tests/Storage/AbstractProfilerStorageTest.php b/src/Symfony/Component/Profiler/Tests/Storage/AbstractProfilerStorageTest.php new file mode 100644 index 0000000000000..71f0cba012585 --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/Storage/AbstractProfilerStorageTest.php @@ -0,0 +1,261 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Tests\Storage; + +use Symfony\Component\Profiler\Profile; + +/** + * AbstractProfilerStorageTest. + * + * @author Jelte Steijaert + */ +abstract class AbstractProfilerStorageTest extends \PHPUnit_Framework_TestCase +{ + public function testStoreHttpProfile() + { + for ($i = 0; $i < 10; ++$i) { + $profile = new Profile('token_'.$i); + $this->getStorage()->write($profile, array( + 'ip' => '127.0.0.1', + 'url' => 'http://foo.bar', + 'method' => 'GET', + 'status_code' => 200 + )); + } + $this->assertCount(10, $this->getStorage()->findBy(array('ip' => '127.0.0.1', 'url' => 'http://foo.bar', 'method' => 'GET'), 20), sprintf('->write() stores data in the storage "%s"', get_class($this->getStorage()))); + } + + public function testStoreConsoleProfile() + { + for ($i = 0; $i < 10; ++$i) { + $profile = new Profile('token_'.$i); + $this->getStorage()->write($profile, array('command' => 'debug:test', 'result_code' => 1)); + } + $this->assertCount(10, $this->getStorage()->findBy(array('command' => 'debug:test'), 10), sprintf('->write() stores data in the storage "%s"', get_class($this->getStorage()))); + } + + public function testChildren() + { + $parentProfile = new Profile('token_parent'); + $childProfile = new Profile('token_child'); + + $parentProfile->addChild($childProfile); + + $this->getStorage()->write($parentProfile, array( + 'ip' => '127.0.0.1', + 'url' => 'http://foo.bar/parent', + 'method' => 'GET', + 'status_code' => 200 + )); + + // Load them from storage + $parentProfile = $this->getStorage()->read('token_parent'); + /** @var Profile $childProfile */ + + $childProfile = $this->getStorage()->read('token_child'); + + // Check if childProfile is loaded + $this->assertNotNull($childProfile); + + // Check child has link to parent + $this->assertNotNull($childProfile->getParentToken()); + $this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken()); + + // Check parent has child + $children = $parentProfile->getChildren(); + $this->assertCount(1, $children); + $this->assertEquals($childProfile->getToken(), $children[0]->getToken()); + } + + public function testStoreSpecialCharsInUrl() + { + // The storage accepts special characters in URLs (Even though URLs are not + // supposed to contain them) + $this->getStorage()->write(new Profile('simple_quote'), array( + 'url' => 'http://foo.bar/\'', + )); + $this->assertTrue(false !== $this->getStorage()->read('simple_quote'), '->write() accepts single quotes in URL'); + + $this->getStorage()->write(new Profile('double_quote'), array( + 'url' => 'http://foo.bar/"', + )); + $this->assertTrue(false !== $this->getStorage()->read('double_quote'), '->write() accepts double quotes in URL'); + + $this->getStorage()->write(new Profile('backslash'), array( + 'url' => 'http://foo.bar/\\', + )); + $this->assertTrue(false !== $this->getStorage()->read('backslash'), '->write() accepts backslash in URL'); + + $this->getStorage()->write(new Profile('comma'), array( + 'url' => 'http://foo.bar/,', + )); + $this->assertTrue(false !== $this->getStorage()->read('comma'), '->write() accepts comma in URL'); + } + + public function testStoreDuplicateToken() + { + $this->assertTrue($this->getStorage()->write(new Profile('token'), array()), '->write() returns true when the token is unique'); + $this->assertTrue($this->getStorage()->write(new Profile('token'), array()), '->write() returns true when the token is unique'); + + $this->assertCount(1, $this->getStorage()->findBy(array(), 1000), '->findBy() does not return the same profile twice'); + } + + public function testRetrieveByIp() + { + $this->assertTrue($this->getStorage()->write(new Profile('token'), array( + 'ip' => '127.0.0.1', + )), '->write() returns true when the token is unique'); + + $this->assertCount(1, $this->getStorage()->findBy(array('ip' => '127.0.0.1'), 10), '->findBy() retrieve a record by IP'); + $this->assertCount(0, $this->getStorage()->findBy(array('ip' => '127.0.%.1'), 10), '->findBy() does not interpret a "%" as a wildcard in the IP'); + $this->assertCount(0, $this->getStorage()->findBy(array('ip' => '127.0._.1'), 10), '->findBy() does not interpret a "_" as a wildcard in the IP'); + } + + public function testRetrieveByUrl() + { + $this->getStorage()->write(new Profile('simple_quote'), array( + 'url' => 'http://foo.bar/\'', + )); + $this->assertCount(1, $this->getStorage()->findBy(array('url' => 'http://foo.bar/\''), 10), '->findBy() accepts single quotes in URLs'); + + $this->getStorage()->write(new Profile('double_quote'), array( + 'url' => 'http://foo.bar/"', + )); + $this->assertCount(1, $this->getStorage()->findBy(array('url' => 'http://foo.bar/"'), 10), '->findBy() accepts double quotes in URLs'); + + $this->getStorage()->write(new Profile('backslash'), array( + 'url' => 'http://foo\\bar/', + )); + $this->assertCount(1, $this->getStorage()->findBy(array('url' => 'http://foo\\bar/'), 10), '->findBy() accepts backslash in URLs'); + + $this->getStorage()->write(new Profile('percent'), array( + 'url' => 'http://foo.bar/%', + )); + $this->assertCount(1, $this->getStorage()->findBy(array('url' => 'http://foo.bar/%'), 10), '->findBy() does not interpret a "%" as a wildcard in the URL'); + + $this->getStorage()->write(new Profile('underscore'), array( + 'url' => 'http://foo.bar/_', + )); + $this->assertCount(1, $this->getStorage()->findBy(array('url' => 'http://foo.bar/_'), 10), '->findBy() does not interpret a "_" as a wildcard in the URL'); + + $this->getStorage()->write(new Profile('semicolon'), array( + 'url' => 'http://foo.bar/;', + )); + $this->assertCount(1, $this->getStorage()->findBy(array('url' => 'http://foo.bar/;'), 10), '->findBy() accepts semicolon in URLs'); + } + + public function testStoreTime() + { + $dt = new \DateTime('now'); + $start = $dt->getTimestamp(); + + for ($i = 0; $i < 3; ++$i) { + $dt->modify('+1 minute'); + $this->getStorage()->write(new Profile('time_'.$i, $dt->getTimestamp()), array()); + } + + $records = $this->getStorage()->findBy(array(), 3, $start, time() + 3 * 60); + $this->assertCount(3, $records, '->findBy() returns all previously added records'); + $this->assertEquals($records[0]['token'], 'time_2', '->findBy() returns records ordered by time in descendant order'); + $this->assertEquals($records[1]['token'], 'time_1', '->findBy() returns records ordered by time in descendant order'); + $this->assertEquals($records[2]['token'], 'time_0', '->findBy() returns records ordered by time in descendant order'); + + $records = $this->getStorage()->findBy(array(), 3, $start, time() + 2 * 60); + $this->assertCount(2, $records, '->findBy() should return only first two of the previously added records'); + } + + public function testRetrieveByEmptyCriteria() + { + for ($i = 0; $i < 5; ++$i) { + $this->getStorage()->write(new Profile('toke_'.$i), array()); + } + $this->assertCount(5, $this->getStorage()->findBy(array(), 10), '->findBy() returns all previously added records'); + $this->getStorage()->purge(); + } + + public function testRetrieveByMethodAndLimit() + { + foreach (array('POST', 'GET') as $method) { + for ($i = 0; $i < 5; ++$i) { + $this->getStorage()->write( new Profile('token_'.$i.$method), array('method' => $method)); + } + } + + $this->assertCount(5, $this->getStorage()->findBy(array('method' => 'POST'), 5)); + + $this->getStorage()->purge(); + } + + public function testPurge() + { + $this->getStorage()->write(new Profile('token1'), array( + 'ip' => '127.0.0.1' + )); + + $this->assertTrue(false !== $this->getStorage()->read('token1')); + $this->assertCount(1, $this->getStorage()->findBy(array('ip' => '127.0.0.1'), 10)); + + $this->getStorage()->write(new Profile('token2'), array( + 'ip' => '127.0.0.1' + )); + $this->assertTrue(false !== $this->getStorage()->read('token2')); + $this->assertCount(2, $this->getStorage()->findBy(array('ip' => '127.0.0.1'), 10)); + + $this->getStorage()->purge(); + + $this->assertEmpty($this->getStorage()->read('token'), '->purge() removes all data stored by profiler'); + $this->assertCount(0, $this->getStorage()->findBy(array('ip' => '127.0.0.1'), 10), '->purge() removes all items from index'); + } + + public function testDuplicates() + { + for ($i = 1; $i <= 5; ++$i) { + $profile = new Profile('token'.$i); + + ///three duplicates + $this->getStorage()->write($profile, array( + 'ip' => '127.0.0.1', + 'url' => 'http://example.net', + )); + $this->getStorage()->write($profile, array( + 'ip' => '127.0.0.1', + 'url' => 'http://example.net', + )); + $this->getStorage()->write($profile, array( + 'ip' => '127.0.0.1', + 'url' => 'http://example.net', + )); + } + $this->assertCount(3, $this->getStorage()->findBy(array('ip' => '127.0.0.1', 'url' => 'http://example.net'), 3), '->findBy() method returns incorrect number of entries'); + } + + public function testStatusCode() + { + $this->assertTrue($this->getStorage()->write(new Profile('token_200'), array( + 'status_code' => 200 + )), '->write() returns true when the token is unique'); + + $this->assertTrue($this->getStorage()->write(new Profile('token_404'), array( + 'status_code' => 404 + )), '->write() returns true when the token is unique'); + + $tokens = $this->getStorage()->findBy(array(), 10); + $this->assertCount(2, $tokens); + $this->assertContains($tokens[0]['status_code'], array(200, 404)); + $this->assertContains($tokens[1]['status_code'], array(200, 404)); + } + + /** + * @return \Symfony\Component\Profiler\Storage\ProfilerStorageInterface + */ + abstract protected function getStorage(); +} diff --git a/src/Symfony/Component/Profiler/Tests/Storage/FileProfilerStorageTest.php b/src/Symfony/Component/Profiler/Tests/Storage/FileProfilerStorageTest.php new file mode 100644 index 0000000000000..0595cd77c9940 --- /dev/null +++ b/src/Symfony/Component/Profiler/Tests/Storage/FileProfilerStorageTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Profiler\Tests\Storage; + +use Symfony\Component\Profiler\Profile; +use Symfony\Component\Profiler\Storage\FileProfilerStorage; + +/** + * FileProfilerStorageTest. + * + * @author Jelte Steijaert + */ +class FileProfilerStorageTest extends AbstractProfilerStorageTest +{ + protected static $tmpDir; + protected static $storage; + + protected static function cleanDir() + { + $flags = \FilesystemIterator::SKIP_DOTS; + $iterator = new \RecursiveDirectoryIterator(self::$tmpDir, $flags); + $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); + + foreach ($iterator as $file) { + if (is_file($file)) { + unlink($file); + } + } + } + + public static function setUpBeforeClass() + { + self::$tmpDir = sys_get_temp_dir().'/sf2_profiler_file_storage'; + if (is_dir(self::$tmpDir)) { + self::cleanDir(); + } + self::$storage = new FileProfilerStorage('file:'.self::$tmpDir); + } + + public static function tearDownAfterClass() + { + self::cleanDir(); + } + + protected function setUp() + { + self::$storage->purge(); + } + + /** + * @return \Symfony\Component\Profiler\Storage\ProfilerStorageInterface + */ + protected function getStorage() + { + return self::$storage; + } + + public function testReadLineFromFile() + { + $r = new \ReflectionMethod(self::$storage, 'readLineFromFile'); + + $r->setAccessible(true); + + $h = tmpfile(); + + fwrite($h, "line1\n\n\nline2\n"); + fseek($h, 0, SEEK_END); + + $this->assertEquals('line2', $r->invoke(self::$storage, $h)); + $this->assertEquals('line1', $r->invoke(self::$storage, $h)); + } +} diff --git a/src/Symfony/Component/Profiler/composer.json b/src/Symfony/Component/Profiler/composer.json new file mode 100644 index 0000000000000..e847976245604 --- /dev/null +++ b/src/Symfony/Component/Profiler/composer.json @@ -0,0 +1,42 @@ +{ + "name": "symfony/profiler", + "type": "library", + "description": "Symfony Profiler Component", + "keywords": [], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.9", + "psr/log": "~1.0" + }, + "require-dev": { + "symfony/dependency-injection": "~2.8", + "symfony/http-foundation": "~2.8", + "symfony/var-dumper": "~2.8", + "symfony/stopwatch": "~2.8", + "symfony/config": "~2.8", + "twig/twig": "~1.18" + }, + "suggest": { + "symfony/stopwatch": "" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Profiler\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + } +} diff --git a/src/Symfony/Component/Profiler/phpunit.xml.dist b/src/Symfony/Component/Profiler/phpunit.xml.dist new file mode 100644 index 0000000000000..01ad669516de0 --- /dev/null +++ b/src/Symfony/Component/Profiler/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + \ No newline at end of file diff --git a/src/Symfony/Component/Security/Core/Profiler/SecurityData.php b/src/Symfony/Component/Security/Core/Profiler/SecurityData.php new file mode 100644 index 0000000000000..b4e54421a1d8c --- /dev/null +++ b/src/Symfony/Component/Security/Core/Profiler/SecurityData.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Profiler; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\Security\Core\Role\RoleInterface; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; + +/** + * SecurityData + * + * @author Jelte Steijaert + */ +class SecurityData implements ProfileDataInterface +{ + private $enabled = false; + private $authenticated = false; + private $logoutUrl = null; + private $tokenClass = null; + private $user = ''; + private $roles = array(); + private $inheritedRoles = array(); + private $supportsRoleHierarchy = false; + + + /** + * Constructor. + * + * @param TokenStorageInterface $tokenStorage + * @param RoleHierarchyInterface $roleHierarchy + */ + public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, + LogoutUrlGenerator $logoutUrlGenerator = null) + { + $this->supportsRoleHierarchy = null !== $roleHierarchy; + if (null !== $tokenStorage) { + $this->enabled = true; + if (null !== $token = $tokenStorage->getToken()) { + $inheritedRoles = array(); + $assignedRoles = $token->getRoles(); + if (null !== $roleHierarchy) { + $allRoles = $roleHierarchy->getReachableRoles($assignedRoles); + foreach ($allRoles as $role) { + if (!in_array($role, $assignedRoles)) { + $inheritedRoles[] = $role; + } + } + } + $this->authenticated = $token->isAuthenticated(); + $this->tokenClass = get_class($token); + $this->user = $token->getUsername(); + $this->roles = array_map(function (RoleInterface $role) { + return $role->getRole(); + }, $assignedRoles); + $this->inheritedRoles = array_map(function (RoleInterface $role) { + return $role->getRole(); + }, $inheritedRoles); + + try { + if (null !== $logoutUrlGenerator) { + $this->logoutUrl = $logoutUrlGenerator->getLogoutPath(); + } + } catch(\Exception $e) { + // fail silently when the logout URL cannot be generated + } + } + } + } + + /** + * Checks if security is enabled. + * + * @return bool true if security is enabled, false otherwise + */ + public function isEnabled() + { + return $this->enabled; + } + + /** + * Gets the user. + * + * @return string The user + */ + public function getUser() + { + return $this->user; + } + + /** + * Gets the roles of the user. + * + * @return array The roles + */ + public function getRoles() + { + return $this->roles; + } + + /** + * Gets the inherited roles of the user. + * + * @return array The inherited roles + */ + public function getInheritedRoles() + { + return $this->inheritedRoles; + } + + /** + * Checks if the data contains information about inherited roles. Still the inherited + * roles can be an empty array. + * + * @return bool true if the profile was contains inherited role information. + */ + public function supportsRoleHierarchy() + { + return $this->supportsRoleHierarchy; + } + + /** + * Checks if the user is authenticated or not. + * + * @return bool true if the user is authenticated, false otherwise + */ + public function isAuthenticated() + { + return $this->authenticated; + } + + /** + * Get the class name of the security token. + * + * @return string The token + */ + public function getTokenClass() + { + return $this->tokenClass; + } + + /** + * Get the logout url. + * + * @return string The Logout url + */ + public function getLogoutUrl() + { + return $this->logoutUrl; + } + + public function getName() + { + return 'security'; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return serialize(array( + 'enabled' => $this->enabled, + 'authenticated' => $this->authenticated, + 'logout_url' => $this->logoutUrl, + 'tokenClass' => $this->tokenClass, + 'user' => $this->user, + 'roles' => $this->roles, + 'supportsRoleHierarchy' => $this->supportsRoleHierarchy, + )); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $unserialized = unserialize($serialized); + $this->enabled = $unserialized['enabled']; + $this->authenticated = $unserialized['authenticated']; + $this->logoutUrl = $unserialized['logout_url']; + $this->tokenClass = $unserialized['tokenClass']; + $this->user = $unserialized['user']; + $this->roles = $unserialized['roles']; + $this->supportsRoleHierarchy = $unserialized['supportsRoleHierarchy']; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Core/Profiler/SecurityDataCollector.php b/src/Symfony/Component/Security/Core/Profiler/SecurityDataCollector.php new file mode 100644 index 0000000000000..65c7d19ca01fc --- /dev/null +++ b/src/Symfony/Component/Security/Core/Profiler/SecurityDataCollector.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Profiler; + +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; + +/** + * SecurityDataCollector. + * + * @author Fabien Potencier + * @author Jelte Steijaert + */ +class SecurityDataCollector implements DataCollectorInterface +{ + private $tokenStorage; + private $roleHierarchy; + private $logoutUrlGenerator; + + /** + * Constructor. + * + * @param TokenStorageInterface|null $tokenStorage + * @param RoleHierarchyInterface|null $roleHierarchy + * @param LogoutUrlGenerator|null $logoutUrlGenerator + */ + public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, + LogoutUrlGenerator $logoutUrlGenerator = null) + { + $this->tokenStorage = $tokenStorage; + $this->roleHierarchy = $roleHierarchy; + $this->logoutUrlGenerator = $logoutUrlGenerator; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + return new SecurityData($this->tokenStorage, $this->roleHierarchy, $this->logoutUrlGenerator); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Profiler/SecurityDataCollectorTest.php b/src/Symfony/Component/Security/Core/Tests/Profiler/SecurityDataCollectorTest.php new file mode 100644 index 0000000000000..3df1f51112205 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Profiler/SecurityDataCollectorTest.php @@ -0,0 +1,114 @@ +getCollectedData(); + + $this->assertFalse($data->isEnabled()); + $this->assertFalse($data->isAuthenticated()); + $this->assertNull($data->getTokenClass()); + $this->assertFalse($data->supportsRoleHierarchy()); + $this->assertCount(0, $data->getRoles()); + $this->assertCount(0, $data->getInheritedRoles()); + $this->assertEmpty($data->getUser()); + } + + public function testCollectWhenAuthenticationTokenIsNull() + { + $tokenStorage = new TokenStorage(); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $data = $collector->getCollectedData(); + + $this->assertTrue($data->isEnabled()); + $this->assertFalse($data->isAuthenticated()); + $this->assertNull($data->getTokenClass()); + $this->assertTrue($data->supportsRoleHierarchy()); + $this->assertCount(0, $data->getRoles()); + $this->assertCount(0, $data->getInheritedRoles()); + $this->assertEmpty($data->getUser()); + } + + /** + * @group legacy + */ + public function testLegacyCollectWhenAuthenticationTokenIsNull() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $data = $collector->getCollectedData(); + + $this->assertTrue($data->isEnabled()); + $this->assertFalse($data->isAuthenticated()); + $this->assertNull($data->getTokenClass()); + $this->assertTrue($data->supportsRoleHierarchy()); + $this->assertCount(0, $data->getRoles()); + $this->assertCount(0, $data->getInheritedRoles()); + $this->assertEmpty($data->getUser()); + } + + /** @dataProvider provideRoles */ + public function testCollectAuthenticationTokenAndRoles(array $roles, array $normalizedRoles, array $inheritedRoles) + { + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles)); + + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $data = $collector->getCollectedData(); + + $this->assertTrue($data->isEnabled()); + $this->assertTrue($data->isAuthenticated()); + $this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $data->getTokenClass()); + $this->assertTrue($data->supportsRoleHierarchy()); + $this->assertSame($normalizedRoles, $data->getRoles()); + $this->assertSame($inheritedRoles, $data->getInheritedRoles()); + $this->assertSame('hhamon', $data->getUser()); + } + + public function provideRoles() + { + return array( + // Basic roles + array( + array('ROLE_USER'), + array('ROLE_USER'), + array(), + ), + array( + array(new Role('ROLE_USER')), + array('ROLE_USER'), + array(), + ), + // Inherited roles + array( + array('ROLE_ADMIN'), + array('ROLE_ADMIN'), + array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + ), + array( + array(new Role('ROLE_ADMIN')), + array('ROLE_ADMIN'), + array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + ), + ); + } + + private function getRoleHierarchy() + { + return new RoleHierarchy(array( + 'ROLE_ADMIN' => array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + )); + } +} diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index 3362971d172b4..520f597c69fda 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -27,6 +27,7 @@ "symfony/expression-language": "~2.6|~3.0.0", "symfony/http-foundation": "~2.4|~3.0.0", "symfony/ldap": "~2.8|~3.0.0", + "symfony/profiler": "~2.8|~3.0.0", "symfony/validator": "~2.5,>=2.5.9|~3.0.0", "psr/log": "~1.0" }, diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index 8764ce6d56447..f37933872469b 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -36,6 +36,7 @@ "require-dev": { "symfony/finder": "~2.3|~3.0.0", "symfony/polyfill-intl-icu": "~1.0", + "symfony/profiler": "~2.8|~3.0.0", "symfony/routing": "~2.2|~3.0.0", "symfony/validator": "~2.5,>=2.5.9|~3.0.0", "psr/log": "~1.0", diff --git a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php index cb59d0a7e70e4..be1ff798119d7 100644 --- a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php +++ b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php @@ -16,9 +16,12 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Translation\DataCollectorTranslator; +use Symfony\Component\Translation\Profiler\TranslationData; /** * @author Abdellatif Ait boudad + * + * @deprecated since 2.8, to be removed in 3.0. Use Symfony\Component\Translation\Profiler\TranslationDataCollector instead. */ class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface { diff --git a/src/Symfony/Component/Translation/Profiler/TranslationData.php b/src/Symfony/Component/Translation/Profiler/TranslationData.php new file mode 100644 index 0000000000000..78de359aa8296 --- /dev/null +++ b/src/Symfony/Component/Translation/Profiler/TranslationData.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Profiler; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; +use Symfony\Component\Translation\DataCollectorTranslator; + +/** + * TranslationData. + * + * @author Jelte Steijaert + */ +class TranslationData implements ProfileDataInterface +{ + private $counters; + private $messages; + + public function __construct(DataCollectorTranslator $translator) + { + $messages = $this->sanitizeCollectedMessages($translator->getCollectedMessages()); + + $this->counters = $this->computeCount($messages); + $this->messages = $messages; + } + + /** + * @return array + */ + public function getMessages() + { + return $this->messages; + } + + /** + * @return int + */ + public function getCountMissings() + { + return isset($this->counters[DataCollectorTranslator::MESSAGE_MISSING]) ? $this->counters[DataCollectorTranslator::MESSAGE_MISSING] : 0; + } + + /** + * @return int + */ + public function getCountFallbacks() + { + return isset($this->counters[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK]) ? $this->counters[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] : 0; + } + + /** + * @return int + */ + public function getCountDefines() + { + return isset($this->counters[DataCollectorTranslator::MESSAGE_DEFINED]) ? $this->counters[DataCollectorTranslator::MESSAGE_DEFINED] : 0; + } + + private function sanitizeCollectedMessages($messages) + { + $result = array(); + foreach ($messages as $key => $message) { + $messageId = $message['locale'].$message['domain'].$message['id']; + + if (!isset($result[$messageId])) { + $message['count'] = 1; + $messages[$key]['translation'] = $this->sanitizeString($message['translation']); + $result[$messageId] = $message; + } else { + $result[$messageId]['count']++; + } + + unset($messages[$key]); + } + + return $result; + } + + private function computeCount($messages) + { + $count = array( + DataCollectorTranslator::MESSAGE_DEFINED => 0, + DataCollectorTranslator::MESSAGE_MISSING => 0, + DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0, + ); + + foreach ($messages as $message) { + ++$count[$message['state']]; + } + + return $count; + } + + private function sanitizeString($string, $length = 80) + { + $string = trim(preg_replace('/\s+/', ' ', $string)); + + if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($string)) { + if (mb_strlen($string, $encoding) > $length) { + return mb_substr($string, 0, $length - 3, $encoding).'...'; + } + } elseif (strlen($string) > $length) { + return substr($string, 0, $length - 3).'...'; + } + + return $string; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'translation'; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return serialize(array('counters' => $this->counters, 'messages' => $this->messages)); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $unserialized = unserialize($serialized); + $this->counters = $unserialized['counters']; + $this->messages = $unserialized['messages']; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Profiler/TranslationDataCollector.php b/src/Symfony/Component/Translation/Profiler/TranslationDataCollector.php new file mode 100644 index 0000000000000..b8d50e27f13d4 --- /dev/null +++ b/src/Symfony/Component/Translation/Profiler/TranslationDataCollector.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\Translation\Profiler; + +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; +use Symfony\Component\Profiler\DataCollector\LateDataCollectorInterface; +use Symfony\Component\Translation\DataCollectorTranslator; + +/** + * @author Abdellatif Ait boudad + * @author Jelte Steijaert + */ +class TranslationDataCollector implements LateDataCollectorInterface +{ + /** + * @var DataCollectorTranslator + */ + private $translator; + + /** + * @param DataCollectorTranslator $translator + */ + public function __construct(DataCollectorTranslator $translator) + { + $this->translator = $translator; + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + return new TranslationData($this->translator); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Profiler/TranslationDataCollectorTest.php b/src/Symfony/Component/Translation/Tests/Profiler/TranslationDataCollectorTest.php new file mode 100644 index 0000000000000..57883fd189c12 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Profiler/TranslationDataCollectorTest.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\Translation\Tests\Profiler; + +use Symfony\Component\Translation\DataCollectorTranslator; +use Symfony\Component\Translation\Profiler\TranslationDataCollector; + +class TranslationDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\Profiler\DataCollector\DataCollectorInterface')) { + $this->markTestSkipped('The "DataCollector" is not available'); + } + } + + public function testCollectEmptyMessages() + { + $translator = $this->getTranslator(); + $translator->expects($this->any())->method('getCollectedMessages')->will($this->returnValue(array())); + + $dataCollector = new TranslationDataCollector($translator); + $data = $dataCollector->lateCollect(); + + $this->assertEquals(0, $data->getCountMissings()); + $this->assertEquals(0, $data->getCountFallbacks()); + $this->assertEquals(0, $data->getCountDefines()); + $this->assertEquals(array(), $data->getMessages()); + } + + public function testCollect() + { + $collectedMessages = array( + array( + 'id' => 'foo', + 'translation' => 'foo (en)', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_DEFINED, + ), + array( + 'id' => 'bar', + 'translation' => 'bar (fr)', + 'locale' => 'fr', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, + ), + array( + 'id' => 'choice', + 'translation' => 'choice', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_MISSING, + ), + array( + 'id' => 'choice', + 'translation' => 'choice', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_MISSING, + ), + ); + $expectedMessages = array( + array( + 'id' => 'foo', + 'translation' => 'foo (en)', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_DEFINED, + 'count' => 1, + ), + array( + 'id' => 'bar', + 'translation' => 'bar (fr)', + 'locale' => 'fr', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, + 'count' => 1, + ), + array( + 'id' => 'choice', + 'translation' => 'choice', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_MISSING, + 'count' => 2, + ), + ); + + $translator = $this->getTranslator(); + $translator->expects($this->any())->method('getCollectedMessages')->will($this->returnValue($collectedMessages)); + + $dataCollector = new TranslationDataCollector($translator); + $data = $dataCollector->lateCollect(); + + $this->assertEquals(1, $data->getCountMissings()); + $this->assertEquals(1, $data->getCountFallbacks()); + $this->assertEquals(1, $data->getCountDefines()); + $this->assertEquals($expectedMessages, array_values($data->getMessages())); + } + + private function getTranslator() + { + $translator = $this + ->getMockBuilder('Symfony\Component\Translation\DataCollectorTranslator') + ->disableOriginalConstructor() + ->getMock() + ; + + return $translator; + } +} diff --git a/src/Symfony/Component/VarDumper/Dumper/TraceableDumper.php b/src/Symfony/Component/VarDumper/Dumper/TraceableDumper.php new file mode 100644 index 0000000000000..4711158d17e7e --- /dev/null +++ b/src/Symfony/Component/VarDumper/Dumper/TraceableDumper.php @@ -0,0 +1,258 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Dumper; + +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Cloner\VarCloner; + +/** + * TraceableDumper. + * + * @author Jelte Steijaert + */ +class TraceableDumper implements DataDumperInterface +{ + private $dumper; + private $stopwatch; + private $fileLinkFormat; + private $data = array(); + private $dataCount = 0; + private $isCollected = true; + private $twigLoader = null; + + /** + * Constructor. + * + * @param DataDumperInterface $dumper + * @param Stopwatch $stopwatch + * @param string|null $fileLinkFormat + * @param string|null $charset + */ + public function __construct(DataDumperInterface $dumper = null, Stopwatch $stopwatch = null, $fileLinkFormat = null, + $charset = null, \Twig_LoaderInterface $twigLoader = null) + { + $this->stopwatch = $stopwatch; + $this->dumper = $dumper; + $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + $this->charset = $charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'; + $this->twigLoader = $twigLoader; + } + + /** + * {@inheritdoc} + */ + public function dump(Data $data) + { + if ($this->stopwatch) { + $this->stopwatch->start('dump'); + } + + if ($this->isCollected) { + $this->isCollected = false; + } + + $trace = DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS; + if (PHP_VERSION_ID >= 50400) { + $trace = debug_backtrace($trace, 7); + } else { + $trace = debug_backtrace($trace); + } + + $file = isset($trace[0]['file']) ? $trace[0]['file'] : null; + $line = isset($trace[0]['line']) ? $trace[0]['line'] : null; + $name = false; + $fileExcerpt = false; + + for ($i = 1; $i < 7; ++$i) { + if (isset($trace[$i]['class'], $trace[$i]['function']) + && 'dump' === $trace[$i]['function'] + && 'Symfony\Component\VarDumper\VarDumper' === $trace[$i]['class'] + ) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + while (++$i < 7) { + if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + if (isset($trace[++$i]) && isset($trace[$i]['object']) && $trace[$i]['object'] instanceof \Twig_Template) { + $info = $trace[$i]['object']; + $name = $info->getTemplateName(); + $info = $info->getDebugInfo(); + $line = $trace[$i - 1]['line']; + if ( !isset($info[$line]) ) { + $line--; + } + + if (null !== $this->twigLoader && isset($info[$line])) { + + $src = $this->twigLoader->getSource($name); + $file = false; + $line = $info[$line]; + $src = explode("\n", $src); + $fileExcerpt = array(); + + for ($i = max($line - 3, 1), $max = min($line + 3, count($src)); $i <= $max; ++$i) { + $fileExcerpt[] = ''.$this->htmlEncode($src[$i - 1]).''; + } + + $fileExcerpt = '
    '.implode("\n", $fileExcerpt).'
'; + } + } + break; + } + } + break; + } + } + + if (false === $name) { + $name = strtr($file, '\\', '/'); + $name = substr($name, strrpos($name, '/') + 1); + } + + if ($this->dumper) { + $this->doDump($data, $name, $file, $line); + } + + $this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt'); + ++$this->dataCount; + + if ($this->stopwatch) { + $this->stopwatch->stop('dump'); + } + } + + /** + * Trigger force writing output. + * + * @param string|null $format + */ + public function forceOutput($format = null) + { + $currentDumper = $this->dumper; + if ('html' == $format) { + $this->dumper = new HtmlDumper('php://output', $this->charset); + } else { + $this->dumper = new CliDumper('php://output', $this->charset); + } + foreach ($this->getData() as $dump) { + $this->doDump($dump['data'], $dump['name'], $dump['file'], $dump['line']); + } + $this->dumper = $currentDumper; + } + + /** + * Returns the collection of dumps. + * + * @return array + */ + public function getData() + { + $data = $this->data; + $this->isCollected = true; + $this->data = array(); + $this->dataCount = 0; + return $data; + } + + /** + * Returns the Charset. + * + * @return string + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Destructor. + */ + public function __destruct() + { + if (!$this->isCollected && $this->data) { + $this->isCollected = true; + + $h = headers_list(); + $i = count($h); + array_unshift($h, 'Content-Type: '.ini_get('default_mimetype')); + while (0 !== stripos($h[$i], 'Content-Type:')) { + --$i; + } + + if ('cli' !== PHP_SAPI && stripos($h[$i], 'html')) { + $this->dumper = new HtmlDumper('php://output', $this->charset); + } else { + $this->dumper = new CliDumper('php://output', $this->charset); + } + + foreach ($this->data as $i => $dump) { + $this->data[$i] = null; + $this->doDump($dump['data'], $dump['name'], $dump['file'], $dump['line']); + } + + $this->data = array(); + $this->dataCount = 0; + } + } + + private function doDump($data, $name, $file, $line) + { + if (PHP_VERSION_ID >= 50400 && $this->dumper instanceof CliDumper) { + $contextDumper = function ($name, $file, $line, $fileLinkFormat) { + if ($this instanceof HtmlDumper) { + if ('' !== $file) { + $s = $this->style('meta', '%s'); + $name = strip_tags($this->style('', $name)); + $file = strip_tags($this->style('', $file)); + if ($fileLinkFormat) { + $link = strtr($fileLinkFormat, array('%f' => $file, '%l' => (int) $line)); + $name = sprintf(''.$s.'', $link, $file, $name); + } else { + $name = sprintf(''.$s.'', $file, $name); + } + } else { + $name = $this->style('meta', $name); + } + $this->line = $name.' on line '.$this->style('meta', $line).':'; + } else { + $this->line = $this->style('meta', $name).' on line '.$this->style('meta', $line).':'; + } + $this->dumpLine(0); + }; + $contextDumper = $contextDumper->bindTo($this->dumper, $this->dumper); + $contextDumper($name, $file, $line, $this->fileLinkFormat); + } else { + $cloner = new VarCloner(); + $this->dumper->dump($cloner->cloneVar($name.' on line '.$line.':')); + } + $this->dumper->dump($data); + } + + private function htmlEncode($s) + { + $html = ''; + + $dumper = new HtmlDumper(function ($line) use (&$html) { + $html .= $line; + }, $this->charset); + $dumper->setDumpHeader(''); + $dumper->setDumpBoundaries('', ''); + + $cloner = new VarCloner(); + $dumper->dump($cloner->cloneVar($s)); + + return substr(strip_tags($html), 1, -1); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/VarDumper/Profiler/DumpData.php b/src/Symfony/Component/VarDumper/Profiler/DumpData.php new file mode 100644 index 0000000000000..8bec6d4e2e08d --- /dev/null +++ b/src/Symfony/Component/VarDumper/Profiler/DumpData.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Profiler; + +use Symfony\Component\Profiler\ProfileData\ProfileDataInterface; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; + +/** + * DumpData. + * + * @author Jelte Steijaert + */ +class DumpData implements ProfileDataInterface +{ + private $data; + private $dataCount = 0; + private $charset; + + /** + * Constructor. + * + * @param array $data + * @param $charset + */ + public function __construct(array $data, $charset) + { + $this->data = $data; + $this->dataCount = count($data); + $this->charset = $charset; + } + + /** + * Returns the number of dumps + * + * @return int + */ + public function getDumpsCount() + { + return $this->dataCount; + } + + /** + * Return the collected dumps in a specific format. + * + * @param $format + * @param int $maxDepthLimit + * @param int $maxItemsPerDepth + * @return array + */ + public function getDumps($format, $maxDepthLimit = -1, $maxItemsPerDepth = -1) + { + $data = fopen('php://memory', 'r+b'); + + if ('html' === $format) { + $dumper = new HtmlDumper($data, $this->charset); + } else { + throw new \InvalidArgumentException(sprintf('Invalid dump format: %s', $format)); + } + $dumps = array(); + + foreach ($this->data as $dump) { + if (method_exists($dump['data'], 'withMaxDepth')) { + $dumper->dump($dump['data']->withMaxDepth($maxDepthLimit)->withMaxItemsPerDepth($maxItemsPerDepth)); + } else { + // getLimitedClone is @deprecated, to be removed in 3.0 + $dumper->dump($dump['data']->getLimitedClone($maxDepthLimit, $maxItemsPerDepth)); + } + rewind($data); + $dump['data'] = stream_get_contents($data); + ftruncate($data, 0); + rewind($data); + $dumps[] = $dump; + } + + return $dumps; + } + + public function getName() + { + return 'dump'; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return serialize(array('data' => $this->data, 'dataCount' => $this->dataCount, 'charset' => $this->charset)); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $unserialized = unserialize($serialized); + $this->data = $unserialized['data']; + $this->dataCount = $unserialized['dataCount']; + $this->charset = $unserialized['charset']; + } +} diff --git a/src/Symfony/Component/VarDumper/Profiler/DumpDataCollector.php b/src/Symfony/Component/VarDumper/Profiler/DumpDataCollector.php new file mode 100644 index 0000000000000..09e6eb5608493 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Profiler/DumpDataCollector.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Profiler; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Profiler\DataCollector\DataCollectorInterface; +use Symfony\Component\VarDumper\Dumper\TraceableDumper; + +/** + * DumpDataCollector. + * + * @author Nicolas Grekas + * @author Jelte Steijaert + */ +class DumpDataCollector implements EventSubscriberInterface, DataCollectorInterface +{ + private $requestStack; + private $dumper; + private $responses; + + /** + * Constructor. + * + * @param RequestStack $requestStack The RequestStack. + * @param TraceableDumper $dumper The TraceableDumper. + */ + public function __construct(RequestStack $requestStack, TraceableDumper $dumper) + { + $this->requestStack = $requestStack; + $this->dumper = $dumper; + $this->responses = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function getCollectedData() + { + $request = $this->requestStack->getCurrentRequest(); + + if (null === $request) { + return; + } + + // Sub-requests and programmatic calls stay in the collected profile. + if ($this->requestStack->getMasterRequest() !== $request || $request->isXmlHttpRequest() || $request->headers->has('Origin')) { + return; + } + + if ( !isset($this->responses[$request]) ) { + return; + } + + $response = $this->responses[$request]; + + // In all other conditions that remove the web debug toolbar, dumps are written on the output. + if ( + !$response->headers->has('X-Debug-Token') + || $response->isRedirection() + || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) + || 'html' !== $request->getRequestFormat() + || false === strripos($response->getContent(), '') + ) { + $isHtml = ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')); + $this->dumper->forceOutput($isHtml?'html':'cli'); + } + + return new DumpData($this->dumper->getData(), $this->dumper->getCharset()); + } + + /** + * Remembers the response associated to each request. + * + * @param FilterResponseEvent $event The filter response event + */ + public function onKernelResponse(FilterResponseEvent $event) + { + $this->responses[$event->getRequest()] = $event->getResponse(); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + KernelEvents::RESPONSE => 'onKernelResponse', + ); + } +} diff --git a/src/Symfony/Component/VarDumper/Tests/Dumper/TraceableDumperTest.php b/src/Symfony/Component/VarDumper/Tests/Dumper/TraceableDumperTest.php new file mode 100644 index 0000000000000..375d21ccd7383 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/Dumper/TraceableDumperTest.php @@ -0,0 +1,204 @@ +dump($data); + $output = ob_get_clean(); + $line = __LINE__ -2; + + if (PHP_VERSION_ID >= 50400) { + $xOutput = $this->expectedOutput($line, 123); + } else { + $xOutput = $this->legacyOutput($line, 123); + } + + $this->assertSame($xOutput, $this->cleanOutput($output)); + $this->assertCount(1, $dumper->getData()); + } + + public function testDumpExtensive() + { + $data = new Data(array(array(123))); + + $dumper = new TraceableDumper(new HtmlDumper()); + + ob_start(); + VarDumper::setHandler(array($dumper, 'dump')); + VarDumper::dump($data); + $output = ob_get_clean(); + $line = __LINE__ -2; + + if (PHP_VERSION_ID >= 50400) { + $xOutput = $this->expectedOutput($line, 123); + } else { + $xOutput = $this->legacyOutput($line, 123); + } + + $this->assertSame($xOutput, $this->cleanOutput($output)); + $this->assertCount(1, $dumper->getData()); + } + + public function testDumpFromTwigTemplate() + { + + $loader = $this->getMockBuilder('Twig_LoaderInterface')->getMock(); + + $dumper = new TraceableDumper(new HtmlDumper(), null, null, null, $loader); + + VarDumper::setHandler(array($dumper, 'dump')); + $src = ''; + for ($i = 1; $i < 100; $i++) { + $src .= $i."\n"; + } + $loader->expects($this->any())->method('getSource')->willReturn($src); + + $environment = $this->getMockBuilder('Twig_Environment') + ->disableOriginalConstructor() + ->getMock(); + + $template = new TwigTemplate($environment, $dumper); + ob_start(); + $template->display(array(123)); + + $output = ob_get_clean(); + + if (PHP_VERSION_ID >= 50400) { + $xOutput = $this->expectedOutput(29, 123, 'num', 'Tests/Profiler/Mock/TwigTemplate.php', 'TwigTemplate.php'); + } else { + $xOutput = $this->legacyOutput(29, 123, 'num', 'TwigTemplate.php'); + } + + $this->assertSame($xOutput, $this->cleanOutput($output)); + $this->assertCount(1, $dumper->getData()); + } + + public function testDumpCallUserFunction() + { + $dumper = new TraceableDumper(new HtmlDumper()); + VarDumper::setHandler(array($dumper, 'dump')); + + ob_start(); + testTraceableDumperTestDump(array(123)); + $output = ob_get_clean(); + $line = __LINE__ -2; + + if (PHP_VERSION_ID >= 50400) { + $xOutput = $this->expectedOutput($line, 123); + } else { + $xOutput = $this->legacyOutput($line, 123); + } + + $this->assertSame($xOutput, $this->cleanOutput($output)); + $this->assertCount(1, $dumper->getData()); + } + + public function testCollectHtml() + { + $dumper = new TraceableDumper(new HtmlDumper(), null, 'test://%f:%l'); + + VarDumper::setHandler(array($dumper, 'dump')); + + ob_start(); + VarDumper::dump(new Data(array(array(123)))); + $output = ob_get_clean(); + + $line = __LINE__ - 3; + if (PHP_VERSION_ID >= 50400) { + $xOutput = sprintf( + "
%s on line %s:\n123\n
\n", + __FILE__, + $line, + __FILE__, + substr(__FILE__, strlen(__DIR__) - strlen(__FILE__)+1), + $line + ); + } else { + $xOutput = $this->legacyOutput($line, 123); + } + + $this->assertSame($xOutput, $this->cleanOutput($output, false)); + $this->assertCount(1, $dumper->getData()); + } + + public function testDescruction() + { + $data = new Data(array(array(123))); + + $traceableDumper = new TraceableDumper(); + + ob_start(); + $traceableDumper->dump($data); + $line = __LINE__ - 1; + unset($traceableDumper); + $output = ob_get_clean(); + + + if (PHP_VERSION_ID >= 50400) { + $this->assertSame("TraceableDumperTest.php on line {$line}:\n123\n", $output); + } else { + $this->assertSame("\"TraceableDumperTest.php on line {$line}:\"\n123\n", $output); + } + } + + private function cleanOutput($output, $replacePath = true) + { + $output = preg_replace('#<(script|style).*?#s', '', $output); + $output = preg_replace('/sf-dump-\d+/', 'sf-dump', $output); + $output = preg_replace('/^.*?
.*$/', '
',$output); + if ( $replacePath ) { + $output = preg_replace('/^.*Tests\/Dumper\/TraceableDumperTest.php\">/', '
', $output);
+            $output = preg_replace('/^.*Tests\/Profiler\/Mock\/TwigTemplate.php\">/', '
', $output);
+        }
+
+        return $output;
+    }
+
+    private function legacyOutput($line, $value, $type = 'num', $file = 'TraceableDumperTest.php')
+    {
+        $len = strlen(sprintf("%s on line %s:", $file, $line));
+        return sprintf(
+            "
\"%s on line %s:\"\n
\n
%s\n
\n", + $len, + $file, + $line, + $type, + $value + ); + } + + + private function expectedOutput($line, $value, $type = 'num', $path = 'Tests/Dumper/TraceableDumperTest.php', $file = 'TraceableDumperTest.php') + { + return sprintf( + "
%s on line %s:\n%s\n
\n", + $path, + $file, + $line, + $type, + $value + ); + } +} + +function testTraceableDumperTestDump($data) { + VarDumper::dump(new Data(array($data))); +} \ No newline at end of file diff --git a/src/Symfony/Component/VarDumper/Tests/Profiler/DumpDataCollectorTest.php b/src/Symfony/Component/VarDumper/Tests/Profiler/DumpDataCollectorTest.php new file mode 100644 index 0000000000000..2b413bbdc32cd --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/Profiler/DumpDataCollectorTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests\Profiler; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\TraceableDumper; +use Symfony\Component\VarDumper\Profiler\DumpData; +use Symfony\Component\VarDumper\Profiler\DumpDataCollector; + +/** + * @author Nicolas Grekas + */ +class DumpDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + + public function testCollectDefault() + { + $data = new Data(array(array(123))); + + $traceableDumper = new TraceableDumper(); + + $request = new Request(); + $requestStack = new RequestStack(); + $requestStack->push($request); + $collector = new DumpDataCollector($requestStack, $traceableDumper); + $response = new Response('', 200, array('X-Debug-Token' => 'test')); + $request->setRequestFormat('html'); + + $collector->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $request, HttpKernelInterface::MASTER_REQUEST, $response + ) + ); + $traceableDumper->dump($data); + $line = __LINE__ - 1; + + /** @var DumpData $data */ + $data = $collector->getCollectedData(); + $this->assertInstanceof('Symfony\Component\VarDumper\Profiler\DumpData', $data); + $this->assertEquals(1, $data->getDumpsCount()); + $dumps = $data->getDumps('html'); + $this->assertCount(1, $dumps); + $this->assertEquals($line, $dumps[0]['line']); + $this->assertEquals(__FILE__, $dumps[0]['file']); + } + + public function testCollectRedirectResponse() + { + $data = new Data(array(array(123))); + + $traceableDumper = new TraceableDumper(); + + $request = new Request(); + $requestStack = new RequestStack(); + $requestStack->push($request); + $collector = new DumpDataCollector($requestStack, $traceableDumper); + + ob_start(); + $traceableDumper->dump($data); + $line = __LINE__ - 1; + unset($traceableDumper); + unset($collector); + $output = ob_get_clean(); + + + if (PHP_VERSION_ID >= 50400) { + $this->assertSame("DumpDataCollectorTest.php on line {$line}:\n123\n", $output); + } else { + $this->assertSame("\"DumpDataCollectorTest.php on line {$line}:\"\n123\n", $output); + } + } + + public function testCollectWithoutRequestOrResponse() + { + $requestStack = new RequestStack(); + $collector = new DumpDataCollector($requestStack, new TraceableDumper()); + $this->assertNULL($collector->getCollectedData()); + $request1 = new Request(); + $request2 = new Request(); + $requestStack->push($request1); + $requestStack->push($request2); + + $collector->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $request1, HttpKernelInterface::MASTER_REQUEST, new Response('', 200, array('Content-Type' => 'text/html')) + ) + ); + $collector->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $request2, HttpKernelInterface::SUB_REQUEST, new Response('', 200, array('Content-Type' => 'text/html')) + ) + ); + $this->assertNULL($collector->getCollectedData()); + $requestStack->pop(); + $this->assertNotNULL($collector->getCollectedData()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid dump format: json + */ + public function testDumpsUnsupportedFormat() + { + $requestStack = new RequestStack(); + $request = new Request(); + $requestStack->push($request); + $collector = new DumpDataCollector($requestStack, new TraceableDumper()); + $response = new Response('', 200, array('Content-Type' => 'text/html')); + $collector->onKernelResponse( + new FilterResponseEvent( + $this->getKernel(), $requestStack->getMasterRequest(), HttpKernelInterface::MASTER_REQUEST, $response + ) + ); + $data = $collector->getCollectedData(); + $data->getDumps('json'); + } + + public function testSubscribedEvents() + { + $events = DumpDataCollector::getSubscribedEvents(); + $this->assertArrayHasKey(KernelEvents::RESPONSE, $events); + } + + protected function getKernel() + { + return $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + } +} diff --git a/src/Symfony/Component/VarDumper/Tests/Profiler/Mock/TwigTemplate.php b/src/Symfony/Component/VarDumper/Tests/Profiler/Mock/TwigTemplate.php new file mode 100644 index 0000000000000..bb74ece6c9949 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/Profiler/Mock/TwigTemplate.php @@ -0,0 +1,77 @@ + 29, + ); + } + + protected function doGetParent(array $context) + { + } + + public function getAttribute($object, $item, array $arguments = array(), $type = \Twig_Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false) + { + return parent::getAttribute($object, $item, $arguments, $type, $isDefinedTest, $ignoreStrictCheck); + } +} diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json index 3f872b07b7f52..fcf790dd2147b 100644 --- a/src/Symfony/Component/VarDumper/composer.json +++ b/src/Symfony/Component/VarDumper/composer.json @@ -20,6 +20,11 @@ "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { + "symfony/phpunit-bridge": "~2.7|~3.0.0", + "symfony/config": "~2.8|~3.0.0", + "symfony/http-kernel": "~2.8|~3.0.0", + "symfony/profiler": "~2.8|~3.0.0", + "symfony/stopwatch": "~2.8|~3.0.0", "twig/twig": "~1.20|~2.0" }, "suggest": { From d805c9fa144c3a627952b0771ef70fd2979c362d Mon Sep 17 00:00:00 2001 From: Jelte Steijaert Date: Thu, 12 Nov 2015 16:02:10 +0100 Subject: [PATCH 2/2] Require zendframework/zend-stdlib to fix dependency in ocramius/proxy-manager --- composer.json | 1 + src/Symfony/Bridge/ProxyManager/composer.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 985eac6f78e5e..2436f1368327e 100644 --- a/composer.json +++ b/composer.json @@ -85,6 +85,7 @@ "doctrine/doctrine-bundle": "~1.2", "monolog/monolog": "~1.11", "ocramius/proxy-manager": "~0.4|~1.0", + "zendframework/zend-stdlib": "~2.4.8", "egulias/email-validator": "~1.2", "phpdocumentor/reflection": "^1.0.7" }, diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 2a9fd8c0cfa55..0315f6ffb8853 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -18,7 +18,8 @@ "require": { "php": ">=5.3.9", "symfony/dependency-injection": "~2.8|~3.0.0", - "ocramius/proxy-manager": "~0.4|~1.0" + "ocramius/proxy-manager": "~0.4|~1.0", + "zendframework/zend-stdlib": "~2.4.8" }, "require-dev": { "symfony/config": "~2.3|~3.0.0"