diff --git a/CHANGELOG-2.4.md b/CHANGELOG-2.4.md new file mode 100644 index 0000000000000..04115910cc56d --- /dev/null +++ b/CHANGELOG-2.4.md @@ -0,0 +1,162 @@ +CHANGELOG for 2.4.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 2.4 minor versions. + +To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash +To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v2.4.0...v2.4.1 + +* 2.4.1 (2014-01-05) + + * bug #9938 [Process] Add support SAPI cli-server (peter-gribanov) + * bug #9940 [EventDispatcher] Fix hardcoded listenerTag name in error message (lemoinem) + * bug #9923 [DoctrineBridge] Fixed an issue with DoctrineParserCache (florianv) + * bug #9908 [HttpFoundation] Throw proper exception when invalid data is passed to JsonResponse class (stloyd) + * bug #9902 [Security] fixed pre/post authentication checks (fabpot) + * bug #9910 fixed missing use statements (fabpot) + * bug #9895 [Intl] Added round support for ROUND_CEILING, ROUND_FLOOR, ROUND_DOWN, ROUND_UP (pamil) + * bug #9899 [Filesystem | WCM] 9339 fix stat on url for filesystem copy (cordoval) + * bug #9589 [DependencyInjection] Fixed #9020 - Added support for collections in service#parameters (lavoiesl) + * bug #9889 [Console] fixed column width when using the Table helper with some decoration in cells (fabpot) + * bug #9323 [DomCrawler]fix #9321 Crawler::addHtmlContent add gbk encoding support (bronze1man) + * bug #8997 [Security] Fixed problem with losing ROLE_PREVIOUS_ADMIN role. (pawaclawczyk) + * bug #9557 [DoctrineBridge] Fix for cache-key conflict when having a \Traversable as choices (DRvanR) + * bug #9879 [Security] Fix ExceptionListener to catch correctly AccessDeniedException if is not first exception (fabpot) + * bug #9885 [Dependencyinjection] Fixed handling of inlined references in the AnalyzeServiceReferencesPass (fabpot) + * bug #9884 [DomCrawler] Fixed creating form objects from named form nodes (jakzal) + * bug #9882 Add support for HHVM in the getting of the PHP executable (fabpot) + * bug #9850 [Validator] Fixed IBAN validator with 0750447346 value (stewe) + * bug #9865 [Validator] Fixes message value for objects (jongotlin) + * bug #9441 [Form][DateTimeToArrayTransformer] Check for hour, minute & second validity (egeloen) + * bug #9720 [FrameworkBundle] avoid tables to have apparently long blank line breaks and be too far appart for long nested array params (cordoval) + * bug #9867 #9866 [Filesystem] Fixed mirror for symlinks (COil) + * bug #9806 [Security] Fix parent serialization of user object (ddeboer) + * bug #9834 [DependencyInjection] Fixed support for backslashes in service ids. (jakzal) + * bug #9826 fix #9356 [Security] Logger should manipulate the user reloaded from provider (matthieuauger) + * feature #9775 [FrameworkBundle] Added extra details in XMLDescriptor to improve container description (Exelenz) + * bug #9771 Crawler default namespace fix (crudecki) + * bug #9769 [BrowserKit] fixes #8311 CookieJar is totally ignorant of RFC 6265 edge cases (jzawadzki) + * bug #9697 [Config] fix 5528 let ArrayNode::normalizeValue respect order of value array provided (cordoval) + * bug #9701 [Config] fix #7243 allow 0 as arraynode name (cordoval) + * bug #9795 [Form] Fixed issue in BaseDateTimeTransformer when invalid timezone cause Trans... (tyomo4ka) + * bug #9714 [HttpFoundation] BinaryFileResponse should also return 416 or 200 on some range-requets (SimonSimCity) + * bug #9601 [Routing] Remove usage of deprecated _scheme requirement (Danez) + * bug #9489 [DependencyInjection] Add normalization to tag options (WouterJ) + * bug #9135 [Form] [Validator] fix maxLength guesser (franek) + * bug #9790 [Filesystem] Changed the mode for a target file in copy() to be write only (jakzal) + * bug #9758 [Console] fixed TableHelper when cell value has new line (k-przybyszewski) + * bug #9760 [Routing] Fix router matching pattern against multiple hosts (karolsojko) + * bug #9768 [FrameworkBundle] Fixed bug in XMLDescriptor (Exelenz) + * bug #9700 [ExpressionLanguage] throw exception when parameters contain expressions (aitboudad) + * bug #9674 [Form] rename validators.ua.xlf to validators.uk.xlf (craue) + * bug #9722 [Validator]Fixed getting wrong msg when value is an object in Exception (aitboudad) + * bug #9750 allow TraceableEventDispatcher to reuse event instance in nested events (evillemez) + * bug #9718 [validator] throw an exception if isn't an instance of ConstraintValidatorInterface. (aitboudad) + * bug #9716 Reset the box model to content-box in the web debug toolbar (stof) + * bug #9711 [FrameworkBundle] Allowed "0" as a checkbox value in php templates (jakzal) + +* 2.4.0 (2013-12-03) + + * bug #9673 Fixed BC break in csrf protection (WouterJ) + * bug #9665 [Bridge/Doctrine] ORMQueryBuilderLoader - handled the scenario when no entity manager is passed with closure query builder (jakzal) + * bug #9662 [FrameworkBundle] Enabled csrf_protection by default if form.csrf_protection is enabled (bschussek) + * bug #9656 [DoctrineBridge] normalized class names in the ORM type guesser (fabpot) + * bug #9647 use the correct class name to retrieve mapped class' metadata and reposi... (xabbuh) + * bug #9648 [Debug] ensured that a fatal PHP error is actually fatal after being handled by our error handler (fabpot) + * bug #9643 [WebProfilerBundle] Fixed js escaping in time.html.twig (hason) + * bug #9641 [Debug] Avoid notice from being "eaten" by fatal error. (fabpot) + * bug #9639 Modified guessDefaultEscapingStrategy to not escape txt templates (fabpot) + * bug #9314 [Form] Fix DateType for 32bits computers. (WedgeSama) + * bug #9443 [FrameworkBundle] Fixed the registration of validation.xml file when the form is disabled (hason) + * bug #9625 [HttpFoundation] Do not return an empty session id if the session was closed (Taluu) + * bug #9621 [ExpressionLanguage] fixed lexing expression ending with spaces (fabpot) + * bug #9637 [Validator] Replaced inexistent interface (jakzal) + * bug #9628 [HttpKernel] Fix profiler event-listener usage outside request stack context (romainneutron) + * bug #9624 [Console] Fix undefined offset when formatting namespace suggestions (GromNaN) + * bug #9605 Adjusting CacheClear Warmup method to namespaced kernels (rdohms) + * bug #9617 [HttpKernel] Http kernel regression fix (hhamon) + * bug #9610 Container::camelize also takes backslashes into consideration (ondrejmirtes) + +* 2.4.0-RC1 (2013-11-25) + + * bug #9607 [HttpKernel] Fix a bug when using the kernel property in overridden method Client::setServerParameters() (gnutix) + * bug #9597 [Security] Typos in Security's ExpressionLanguage (ovrflo) + * feature #9587 [SecurityBundle] Added csrf_token_generator and csrf_token_id as new (shieldo) + * feature #9578 [DomCrawler] Fixes `attr` method returning empty string for missing attributes (aik099) + * bug #9565 [Debug] Fixed ClassNotFoundFatalErrorHandler which could cause a fatal error (jakzal) + * bug #9525 Cache Warmup Breaks Namespaced Kernel (rdohms) + * bug #9447 [BrowserKit] fixed protocol-relative url redirection (jong99) + * bug #9535 No Entity Manager defined exception (armetiz) + * bug #9485 [Acl] Fix for issue #9433 (guilro) + * bug #9516 [AclProvider] Fix incorrect behavior when partial results returned from cache (superdav42) + * feature #9546 [FrameworkBundle] use the new request_stack service in the GlobalVariables object (hhamon) + * bug #9566 [Console] Revert BC-break for verbose option value (chEbba) + * bug #9553 [FrameworkBundle] use the new request_stack service to get the Request object in the base Controller class (hhamon) + * feature #9541 [Translation] make IdentityTranslater consistent with normal translator (Tobion) + * bug #9536 [FrameworkBundle] Update 2 dependencies (currently broken) (asm89) + * bug #9352 [Intl] make currency bundle merge fallback locales when accessing data, ... (shieldo) + * bug #9537 [FrameworkBundle] Fix mistake in translation's service definition. (phpmike) + * bug #9529 [ExpressionLanguage] Fixed conflict between punctation and range (WouterJ) + * bug #9367 [Process] Check if the pipe array is empty before calling stream_select() (jfposton) + * bug #9211 [Form] Fixed memory leak in FormValidator (bschussek) + * bug #9469 [Propel1] re-factor Propel1 ModelChoiceList (havvg) + * bug #9499 Request::overrideGlobals() may call invalid ini value (denkiryokuhatsuden) + * feature #9494 made Router implement RequestMatcherInterface (fabpot) + * bug #9420 [Console][ProgressHelper] Fix ProgressHelper redraw when redrawFreq is greater than 1 (giosh94mhz) + * bug #9212 [Validator] Force Luhn Validator to only work with strings (Richtermeister) + * bug #9476 Fixed bug with lazy services (peterrehm) + * bug #9461 set mergeFallback to true, (ychadwick) + * bug #9451 Fix bug with variable named context to securityContext (mieszko4) + * feature #9434 moved logic for the session listeners into the HttpKernel component (fabpot) + * bug #9431 [DependencyInjection] fixed YamlDumper did not make services private. (realityking) + * bug #9332 [Config] Quoting reserved characters (WouterJ) + * bug #9416 fixed issue with clone now the children of the original form are preserved and the clone form is given new children (yjv) + * bug #9423 [Form] fix CsrfProviderAdapter (Tobion) + * feature #9342 Add X-Debug-Url profiler url header (adrienbrault) + * bug #9412 [HttpFoundation] added content length header to BinaryFileResponse (kbond) + +* 2.4.0-BETA2 (2013-10-30) + + * bug #9408 [Form] Fixed failing FormDataExtractorTest (bschussek) + * bug #9397 [BUG][Form] Fix nonexistent key id in twig of data collector (francoispluchino) + * bug #9395 [HttpKernel] fixed memory limit display in MemoryDataCollector (hhamon) + * bug #9168 [FrameworkBundle] made sure that the debug event dispatcher is used everywhere (fabpot) + * bug #9388 [Form] Fixed: The "data" option is taken into account even if it is NULL (bschussek) + * bug #9394 [Form] Fixed form debugger to work even when no view variables are logged (bschussek) + * bug #9391 [Serializer] Fixed the error handling when decoding invalid XML to avoid a Warning (stof) + * feature #9365 prevent PHP from magically setting a 302 header (lsmith77) + * feature #9252 [FrameworkBundle] Only enable CSRF protection when enabled in config (asm89) + * bug #9378 [DomCrawler] [HttpFoundation] Make `Content-Type` attributes identification case-insensitive (matthieuprat) + * bug #9354 [Process] Fix #9343 : revert file handle usage on Windows platform (romainneutron) + * bug #9335 [Form] Improved FormTypeCsrfExtension to use the type class as default intention if the form name is empty (bschussek) + * bug #9334 [Form] Improved FormTypeCsrfExtension to use the type class as default intention if the form name is empty (bschussek) + * bug #9333 [Form] Improved FormTypeCsrfExtension to use the type class as default intention if the form name is empty (bschussek) + * bug #9338 [DoctrineBridge] Added type check to prevent calling clear() on arrays (bschussek) + * bug #9330 [Config] Fixed namespace when dumping reference (WouterJ) + * bug #9329 [Form] Changed FormTypeCsrfExtension to use the form's name as default token ID (bschussek) + * bug #9328 [Form] Changed FormTypeCsrfExtension to use the form's name as default intention (bschussek) + * bug #9327 [Form] Changed FormTypeCsrfExtension to use the form's name as default intention (bschussek) + * bug #9316 [WebProfilerBundle] Fixed invalid condition in form panel (bschussek) + * bug #9308 [DoctrineBridge] Loosened CollectionToArrayTransformer::transform() to accept arrays (bschussek) + * bug #9297 [Form] Add missing use in form renderer (egeloen) + * bug #9309 [Routing] Fixed unresolved class (francoispluchino) + * bug #9274 [Yaml] Fixed the escaping of strings starting with a dash when dumping (stof) + * bug #9270 [Templating] Fix in ChainLoader.php (janschoenherr) + * bug #9246 [Session] fixed wrong started state (tecbot) + * bug #9234 [Debug] Fixed `ClassNotFoundFatalErrorHandler` (tPl0ch) + * bug #9259 [Process] Fix latest merge from 2.2 in 2.3 (romainneutron) + * bug #9237 [FrameworkBundle] assets:install command should mirror .dotfiles (.htaccess) (FineWolf) + * bug #9223 [Translator] PoFileDumper - PO headers (Padam87) + * bug #9257 [Process] Fix 9182 : random failure on pipes tests (romainneutron) + * bug #9236 [Form] fix missing use statement for exception UnexpectedTypeException (jaugustin) + * bug #9222 [Bridge] [Propel1] Fixed guessed relations (ClementGautier) + * bug #9214 [FramworkBundle] Check event listener services are not abstract (lyrixx) + * bug #9207 [HttpKernel] Check for lock existence before unlinking (ollietb) + * bug #9184 Fixed cache warmup of paths which contain back-slashes (fabpot) + * bug #9192 [Form] remove MinCount and MaxCount constraints in ValidatorTypeGuesser (franek) + +* 2.4.0-BETA1 (2013-10-07) + + * first beta release + diff --git a/UPGRADE-2.2.md b/UPGRADE-2.2.md index 7c7b31c9fe438..993426472f14a 100644 --- a/UPGRADE-2.2.md +++ b/UPGRADE-2.2.md @@ -23,7 +23,7 @@ #### Deprecations - * The `standalone` option is deprecated and will replaced with the `strategy` option in 2.3. + * The `standalone` option is deprecated and will be replaced with the `strategy` option in 2.3. * The values `true`, `false`, `js` for the `standalone` option were deprecated and replaced respectively with the `esi`, `inline`, `hinclude` in 2.3. diff --git a/UPGRADE-2.4.md b/UPGRADE-2.4.md new file mode 100644 index 0000000000000..bd9ecc3c4dae4 --- /dev/null +++ b/UPGRADE-2.4.md @@ -0,0 +1,9 @@ +UPGRADE FROM 2.3 to 2.4 +======================= + +Form +---- + + * The constructor parameter `$precision` in `IntegerToLocalizedStringTransformer` + is now ignored completely, because a precision does not make sense for + integers. diff --git a/UPGRADE-2.5.md b/UPGRADE-2.5.md new file mode 100644 index 0000000000000..053d5fa1f7562 --- /dev/null +++ b/UPGRADE-2.5.md @@ -0,0 +1,7 @@ +UPGRADE FROM 2.4 to 2.5 +======================= + +Routing +------- + + * Added a new optional parameter `$requiredSchemes` to `Symfony\Component\Routing\Generator\UrlGenerator::doGenerate()` diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index dd647eaf9ea12..2b2fd16c94eb7 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -18,6 +18,11 @@ UPGRADE FROM 2.x to 3.0 `DebugClassLoader`. The difference is that the constructor now takes a loader to wrap. +### EventDispatcher + + * The interface `Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface` + extends `Symfony\Component\EventDispatcher\EventDispatcherInterface`. + ### Form * The methods `Form::bind()` and `Form::isBound()` were removed. You should @@ -133,7 +138,7 @@ UPGRADE FROM 2.x to 3.0 ``` * The `TypeTestCase` class was moved from the `Symfony\Component\Form\Tests\Extension\Core\Type` namespace to the `Symfony\Component\Form\Test` namespace. - + Before: ``` @@ -156,11 +161,66 @@ UPGRADE FROM 2.x to 3.0 } ``` - * The `FormItegrationTestCase` and `FormPerformanceTestCase` classes were moved form the `Symfony\Component\Form\Tests` namespace to the `Symfony\Component\Form\Test` namespace. + * The `FormIntegrationTestCase` and `FormPerformanceTestCase` classes were moved form the `Symfony\Component\Form\Tests` namespace to the `Symfony\Component\Form\Test` namespace. + + * The constants `ROUND_HALFEVEN`, `ROUND_HALFUP` and `ROUND_HALFDOWN` in class + `NumberToLocalizedStringTransformer` were renamed to `ROUND_HALF_EVEN`, + `ROUND_HALF_UP` and `ROUND_HALF_DOWN`. + + * The methods `ChoiceListInterface::getIndicesForChoices()` and + `ChoiceListInterface::getIndicesForValues()` were removed. No direct + replacement exists, although in most cases + `ChoiceListInterface::getChoicesForValues()` and + `ChoiceListInterface::getValuesForChoices()` should be sufficient. + + * The interface `Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface` + and all of its implementations were removed. Use the new interface + `Symfony\Component\Security\Csrf\CsrfTokenManagerInterface` instead. + + * The options "csrf_provider" and "intention" were renamed to "csrf_token_generator" + and "csrf_token_id". ### FrameworkBundle + * The `getRequest` method of the base `Controller` class has been deprecated + since Symfony 2.4 and must be therefore removed in 3.0. The only reliable + way to get the `Request` object is to inject it in the action method. + + Before: + + ``` + namespace Acme\FooBundle\Controller; + + class DemoController + { + public function showAction() + { + $request = $this->getRequest(); + // ... + } + } + ``` + + After: + + ``` + namespace Acme\FooBundle\Controller; + + use Symfony\Component\HttpFoundation\Request; + + class DemoController + { + public function showAction(Request $request) + { + // ... + } + } + ``` + + * The `request` service was removed. You must inject the `request_stack` + service instead. + * The `enctype` method of the `form` helper was removed. You should use the new method `start` instead. @@ -211,6 +271,8 @@ UPGRADE FROM 2.x to 3.0 end($form) ?> ``` + * The `RouterApacheDumperCommand` was removed. + ### HttpKernel * The `Symfony\Component\HttpKernel\Log\LoggerInterface` has been removed in @@ -240,12 +302,15 @@ UPGRADE FROM 2.x to 3.0 * The `Symfony\Component\HttpKernel\EventListener\ExceptionListener` now passes the Request format as the `_format` argument instead of `format`. + * The `Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass` has been renamed to + `Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass` and moved to the EventDispatcher component. + ### Locale * The Locale component was removed and replaced by the Intl component. Instead of the methods in `Symfony\Component\Locale\Locale`, you should use these equivalent methods in `Symfony\Component\Intl\Intl` now: - + * `Locale::getDisplayCountries()` -> `Intl::getRegionBundle()->getCountryNames()` * `Locale::getCountries()` -> `array_keys(Intl::getRegionBundle()->getCountryNames())` * `Locale::getDisplayLanguages()` -> `Intl::getLanguageBundle()->getLanguageNames()` @@ -318,6 +383,14 @@ UPGRADE FROM 2.x to 3.0 $route->setSchemes('https'); ``` + * The `ApacheMatcherDumper` and `ApacheUrlMatcher` were removed since + the performance gains were minimal and it's hard to replicate the behaviour + of PHP implementation. + +### Security + + * The `Resources/` directory was moved to `Core/Resources/` + ### Translator * The `Translator::setFallbackLocale()` method has been removed in favor of @@ -379,6 +452,29 @@ UPGRADE FROM 2.x to 3.0 ### Validator + * The class `Symfony\Component\Validator\Mapping\Cache\ApcCache` has been removed in favor + of `Symfony\Component\Validator\Mapping\Cache\DoctrineCache`. + + Before: + + ``` + use Symfony\Component\Validator\Mapping\Cache\ApcCache; + + $cache = new ApcCache('symfony.validator'); + ``` + + After: + + ``` + use Symfony\Component\Validator\Mapping\Cache\DoctrineCache; + use Doctrine\Common\Cache\ApcCache; + + $apcCache = new ApcCache(); + $apcCache->setNamespace('symfony.validator'); + + $cache = new DoctrineCache($apcCache); + ``` + * The constraints `Optional` and `Required` were moved to the `Symfony\Component\Validator\Constraints\` namespace. You should adapt the path wherever you used them. @@ -411,6 +507,64 @@ UPGRADE FROM 2.x to 3.0 private $property; ``` + * The option "methods" of the `Callback` constraint was removed. You should + use the option "callback" instead. If you have multiple callbacks, add + multiple callback constraints instead. + + Before (YAML): + + ``` + constraints: + - Callback: [firstCallback, secondCallback] + ``` + + After (YAML): + + ``` + constraints: + - Callback: firstCallback + - Callback: secondCallback + ``` + + When using annotations, you can now put the Callback constraint directly on + the method that should be executed. + + Before (Annotations): + + ``` + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\ExecutionContextInterface; + + /** + * @Assert\Callback({"callback"}) + */ + class MyClass + { + public function callback(ExecutionContextInterface $context) + { + // ... + } + } + ``` + + After (Annotations): + + ``` + use Symfony\Component\Validator\Constraints as Assert; + use Symfony\Component\Validator\ExecutionContextInterface; + + class MyClass + { + /** + * @Assert\Callback + */ + public function callback(ExecutionContextInterface $context) + { + // ... + } + } + ``` + ### Yaml * The ability to pass file names to `Yaml::parse()` has been removed. diff --git a/composer.json b/composer.json index 9e2eee3214267..3b67c4b3e8ec5 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "symfony/doctrine-bridge": "self.version", "symfony/dom-crawler": "self.version", "symfony/event-dispatcher": "self.version", + "symfony/expression-language": "self.version", "symfony/filesystem": "self.version", "symfony/finder": "self.version", "symfony/form": "self.version", @@ -49,6 +50,10 @@ "symfony/proxy-manager-bridge": "self.version", "symfony/routing": "self.version", "symfony/security": "self.version", + "symfony/security-acl": "self.version", + "symfony/security-core": "self.version", + "symfony/security-csrf": "self.version", + "symfony/security-http": "self.version", "symfony/security-bundle": "self.version", "symfony/serializer": "self.version", "symfony/stopwatch": "self.version", @@ -68,7 +73,7 @@ "monolog/monolog": "~1.3", "propel/propel1": "1.6.*", "ircmaxell/password-compat": "1.0.*", - "ocramius/proxy-manager": ">=0.3.1,<0.4-dev" + "ocramius/proxy-manager": ">=0.3.1,<0.6-dev" }, "autoload": { "psr-0": { "Symfony\\": "src/" }, @@ -81,7 +86,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 823b03f7c0d72..66323e43d16bc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,7 @@ ./src/Symfony/Bridge/*/Tests/ ./src/Symfony/Component/*/Tests/ + ./src/Symfony/Component/*/*/Tests/ ./src/Symfony/Bundle/*/Tests/ @@ -37,6 +38,7 @@ ./src/Symfony/Bridge/*/Tests ./src/Symfony/Component/*/Tests + ./src/Symfony/Component/*/*/Tests ./src/Symfony/Bundle/*/Tests ./src/Symfony/Bundle/*/Resources ./src/Symfony/Component/*/Resources diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php index 331a465b2fdba..50cb74bb8650b 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php @@ -306,14 +306,30 @@ protected function detectMetadataDriver($dir, ContainerBuilder $container) */ protected function loadObjectManagerCacheDriver(array $objectManager, ContainerBuilder $container, $cacheName) { - $cacheDriver = $objectManager[$cacheName.'_driver']; - $cacheDriverService = $this->getObjectManagerElementName($objectManager['name'].'_'.$cacheName); + $this->loadCacheDriver($cacheName, $objectManager['name'], $objectManager[$cacheName.'_driver'], $container); + } + + /** + * Loads a cache driver. + * + * @param string $cacheDriverServiceId The cache driver name. + * @param string $objectManagerName The object manager name. + * @param array $cacheDriver The cache driver mapping. + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container The ContainerBuilder instance. + * + * @return string + * + * @throws \InvalidArgumentException + */ + protected function loadCacheDriver($cacheName, $objectManagerName, array $cacheDriver, ContainerBuilder $container) + { + $cacheDriverServiceId = $this->getObjectManagerElementName($objectManagerName . '_' . $cacheName); switch ($cacheDriver['type']) { case 'service': - $container->setAlias($cacheDriverService, new Alias($cacheDriver['id'], false)); + $container->setAlias($cacheDriverServiceId, new Alias($cacheDriver['id'], false)); - return; + return $cacheDriverServiceId; case 'memcache': $memcacheClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.memcache.class').'%'; $memcacheInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.memcache_instance.class').'%'; @@ -324,8 +340,8 @@ protected function loadObjectManagerCacheDriver(array $objectManager, ContainerB $memcacheInstance->addMethodCall('connect', array( $memcacheHost, $memcachePort )); - $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_memcache_instance', $objectManager['name'])), $memcacheInstance); - $cacheDef->addMethodCall('setMemcache', array(new Reference($this->getObjectManagerElementName(sprintf('%s_memcache_instance', $objectManager['name']))))); + $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_memcache_instance', $objectManagerName)), $memcacheInstance); + $cacheDef->addMethodCall('setMemcache', array(new Reference($this->getObjectManagerElementName(sprintf('%s_memcache_instance', $objectManagerName))))); break; case 'memcached': $memcachedClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.memcached.class').'%'; @@ -337,8 +353,8 @@ protected function loadObjectManagerCacheDriver(array $objectManager, ContainerB $memcachedInstance->addMethodCall('addServer', array( $memcachedHost, $memcachedPort )); - $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManager['name'])), $memcachedInstance); - $cacheDef->addMethodCall('setMemcached', array(new Reference($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManager['name']))))); + $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName)), $memcachedInstance); + $cacheDef->addMethodCall('setMemcached', array(new Reference($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName))))); break; case 'redis': $redisClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.redis.class').'%'; @@ -350,8 +366,8 @@ protected function loadObjectManagerCacheDriver(array $objectManager, ContainerB $redisInstance->addMethodCall('connect', array( $redisHost, $redisPort )); - $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManager['name'])), $redisInstance); - $cacheDef->addMethodCall('setRedis', array(new Reference($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManager['name']))))); + $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManagerName)), $redisInstance); + $cacheDef->addMethodCall('setRedis', array(new Reference($this->getObjectManagerElementName(sprintf('%s_redis_instance', $objectManagerName))))); break; case 'apc': case 'array': @@ -365,11 +381,21 @@ protected function loadObjectManagerCacheDriver(array $objectManager, ContainerB } $cacheDef->setPublic(false); - // generate a unique namespace for the given application - $namespace = 'sf2'.$this->getMappingResourceExtension().'_'.$objectManager['name'].'_'.md5($container->getParameter('kernel.root_dir').$container->getParameter('kernel.environment')); - $cacheDef->addMethodCall('setNamespace', array($namespace)); - $container->setDefinition($cacheDriverService, $cacheDef); + if (!isset($cacheDriver['namespace'])) { + // generate a unique namespace for the given application + $env = $container->getParameter('kernel.root_dir').$container->getParameter('kernel.environment'); + $hash = hash('sha256', $env); + $namespace = 'sf2'.$this->getMappingResourceExtension().'_'.$objectManagerName.'_'.$hash; + + $cacheDriver['namespace'] = $namespace; + } + + $cacheDef->addMethodCall('setNamespace', array($cacheDriver['namespace'])); + + $container->setDefinition($cacheDriverServiceId, $cacheDef); + + return $cacheDriverServiceId; } /** diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php index 4e871e563a149..24688d1c1a4e0 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php @@ -53,6 +53,13 @@ public function process(ContainerBuilder $container) return; } + $taggedSubscribers = $container->findTaggedServiceIds($this->tagPrefix.'.event_subscriber'); + $taggedListeners = $container->findTaggedServiceIds($this->tagPrefix.'.event_listener'); + + if (empty($taggedSubscribers) && empty($taggedListeners)) { + return; + } + $this->container = $container; $this->connections = $container->getParameter($this->connections); $sortFunc = function ($a, $b) { @@ -62,26 +69,31 @@ public function process(ContainerBuilder $container) return $a > $b ? -1 : 1; }; - $subscribersPerCon = $this->groupByConnection($container->findTaggedServiceIds($this->tagPrefix.'.event_subscriber')); - foreach ($subscribersPerCon as $con => $subscribers) { - $em = $this->getEventManager($con); - uasort($subscribers, $sortFunc); - foreach ($subscribers as $id => $instance) { - $em->addMethodCall('addEventSubscriber', array(new Reference($id))); + if (!empty($taggedSubscribers)) { + $subscribersPerCon = $this->groupByConnection($taggedSubscribers); + foreach ($subscribersPerCon as $con => $subscribers) { + $em = $this->getEventManager($con); + + uasort($subscribers, $sortFunc); + foreach ($subscribers as $id => $instance) { + $em->addMethodCall('addEventSubscriber', array(new Reference($id))); + } } } - $listenersPerCon = $this->groupByConnection($container->findTaggedServiceIds($this->tagPrefix.'.event_listener'), true); - foreach ($listenersPerCon as $con => $listeners) { - $em = $this->getEventManager($con); - - uasort($listeners, $sortFunc); - foreach ($listeners as $id => $instance) { - $em->addMethodCall('addEventListener', array( - array_unique($instance['event']), - isset($instance['lazy']) && $instance['lazy'] ? $id : new Reference($id), - )); + if (!empty($taggedListeners)) { + $listenersPerCon = $this->groupByConnection($taggedListeners, true); + foreach ($listenersPerCon as $con => $listeners) { + $em = $this->getEventManager($con); + + uasort($listeners, $sortFunc); + foreach ($listeners as $id => $instance) { + $em->addMethodCall('addEventListener', array( + array_unique($instance['event']), + isset($instance['lazy']) && $instance['lazy'] ? $id : new Reference($id), + )); + } } } } diff --git a/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php b/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php new file mode 100644 index 0000000000000..0ff25ee3e5882 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.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\Bridge\Doctrine\ExpressionLanguage; + +use Doctrine\Common\Cache\Cache; +use Symfony\Component\ExpressionLanguage\ParsedExpression; +use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface; + +/** + * @author Adrien Brault + */ +class DoctrineParserCache implements ParserCacheInterface +{ + /** + * @var Cache + */ + private $cache; + + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + /** + * {@inheritdoc} + */ + public function fetch($key) + { + if (false === $value = $this->cache->fetch($key)) { + return null; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function save($key, ParsedExpression $expression) + { + $this->cache->save($key, $expression); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 61f1a4c1a22c0..9719462f1977a 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -294,6 +294,8 @@ public function getValuesForChoices(array $entities) * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForChoices(array $entities) { @@ -334,6 +336,8 @@ public function getIndicesForChoices(array $entities) * @return array * * @see Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForValues(array $values) { diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index 5a2b42324836c..9a7a1f8774538 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -25,12 +25,11 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface { protected $registry; - private $cache; + private $cache = array(); public function __construct(ManagerRegistry $registry) { $this->registry = $registry; - $this->cache = array(); } /** @@ -91,7 +90,7 @@ public function guessRequired($class, $property) return null; } - /* @var ClassMetadataInfo $classMetadata */ + /** @var ClassMetadataInfo $classMetadata */ $classMetadata = $classMetadatas[0]; // Check whether the field exists and is nullable or not diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 047cf12c5ef69..af127d42fae2b 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -116,7 +116,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) ? spl_object_hash($options['group_by']) : $options['group_by']; - $hash = md5(json_encode(array( + $hash = hash('sha256', json_encode(array( spl_object_hash($options['em']), $options['class'], $propertyHash, diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index 999f469ff4cc2..e786a8aeb5a64 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -47,7 +47,7 @@ class DoctrineTokenProvider implements TokenProviderInterface private $conn; /** - * new DoctrineTokenProvider for the RemembeMe authentication service + * new DoctrineTokenProvider for the RememberMe authentication service * * @param \Doctrine\DBAL\Connection $conn */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php index b5985caed47f9..5185a75998d66 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php @@ -18,10 +18,6 @@ class ContainerAwareEventManagerTest extends \PHPUnit_Framework_TestCase { protected function setUp() { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - $this->container = new Container(); $this->evm = new ContainerAwareEventManager($this->container); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php index 067935608e50d..2772461c2eb42 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php @@ -18,17 +18,6 @@ class DoctrineDataCollectorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Doctrine\DBAL\Platforms\MySqlPlatform')) { - $this->markTestSkipped('Doctrine DBAL is not available.'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testCollectConnections() { $c = $this->createCollector(array()); diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php index 52d653bdd9681..cc6f47ca1c6c7 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php @@ -16,17 +16,6 @@ class ContainerAwareLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - - if (!class_exists('Doctrine\Common\DataFixtures\Loader')) { - $this->markTestSkipped('Doctrine Data Fixtures is not available.'); - } - } - public function testShouldSetContainerOnContainerAwareFixture() { $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php index e096de2e4b3b3..424f448c5e777 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php @@ -16,13 +16,6 @@ class RegisterEventListenersAndSubscribersPassTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - } - public function testProcessEventListenersWithPriorities() { $container = $this->createBuilder(); @@ -109,6 +102,17 @@ public function testProcessEventSubscribersWithPriorities() $this->assertEquals(array('c', 'd', 'e', 'b', 'a'), $this->getServiceOrder($container, 'addEventSubscriber')); } + public function testProcessNoTaggedServices() + { + $container = $this->createBuilder(true); + + $this->process($container); + + $this->assertEquals(array(), $container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls()); + + $this->assertEquals(array(), $container->getDefinition('doctrine.dbal.second_connection.event_manager')->getMethodCalls()); + } + private function process(ContainerBuilder $container) { $pass = new RegisterEventListenersAndSubscribersPass('doctrine.connections', 'doctrine.dbal.%s_connection.event_manager', 'doctrine'); diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php new file mode 100644 index 0000000000000..b891f28c8b64c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.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\Doctrine\Tests\DependencyInjection; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + +/** + * @author Fabio B. Silva + */ +class DoctrineExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension + */ + private $extension; + + protected function setUp() + { + parent::setUp(); + + $this->extension = $this + ->getMockBuilder('Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension') + ->setMethods(array( + 'getMappingResourceConfigDirectory', + 'getObjectManagerElementName', + 'getMappingObjectDefaultName', + 'getMappingResourceExtension', + 'load', + )) + ->getMock() + ; + + $this->extension->expects($this->any()) + ->method('getObjectManagerElementName') + ->will($this->returnCallback(function ($name) { + return 'doctrine.orm.'.$name; + })); + } + + public function providerBasicDrivers() + { + return array( + array('doctrine.orm.cache.apc.class', array('type' => 'apc')), + array('doctrine.orm.cache.array.class', array('type' => 'array')), + array('doctrine.orm.cache.xcache.class', array('type' => 'xcache')), + array('doctrine.orm.cache.wincache.class', array('type' => 'wincache')), + array('doctrine.orm.cache.zenddata.class', array('type' => 'zenddata')), + array('doctrine.orm.cache.redis.class', array('type' => 'redis'), array('setRedis')), + array('doctrine.orm.cache.memcache.class', array('type' => 'memcache'), array('setMemcache')), + array('doctrine.orm.cache.memcached.class', array('type' => 'memcached'), array('setMemcached')), + ); + } + + /** + * @param string $class + * @param array $config + * + * @dataProvider providerBasicDrivers + */ + public function testLoadBasicCacheDriver($class, array $config, array $expectedCalls = array()) + { + $container = $this->createContainer(); + $cacheName = 'metadata_cache'; + $objectManager = array( + 'name' => 'default', + 'metadata_cache_driver' => $config + ); + + $this->invokeLoadCacheDriver($objectManager, $container, $cacheName); + + $this->assertTrue($container->hasDefinition('doctrine.orm.default_metadata_cache')); + + $definition = $container->getDefinition('doctrine.orm.default_metadata_cache'); + $defCalls = $definition->getMethodCalls(); + $expectedCalls[] = 'setNamespace'; + $actualCalls = array_map(function ($call) { + return $call[0]; + }, $defCalls); + + $this->assertFalse($definition->isPublic()); + $this->assertEquals("%$class%", $definition->getClass()); + + foreach (array_unique($expectedCalls) as $call) { + $this->assertContains($call, $actualCalls); + } + } + + public function testServiceCacheDriver() + { + $cacheName = 'metadata_cache'; + $container = $this->createContainer(); + $definition = new Definition('%doctrine.orm.cache.apc.class%'); + $objectManager = array( + 'name' => 'default', + 'metadata_cache_driver' => array( + 'type' => 'service', + 'id' => 'service_driver' + ) + ); + + $container->setDefinition('service_driver', $definition); + + $this->invokeLoadCacheDriver($objectManager, $container, $cacheName); + + $this->assertTrue($container->hasAlias('doctrine.orm.default_metadata_cache')); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage "unrecognized_type" is an unrecognized Doctrine cache driver. + */ + public function testUnrecognizedCacheDriverException() + { + $cacheName = 'metadata_cache'; + $container = $this->createContainer(); + $objectManager = array( + 'name' => 'default', + 'metadata_cache_driver' => array( + 'type' => 'unrecognized_type' + ) + ); + + $this->invokeLoadCacheDriver($objectManager, $container, $cacheName); + } + + protected function invokeLoadCacheDriver(array $objectManager, ContainerBuilder $container, $cacheName) + { + $method = new \ReflectionMethod($this->extension, 'loadObjectManagerCacheDriver'); + + $method->setAccessible(true); + + $method->invokeArgs($this->extension, array($objectManager, $container, $cacheName)); + } + + /** + * @param array $data + * + * @return \Symfony\Component\DependencyInjection\ContainerBuilder + */ + protected function createContainer(array $data = array()) + { + return new ContainerBuilder(new ParameterBag(array_merge(array( + 'kernel.bundles' => array('FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'), + 'kernel.cache_dir' => __DIR__, + 'kernel.debug' => false, + 'kernel.environment' => 'test', + 'kernel.name' => 'kernel', + 'kernel.root_dir' => __DIR__, + ), $data))); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/DoctrineOrmTestCase.php b/src/Symfony/Bridge/Doctrine/Tests/DoctrineOrmTestCase.php index 970c9973140ff..577d1985990a5 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DoctrineOrmTestCase.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DoctrineOrmTestCase.php @@ -21,21 +21,6 @@ */ abstract class DoctrineOrmTestCase extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Doctrine\Common\Version')) { - $this->markTestSkipped('Doctrine Common is not available.'); - } - - if (!class_exists('Doctrine\DBAL\Platforms\MySqlPlatform')) { - $this->markTestSkipped('Doctrine DBAL is not available.'); - } - - if (!class_exists('Doctrine\ORM\EntityManager')) { - $this->markTestSkipped('Doctrine ORM is not available.'); - } - } - /** * @return \Doctrine\ORM\EntityManager */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/ExpressionLanguage/DoctrineParserCacheTest.php b/src/Symfony/Bridge/Doctrine/Tests/ExpressionLanguage/DoctrineParserCacheTest.php new file mode 100644 index 0000000000000..a473b3ace2fe5 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/ExpressionLanguage/DoctrineParserCacheTest.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\Bridge\Doctrine\Tests\ExpressionLanguage; + +use Symfony\Bridge\Doctrine\ExpressionLanguage\DoctrineParserCache; + +class DoctrineParserCacheTest extends \PHPUnit_Framework_TestCase +{ + public function testFetch() + { + $doctrineCacheMock = $this->getMock('Doctrine\Common\Cache\Cache'); + $parserCache = new DoctrineParserCache($doctrineCacheMock); + + $doctrineCacheMock->expects($this->once()) + ->method('fetch') + ->will($this->returnValue('bar')); + + $result = $parserCache->fetch('foo'); + + $this->assertEquals('bar', $result); + } + + public function testFetchUnexisting() + { + $doctrineCacheMock = $this->getMock('Doctrine\Common\Cache\Cache'); + $parserCache = new DoctrineParserCache($doctrineCacheMock); + + $doctrineCacheMock + ->expects($this->once()) + ->method('fetch') + ->will($this->returnValue(false)); + + $this->assertNull($parserCache->fetch('')); + } + + public function testSave() + { + $doctrineCacheMock = $this->getMock('Doctrine\Common\Cache\Cache'); + $parserCache = new DoctrineParserCache($doctrineCacheMock); + + $expression = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParsedExpression') + ->disableOriginalConstructor() + ->getMock(); + + $doctrineCacheMock->expects($this->once()) + ->method('save') + ->with('foo', $expression); + + $parserCache->save('foo', $expression); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php index bdb40134905d1..2c5de735002f4 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php @@ -50,22 +50,6 @@ protected function getExtensions() protected function setUp() { - if (!class_exists('Symfony\Component\Form\Form')) { - $this->markTestSkipped('The "Form" component is not available'); - } - - if (!class_exists('Doctrine\DBAL\Platforms\MySqlPlatform')) { - $this->markTestSkipped('Doctrine DBAL is not available.'); - } - - if (!class_exists('Doctrine\Common\Version')) { - $this->markTestSkipped('Doctrine Common is not available.'); - } - - if (!class_exists('Doctrine\ORM\EntityManager')) { - $this->markTestSkipped('Doctrine ORM is not available.'); - } - $this->em = DoctrineOrmTestCase::createTestEntityManager(); parent::setUp(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/HttpFoundation/DbalSessionHandlerTest.php b/src/Symfony/Bridge/Doctrine/Tests/HttpFoundation/DbalSessionHandlerTest.php index 8e51f40f4701d..cb41a770dac95 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/HttpFoundation/DbalSessionHandlerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/HttpFoundation/DbalSessionHandlerTest.php @@ -20,13 +20,6 @@ */ class DbalSessionHandlerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testConstruct() { $this->connection = $this->getMock('Doctrine\DBAL\Driver\Connection'); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php index f791f97f01ff0..8c179cd31f246 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php @@ -18,15 +18,6 @@ class EntityUserProviderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Security\Core\SecurityContext')) { - $this->markTestSkipped('The "Security" component is not available'); - } - - parent::setUp(); - } - public function testRefreshUserGetsUserByPrimaryKey() { $em = DoctrineTestHelper::createTestEntityManager(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueValidatorTest.php index 9d0a75af5ec29..57eaf3295a2dd 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueValidatorTest.php @@ -26,19 +26,6 @@ class UniqueValidatorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - parent::setUp(); - - if (!class_exists('Symfony\Component\Security\Core\SecurityContext')) { - $this->markTestSkipped('The "Security" component is not available'); - } - - if (!class_exists('Symfony\Component\Validator\Constraint')) { - $this->markTestSkipped('The "Validator" component is not available'); - } - } - protected function createRegistryMock($entityManagerName, $em) { $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index e657db70d1bba..383df1ed370b1 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -25,6 +25,7 @@ "symfony/form": "~2.2", "symfony/http-kernel": "~2.2", "symfony/security": "~2.2", + "symfony/expression-language": "~2.2", "symfony/validator": "~2.2", "doctrine/data-fixtures": "1.0.*", "doctrine/dbal": "~2.2", @@ -44,7 +45,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 88ecedd5f6b5c..73239fa86b649 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +2.4.0 +----- + + * added ConsoleHandler and ConsoleFormatter which can be used to show log messages + in the console output depending on the verbosity settings + 2.1.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php new file mode 100644 index 0000000000000..ddf6308860073 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Formatter; + +use Monolog\Formatter\LineFormatter; +use Monolog\Logger; + +/** + * Formats incoming records for console output by coloring them depending on log level. + * + * @author Tobias Schultze + */ +class ConsoleFormatter extends LineFormatter +{ + const SIMPLE_FORMAT = "%start_tag%[%datetime%] %channel%.%level_name%:%end_tag% %message% %context% %extra%\n"; + + /** + * {@inheritdoc} + */ + public function format(array $record) + { + if ($record['level'] >= Logger::ERROR) { + $record['start_tag'] = ''; + $record['end_tag'] = ''; + } elseif ($record['level'] >= Logger::NOTICE) { + $record['start_tag'] = ''; + $record['end_tag'] = ''; + } elseif ($record['level'] >= Logger::INFO) { + $record['start_tag'] = ''; + $record['end_tag'] = ''; + } else { + $record['start_tag'] = ''; + $record['end_tag'] = ''; + } + + return parent::format($record); + } +} diff --git a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php index 81766d7a54ca8..25a207f0e5db9 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php @@ -14,7 +14,6 @@ use Monolog\Handler\ChromePHPHandler as BaseChromePhpHandler; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; /** * ChromePhpHandler. @@ -38,7 +37,7 @@ class ChromePhpHandler extends BaseChromePhpHandler */ public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } diff --git a/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php new file mode 100644 index 0000000000000..8a30a9c06ad7f --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Handler; + +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Writes logs to the console output depending on its verbosity setting. + * + * It is disabled by default and gets activated as soon as a command is executed. + * Instead of listening to the console events, the output can also be set manually. + * + * The minimum logging level at which this handler will be triggered depends on the + * verbosity setting of the console output. The default mapping is: + * - OutputInterface::VERBOSITY_NORMAL will show all WARNING and higher logs + * - OutputInterface::VERBOSITY_VERBOSE (-v) will show all NOTICE and higher logs + * - OutputInterface::VERBOSITY_VERY_VERBOSE (-vv) will show all INFO and higher logs + * - OutputInterface::VERBOSITY_DEBUG (-vvv) will show all DEBUG and higher logs, i.e. all logs + * + * This mapping can be customized with the $verbosityLevelMap constructor parameter. + * + * @author Tobias Schultze + */ +class ConsoleHandler extends AbstractProcessingHandler implements EventSubscriberInterface +{ + /** + * @var OutputInterface|null + */ + private $output; + + /** + * @var array + */ + private $verbosityLevelMap = array( + OutputInterface::VERBOSITY_NORMAL => Logger::WARNING, + OutputInterface::VERBOSITY_VERBOSE => Logger::NOTICE, + OutputInterface::VERBOSITY_VERY_VERBOSE => Logger::INFO, + OutputInterface::VERBOSITY_DEBUG => Logger::DEBUG + ); + + /** + * Constructor. + * + * @param OutputInterface|null $output The console output to use (the handler remains disabled when passing null + * until the output is set, e.g. by using console events) + * @param Boolean $bubble Whether the messages that are handled can bubble up the stack + * @param array $verbosityLevelMap Array that maps the OutputInterface verbosity to a minimum logging + * level (leave empty to use the default mapping) + */ + public function __construct(OutputInterface $output = null, $bubble = true, array $verbosityLevelMap = array()) + { + parent::__construct(Logger::DEBUG, $bubble); + $this->output = $output; + + if ($verbosityLevelMap) { + $this->verbosityLevelMap = $verbosityLevelMap; + } + } + + /** + * {@inheritdoc} + */ + public function isHandling(array $record) + { + return $this->updateLevel() && parent::isHandling($record); + } + + /** + * {@inheritdoc} + */ + public function handle(array $record) + { + // we have to update the logging level each time because the verbosity of the + // console output might have changed in the meantime (it is not immutable) + return $this->updateLevel() && parent::handle($record); + } + + /** + * Sets the console output to use for printing logs. + * + * @param OutputInterface $output The console output to use + */ + public function setOutput(OutputInterface $output) + { + $this->output = $output; + } + + /** + * Disables the output. + */ + public function close() + { + $this->output = null; + + parent::close(); + } + + /** + * Before a command is executed, the handler gets activated and the console output + * is set in order to know where to write the logs. + * + * @param ConsoleCommandEvent $event + */ + public function onCommand(ConsoleCommandEvent $event) + { + $this->setOutput($event->getOutput()); + } + + /** + * After a command has been executed, it disables the output. + * + * @param ConsoleTerminateEvent $event + */ + public function onTerminate(ConsoleTerminateEvent $event) + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + ConsoleEvents::COMMAND => 'onCommand', + ConsoleEvents::TERMINATE => 'onTerminate' + ); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record) + { + if ($record['level'] >= Logger::ERROR && $this->output instanceof ConsoleOutputInterface) { + $this->output->getErrorOutput()->write((string) $record['formatted']); + } else { + $this->output->write((string) $record['formatted']); + } + } + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter() + { + return new ConsoleFormatter(); + } + + /** + * Updates the logging level based on the verbosity setting of the console output. + * + * @return Boolean Whether the handler is enabled and verbosity is not set to quiet. + */ + private function updateLevel() + { + if (null === $this->output || OutputInterface::VERBOSITY_QUIET === $verbosity = $this->output->getVerbosity()) { + return false; + } + + if (isset($this->verbosityLevelMap[$verbosity])) { + $this->setLevel($this->verbosityLevelMap[$verbosity]); + } else { + $this->setLevel(Logger::DEBUG); + } + + return true; + } +} diff --git a/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php b/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php index f36cd9f5b044c..45344c12a6bc5 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php @@ -14,7 +14,6 @@ use Monolog\Handler\FirePHPHandler as BaseFirePHPHandler; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelInterface; /** * FirePHPHandler. @@ -38,7 +37,7 @@ class FirePHPHandler extends BaseFirePHPHandler */ public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } diff --git a/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php b/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php index d5ac5a5dc504b..e27c163ae300e 100644 --- a/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php @@ -13,7 +13,6 @@ use Monolog\Processor\WebProcessor as BaseWebProcessor; use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; /** * WebProcessor override to read from the HttpFoundation's Request @@ -30,7 +29,7 @@ public function __construct() public function onKernelRequest(GetResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) { + if ($event->isMasterRequest()) { $this->serverData = $event->getRequest()->server->all(); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php new file mode 100644 index 0000000000000..3266543e7dc09 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Handler; + +use Monolog\Logger; +use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Tests the ConsoleHandler and also the ConsoleFormatter. + * + * @author Tobias Schultze + */ +class ConsoleHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function testConstructor() + { + $handler = new ConsoleHandler(null, false); + $this->assertFalse($handler->getBubble(), 'the bubble parameter gets propagated'); + } + + public function testIsHandling() + { + $handler = new ConsoleHandler(); + $this->assertFalse($handler->isHandling(array()), '->isHandling returns false when no output is set'); + } + + /** + * @dataProvider provideVerbosityMappingTests + */ + public function testVerbosityMapping($verbosity, $level, $isHandling, array $map = array()) + { + $output = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + $output + ->expects($this->atLeastOnce()) + ->method('getVerbosity') + ->will($this->returnValue($verbosity)) + ; + $handler = new ConsoleHandler($output, true, $map); + $this->assertSame($isHandling, $handler->isHandling(array('level' => $level)), + '->isHandling returns correct value depending on console verbosity and log level' + ); + } + + public function provideVerbosityMappingTests() + { + return array( + array(OutputInterface::VERBOSITY_QUIET, Logger::ERROR, false), + array(OutputInterface::VERBOSITY_NORMAL, Logger::WARNING, true), + array(OutputInterface::VERBOSITY_NORMAL, Logger::NOTICE, false), + array(OutputInterface::VERBOSITY_VERBOSE, Logger::NOTICE, true), + array(OutputInterface::VERBOSITY_VERBOSE, Logger::INFO, false), + array(OutputInterface::VERBOSITY_VERY_VERBOSE, Logger::INFO, true), + array(OutputInterface::VERBOSITY_VERY_VERBOSE, Logger::DEBUG, false), + array(OutputInterface::VERBOSITY_DEBUG, Logger::DEBUG, true), + array(OutputInterface::VERBOSITY_DEBUG, Logger::EMERGENCY, true), + array(OutputInterface::VERBOSITY_NORMAL, Logger::NOTICE, true, array( + OutputInterface::VERBOSITY_NORMAL => Logger::NOTICE + )), + array(OutputInterface::VERBOSITY_DEBUG, Logger::NOTICE, true, array( + OutputInterface::VERBOSITY_NORMAL => Logger::NOTICE + )), + ); + } + + public function testVerbosityChanged() + { + $output = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + $output + ->expects($this->at(0)) + ->method('getVerbosity') + ->will($this->returnValue(OutputInterface::VERBOSITY_QUIET)) + ; + $output + ->expects($this->at(1)) + ->method('getVerbosity') + ->will($this->returnValue(OutputInterface::VERBOSITY_DEBUG)) + ; + $handler = new ConsoleHandler($output); + $this->assertFalse($handler->isHandling(array('level' => Logger::NOTICE)), + 'when verbosity is set to quiet, the handler does not handle the log' + ); + $this->assertTrue($handler->isHandling(array('level' => Logger::NOTICE)), + 'since the verbosity of the output increased externally, the handler is now handling the log' + ); + } + + public function testGetFormatter() + { + $handler = new ConsoleHandler(); + $this->assertInstanceOf('Symfony\Bridge\Monolog\Formatter\ConsoleFormatter', $handler->getFormatter(), + '-getFormatter returns ConsoleFormatter by default' + ); + } + + public function testWritingAndFormatting() + { + $output = $this->getMock('Symfony\Component\Console\Output\ConsoleOutputInterface'); + $output + ->expects($this->any()) + ->method('getVerbosity') + ->will($this->returnValue(OutputInterface::VERBOSITY_DEBUG)) + ; + $output + ->expects($this->once()) + ->method('write') + ->with('[2013-05-29 16:21:54] app.INFO: My info message [] []'."\n") + ; + + $errorOutput = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + $errorOutput + ->expects($this->once()) + ->method('write') + ->with('[2013-05-29 16:21:54] app.ERROR: My error message [] []'."\n") + ; + + $output + ->expects($this->any()) + ->method('getErrorOutput') + ->will($this->returnValue($errorOutput)) + ; + + $handler = new ConsoleHandler(null, false); + $handler->setOutput($output); + + $infoRecord = array( + 'message' => 'My info message', + 'context' => array(), + 'level' => Logger::INFO, + 'level_name' => Logger::getLevelName(Logger::INFO), + 'channel' => 'app', + 'datetime' => new \DateTime('2013-05-29 16:21:54'), + 'extra' => array(), + ); + + $this->assertTrue($handler->handle($infoRecord), 'The handler finished handling the log as bubble is false.'); + + $errorRecord = array( + 'message' => 'My error message', + 'context' => array(), + 'level' => Logger::ERROR, + 'level_name' => Logger::getLevelName(Logger::ERROR), + 'channel' => 'app', + 'datetime' => new \DateTime('2013-05-29 16:21:54'), + 'extra' => array(), + ); + + $this->assertTrue($handler->handle($errorRecord), 'The handler finished handling the log as bubble is false.'); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php index ae3379bfe06a8..307605031934c 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php @@ -14,17 +14,9 @@ use Monolog\Logger; use Symfony\Bridge\Monolog\Processor\WebProcessor; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\HttpKernelInterface; class WebProcessorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Monolog\\Logger')) { - $this->markTestSkipped('Monolog is not available.'); - } - } - public function testUsesRequestServerData() { $server = array( @@ -42,8 +34,8 @@ public function testUsesRequestServerData() ->disableOriginalConstructor() ->getMock(); $event->expects($this->any()) - ->method('getRequestType') - ->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST)); + ->method('isMasterRequest') + ->will($this->returnValue(true)); $event->expects($this->any()) ->method('getRequest') ->will($this->returnValue($request)); diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 643bd9229a080..067f1c70d71d7 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -17,9 +17,18 @@ ], "require": { "php": ">=5.3.3", - "symfony/http-kernel": "~2.2", "monolog/monolog": "~1.3" }, + "require-dev": { + "symfony/http-kernel": "~2.2", + "symfony/console": "~2.3", + "symfony/event-dispatcher": "~2.2" + }, + "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" + }, "autoload": { "psr-0": { "Symfony\\Bridge\\Monolog\\": "" } }, @@ -27,7 +36,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php b/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php index c3e6c5f6b1723..eaeeba960bfc6 100644 --- a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php +++ b/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php @@ -291,6 +291,8 @@ public function getValuesForChoices(array $models) /** * {@inheritdoc} + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForChoices(array $models) { @@ -335,6 +337,8 @@ public function getIndicesForChoices(array $models) /** * {@inheritdoc} + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForValues(array $values) { diff --git a/src/Symfony/Bridge/Propel1/Form/PropelTypeGuesser.php b/src/Symfony/Bridge/Propel1/Form/PropelTypeGuesser.php index b4e8ec9ebb6f7..35382d89c3d2c 100644 --- a/src/Symfony/Bridge/Propel1/Form/PropelTypeGuesser.php +++ b/src/Symfony/Bridge/Propel1/Form/PropelTypeGuesser.php @@ -176,5 +176,9 @@ protected function getColumn($class, $property) if ($table && $table->hasColumn($property)) { return $this->cache[$class.'::'.$property] = $table->getColumn($property); } + + if ($table && $table->hasColumnByInsensitiveCase($property)) { + return $this->cache[$class.'::'.$property] = $table->getColumnByInsensitiveCase($property); + } } } diff --git a/src/Symfony/Bridge/Propel1/Logger/PropelLogger.php b/src/Symfony/Bridge/Propel1/Logger/PropelLogger.php index 91c0061f4cae6..8dafbc961ccb2 100644 --- a/src/Symfony/Bridge/Propel1/Logger/PropelLogger.php +++ b/src/Symfony/Bridge/Propel1/Logger/PropelLogger.php @@ -20,7 +20,7 @@ * @author Fabien Potencier * @author William Durand */ -class PropelLogger +class PropelLogger implements \BasicLogger { /** * @var LoggerInterface @@ -30,14 +30,17 @@ class PropelLogger /** * @var array */ - protected $queries; + protected $queries = array(); /** * @var Stopwatch */ protected $stopwatch; - private $isPrepared; + /** + * @var Boolean + */ + private $isPrepared = false; /** * Constructor. @@ -48,87 +51,59 @@ class PropelLogger public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch = null) { $this->logger = $logger; - $this->queries = array(); $this->stopwatch = $stopwatch; - $this->isPrepared = false; } /** - * A convenience function for logging an alert event. - * - * @param mixed $message the message to log. + * {@inheritDoc} */ public function alert($message) { - if (null !== $this->logger) { - $this->logger->alert($message); - } + $this->log($message, 'alert'); } /** - * A convenience function for logging a critical event. - * - * @param mixed $message the message to log. + * {@inheritDoc} */ public function crit($message) { - if (null !== $this->logger) { - $this->logger->critical($message); - } + $this->log($message, 'crit'); } /** - * A convenience function for logging an error event. - * - * @param mixed $message the message to log. + * {@inheritDoc} */ public function err($message) { - if (null !== $this->logger) { - $this->logger->error($message); - } + $this->log($message, 'err'); } /** - * A convenience function for logging a warning event. - * - * @param mixed $message the message to log. + * {@inheritDoc} */ public function warning($message) { - if (null !== $this->logger) { - $this->logger->warning($message); - } + $this->log($message, 'warning'); } /** - * A convenience function for logging an critical event. - * - * @param mixed $message the message to log. + * {@inheritDoc} */ public function notice($message) { - if (null !== $this->logger) { - $this->logger->notice($message); - } + $this->log($message, 'notice'); } /** - * A convenience function for logging an critical event. - * - * @param mixed $message the message to log. + * {@inheritDoc} */ public function info($message) { - if (null !== $this->logger) { - $this->logger->info($message); - } + $this->log($message, 'info'); } /** - * A convenience function for logging a debug event. - * - * @param mixed $message the message to log. + * {@inheritDoc} */ public function debug($message) { @@ -152,8 +127,40 @@ public function debug($message) if ($add) { $this->queries[] = $message; - if (null !== $this->logger) { - $this->logger->debug($message); + $this->log($message, 'debug'); + } + } + + /** + * {@inheritDoc} + */ + public function log($message, $severity = null) + { + if (null !== $this->logger) { + $message = is_string($message) ? $message : var_export($message, true); + + switch ($severity) { + case 'alert': + $this->logger->alert($message); + break; + case 'crit': + $this->logger->critical($message); + break; + case 'err': + $this->logger->error($message); + break; + case 'warning': + $this->logger->warning($message); + break; + case 'notice': + $this->logger->notice($message); + break; + case 'info': + $this->logger->info($message); + break; + case 'debug': + default: + $this->logger->debug($message); } } } diff --git a/src/Symfony/Bridge/Propel1/Tests/DataCollector/PropelDataCollectorTest.php b/src/Symfony/Bridge/Propel1/Tests/DataCollector/PropelDataCollectorTest.php index 23d6fc9fc6a5d..4b54694f74c8d 100644 --- a/src/Symfony/Bridge/Propel1/Tests/DataCollector/PropelDataCollectorTest.php +++ b/src/Symfony/Bridge/Propel1/Tests/DataCollector/PropelDataCollectorTest.php @@ -18,13 +18,6 @@ class PropelDataCollectorTest extends Propel1TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testCollectWithoutData() { $c = $this->createCollector(array()); diff --git a/src/Symfony/Bridge/Propel1/Tests/Fixtures/ItemQuery.php b/src/Symfony/Bridge/Propel1/Tests/Fixtures/ItemQuery.php index 56a0c943871c2..3e1aecc58d26c 100644 --- a/src/Symfony/Bridge/Propel1/Tests/Fixtures/ItemQuery.php +++ b/src/Symfony/Bridge/Propel1/Tests/Fixtures/ItemQuery.php @@ -22,6 +22,11 @@ class ItemQuery 'updated_at' => \PropelColumnTypes::TIMESTAMP, ); + private $caseInsensitiveMap = array( + 'isactive' => 'is_active', + 'updatedat' => 'updated_at', + ); + public static $result = array(); public function find() @@ -70,6 +75,30 @@ public function getColumn($column) return null; } + /** + * Method from the TableMap API + */ + public function hasColumnByInsensitiveCase($column) + { + $column = strtolower($column); + + return in_array($column, array_keys($this->caseInsensitiveMap)); + } + + /** + * Method from the TableMap API + */ + public function getColumnByInsensitiveCase($column) + { + $column = strtolower($column); + + if (isset($this->caseInsensitiveMap[$column])) { + return $this->getColumn($this->caseInsensitiveMap[$column]); + } + + return null; + } + /** * Method from the TableMap API */ diff --git a/src/Symfony/Bridge/Propel1/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php index 13a9f7ffd6635..529e850284204 100644 --- a/src/Symfony/Bridge/Propel1/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php +++ b/src/Symfony/Bridge/Propel1/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php @@ -21,12 +21,6 @@ class CollectionToArrayTransformerTest extends Propel1TestCase protected function setUp() { - if (!class_exists('Symfony\Component\Form\Form')) { - $this->markTestSkipped('The "Form" component is not available'); - } - - parent::setUp(); - $this->transformer = new CollectionToArrayTransformer(); } diff --git a/src/Symfony/Bridge/Propel1/Tests/Form/PropelTypeGuesserTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/PropelTypeGuesserTest.php index 3762a576338b5..bb59503e61c63 100644 --- a/src/Symfony/Bridge/Propel1/Tests/Form/PropelTypeGuesserTest.php +++ b/src/Symfony/Bridge/Propel1/Tests/Form/PropelTypeGuesserTest.php @@ -121,6 +121,9 @@ public static function dataProviderForGuessType() array('price', 'number', Guess::MEDIUM_CONFIDENCE), array('updated_at', 'datetime', Guess::HIGH_CONFIDENCE), + array('isActive', 'checkbox', Guess::HIGH_CONFIDENCE), + array('updatedAt', 'datetime', Guess::HIGH_CONFIDENCE), + array('Authors', 'model', Guess::HIGH_CONFIDENCE, true), array('Resellers', 'model', Guess::HIGH_CONFIDENCE, true), array('MainAuthor', 'model', Guess::HIGH_CONFIDENCE, false), diff --git a/src/Symfony/Bridge/Propel1/Tests/Propel1TestCase.php b/src/Symfony/Bridge/Propel1/Tests/Propel1TestCase.php index 3a03391aaed5e..8a55069665940 100644 --- a/src/Symfony/Bridge/Propel1/Tests/Propel1TestCase.php +++ b/src/Symfony/Bridge/Propel1/Tests/Propel1TestCase.php @@ -13,10 +13,4 @@ abstract class Propel1TestCase extends \PHPUnit_Framework_TestCase { - public static function setUpBeforeClass() - { - if (!class_exists('\Propel')) { - self::markTestSkipped('Propel is not available.'); - } - } } diff --git a/src/Symfony/Bridge/Propel1/composer.json b/src/Symfony/Bridge/Propel1/composer.json index 3f64580182fee..6aa820d6b34e4 100644 --- a/src/Symfony/Bridge/Propel1/composer.json +++ b/src/Symfony/Bridge/Propel1/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php index aba65e85f1993..b352f3fb9c2b6 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service.php @@ -39,7 +39,7 @@ public function __construct() * This service is shared. * This method always returns the same instance of the service. * - * @param boolean $lazyLoad whether to try lazy-loading the service with a proxy + * @param Boolean $lazyLoad whether to try lazy-loading the service with a proxy * * @return stdClass A stdClass instance. */ @@ -109,6 +109,8 @@ public function __set($name, $value) /** * @param string $name + * + * @return Boolean */ public function __isset($name) { diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt index 6bc1c6883b7da..332370c87eb11 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt @@ -23,5 +23,5 @@ class ProjectServiceContainer extends Container } } -class stdClass_%s extends \stdClass implements \ProxyManager\Proxy\LazyLoadingInterface, \ProxyManager\Proxy\ValueHolderInterface +class stdClass_%s extends \stdClass implements \ProxyManager\Proxy\VirtualProxyInterface {%a}%A \ No newline at end of file diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 4d3abb604815e..04cc581ac3817 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=5.3.3", "symfony/dependency-injection": "~2.3", - "ocramius/proxy-manager": ">=0.3.1,<0.4-dev" + "ocramius/proxy-manager": ">=0.3.1,<0.6-dev" }, "autoload": { "psr-0": { @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php b/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php index 3ae96a276f090..36283496f6cbc 100644 --- a/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php +++ b/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php @@ -21,6 +21,9 @@ * * @author Fabien Potencier * @author Clément JOBEILI + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. Use + * MessageDataCollector of SwiftmailerBundle instead. */ class MessageDataCollector extends DataCollector { diff --git a/src/Symfony/Bridge/Swiftmailer/composer.json b/src/Symfony/Bridge/Swiftmailer/composer.json index 9e6c90b7bdaf6..8e5277692e706 100644 --- a/src/Symfony/Bridge/Swiftmailer/composer.json +++ b/src/Symfony/Bridge/Swiftmailer/composer.json @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index ad22216e40b87..4be010ba20e56 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +2.5.0 +----- + + * moved command `twig:lint` from `TwigBundle` + +2.4.0 +----- + + * added stopwatch tag to time templates with the WebProfilerBundle + 2.3.0 ----- diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php new file mode 100644 index 0000000000000..6636f6cce0e82 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Command; + +if (!defined('JSON_PRETTY_PRINT')) { + define('JSON_PRETTY_PRINT', 128); +} + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Finder\Finder; + +/** + * Command that will validate your template syntax and output encountered errors. + * + * @author Marc Weistroff + * @author Jérôme Tamarelle + */ +class LintCommand extends Command +{ + private $twig; + + /** + * {@inheritDoc} + */ + public function __construct($name = 'twig:lint') + { + parent::__construct($name); + } + + /** + * Sets the twig environment + * + * @param \Twig_Environment $twig + */ + public function setTwigEnvironment(\Twig_Environment $twig) + { + $this->twig = $twig; + } + + /** + * @return \Twig_Environment $twig + */ + protected function getTwigEnvironment() + { + return $this->twig; + } + + protected function configure() + { + $this + ->setDescription('Lints a template and outputs encountered errors') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->addArgument('filename') + ->setHelp(<<%command.name% command lints a template and outputs to STDOUT +the first encountered syntax error. + +You can validate the syntax of a file: + +php %command.full_name% filename + +Or of a whole directory: + +php %command.full_name% dirname +php %command.full_name% dirname --format=json + +You can also pass the template contents from STDIN: + +cat filename | php %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $twig = $this->getTwigEnvironment(); + $filename = $input->getArgument('filename'); + + if (!$filename) { + if (0 !== ftell(STDIN)) { + throw new \RuntimeException("Please provide a filename or pipe template content to STDIN."); + } + + $template = ''; + while (!feof(STDIN)) { + $template .= fread(STDIN, 1024); + } + + return $this->display($input, $output, array($this->validate($twig, $template))); + } + + $filesInfo = array(); + foreach ($this->findFiles($filename) as $file) { + $filesInfo[] = $this->validate($twig, file_get_contents($file), $file); + } + + return $this->display($input, $output, $filesInfo); + } + + protected function findFiles($filename) + { + if (is_file($filename)) { + return array($filename); + } elseif (is_dir($filename)) { + return Finder::create()->files()->in($filename)->name('*.twig'); + } + + throw new \RuntimeException(sprintf('File or directory "%s" is not readable', $filename)); + } + + private function validate(\Twig_Environment $twig, $template, $file = null) + { + try { + $twig->parse($twig->tokenize($template, $file ? (string) $file : null)); + } catch (\Twig_Error $e) { + return array('template' => $template, 'file' => $file, 'valid' => false, 'exception' => $e); + } + + return array('template' => $template, 'file' => $file, 'valid' => true); + } + + private function display(InputInterface $input, OutputInterface $output, $files) + { + switch ($input->getOption('format')) { + case 'txt': + return $this->displayTxt($output, $files); + case 'json': + return $this->displayJson($output, $files); + default: + throw new \InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); + } + } + + private function displayTxt(OutputInterface $output, $filesInfo) + { + $errors = 0; + + foreach ($filesInfo as $info) { + if ($info['valid'] && $output->isVerbose()) { + $output->writeln('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); + } elseif (!$info['valid']) { + $errors++; + $this->renderException($output, $info['template'], $info['exception'], $info['file']); + } + } + + $output->writeln(sprintf('%d/%d valid files', count($filesInfo) - $errors, count($filesInfo))); + + return min($errors, 1); + } + + private function displayJson(OutputInterface $output, $filesInfo) + { + $errors = 0; + + array_walk($filesInfo, function (&$v) use (&$errors) { + $v['file'] = (string) $v['file']; + unset($v['template']); + if (!$v['valid']) { + $v['message'] = $v['exception']->getMessage(); + unset($v['exception']); + $errors++; + } + }); + + $output->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT)); + + return min($errors, 1); + } + + private function renderException(OutputInterface $output, $template, \Twig_Error $exception, $file = null) + { + $line = $exception->getTemplateLine(); + + if ($file) { + $output->writeln(sprintf("KO in %s (line %s)", $file, $line)); + } else { + $output->writeln(sprintf("KO (line %s)", $line)); + } + + foreach ($this->getContext($template, $line) as $no => $code) { + $output->writeln(sprintf( + "%s %-6s %s", + $no == $line ? '>>' : ' ', + $no, + $code + )); + if ($no == $line) { + $output->writeln(sprintf('>> %s ', $exception->getRawMessage())); + } + } + } + + private function getContext($template, $line, $context = 3) + { + $lines = explode("\n", $template); + + $position = max(0, $line - $context); + $max = min(count($lines), $line - 1 + $context); + + $result = array(); + while ($position < $max) { + $result[$position + 1] = $lines[$position]; + $position++; + } + + return $result; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php b/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php new file mode 100644 index 0000000000000..6b30a279419b7 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Symfony\Component\ExpressionLanguage\Expression; + +/** + * ExpressionExtension gives a way to create Expressions from a template. + * + * @author Fabien Potencier + */ +class ExpressionExtension extends \Twig_Extension +{ + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('expression', array($this, 'createExpression')), + ); + } + + public function createExpression($expression) + { + return new Expression($expression); + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'expression'; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php new file mode 100644 index 0000000000000..1055b5bac8db8 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.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\Bridge\Twig\Extension; + +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Bridge\Twig\TokenParser\StopwatchTokenParser; + +/** + * Twig extension for the stopwatch helper. + * + * @author Wouter J + */ +class StopwatchExtension extends \Twig_Extension +{ + private $stopwatch; + + public function __construct(Stopwatch $stopwatch = null) + { + $this->stopwatch = $stopwatch; + } + + public function getStopwatch() + { + return $this->stopwatch; + } + + public function getTokenParsers() + { + return array( + /* + * {% stopwatch foo %} + * Some stuff which will be recorded on the timeline + * {% endstopwatch %} + */ + new StopwatchTokenParser($this->stopwatch !== null), + ); + } + + public function getName() + { + return 'stopwatch'; + } +} diff --git a/src/Symfony/Bridge/Twig/Form/TwigRenderer.php b/src/Symfony/Bridge/Twig/Form/TwigRenderer.php index 72798d103fa1f..3b47c26ea624b 100644 --- a/src/Symfony/Bridge/Twig/Form/TwigRenderer.php +++ b/src/Symfony/Bridge/Twig/Form/TwigRenderer.php @@ -11,8 +11,11 @@ namespace Symfony\Bridge\Twig\Form; -use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; /** * @author Bernhard Schussek @@ -24,9 +27,15 @@ class TwigRenderer extends FormRenderer implements TwigRendererInterface */ private $engine; - public function __construct(TwigRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null) + public function __construct(TwigRendererEngineInterface $engine, $csrfTokenManager = null) { - parent::__construct($engine, $csrfProvider); + if ($csrfTokenManager instanceof CsrfProviderInterface) { + $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); + } elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) { + throw new UnexpectedTypeException($csrfTokenManager, 'CsrfProviderInterface or CsrfTokenManagerInterface'); + } + + parent::__construct($engine, $csrfTokenManager); $this->engine = $engine; } diff --git a/src/Symfony/Bridge/Twig/Node/StopwatchNode.php b/src/Symfony/Bridge/Twig/Node/StopwatchNode.php new file mode 100644 index 0000000000000..cc12abd05d06e --- /dev/null +++ b/src/Symfony/Bridge/Twig/Node/StopwatchNode.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\Bridge\Twig\Node; + +/** + * Represents a stopwatch node. + * + * @author Wouter J + */ +class StopwatchNode extends \Twig_Node +{ + public function __construct(\Twig_NodeInterface $name, $body, \Twig_Node_Expression_AssignName $var, $lineno = 0, $tag = null) + { + parent::__construct(array('body' => $body, 'name' => $name, 'var' => $var), array(), $lineno, $tag); + } + + public function compile(\Twig_Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('var')) + ->raw(' = ') + ->subcompile($this->getNode('name')) + ->write(";\n") + ->write("\$this->env->getExtension('stopwatch')->getStopwatch()->start(") + ->subcompile($this->getNode('var')) + ->raw(", 'template');\n") + ->subcompile($this->getNode('body')) + ->write("\$this->env->getExtension('stopwatch')->getStopwatch()->stop(") + ->subcompile($this->getNode('var')) + ->raw(");\n") + ; + } +} diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php index ce27b6a6b209d..12ad604d35076 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php @@ -29,12 +29,12 @@ class Scope /** * @var array */ - private $data; + private $data = array(); /** * @var boolean */ - private $left; + private $left = false; /** * @param Scope $parent @@ -42,8 +42,6 @@ class Scope public function __construct(Scope $parent = null) { $this->parent = $parent; - $this->left = false; - $this->data = array(); } /** diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index e91d44e155bfc..7f5e323975478 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -317,7 +317,7 @@ {% else %} {% set form_method = "POST" %} {% endif %} -
+ {% if form_method != method %} {% endif %} @@ -373,21 +373,53 @@ {% block widget_attributes %} {% spaceless %} - id="{{ id }}" name="{{ full_name }}"{% if read_only %} readonly="readonly"{% endif %}{% if disabled %} disabled="disabled"{% endif %}{% if required %} required="required"{% endif %}{% if max_length %} maxlength="{{ max_length }}"{% endif %}{% if pattern %} pattern="{{ pattern }}"{% endif %} - {% for attrname, attrvalue in attr %}{% if attrname in ['placeholder', 'title'] %}{{ attrname }}="{{ attrvalue|trans({}, translation_domain) }}" {% else %}{{ attrname }}="{{ attrvalue }}" {% endif %}{% endfor %} + id="{{ id }}" name="{{ full_name }}" + {%- if read_only %} readonly="readonly"{% endif -%} + {%- if disabled %} disabled="disabled"{% endif -%} + {%- if required %} required="required"{% endif -%} + {%- if max_length %} maxlength="{{ max_length }}"{% endif -%} + {%- if pattern %} pattern="{{ pattern }}"{% endif -%} + {%- for attrname, attrvalue in attr -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}" + {%- elseif attrvalue is sameas(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not sameas(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} {% endspaceless %} {% endblock widget_attributes %} {% block widget_container_attributes %} {% spaceless %} - {% if id is not empty %}id="{{ id }}" {% endif %} - {% for attrname, attrvalue in attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %} + {%- if id is not empty %}id="{{ id }}"{% endif -%} + {%- for attrname, attrvalue in attr -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}" + {%- elseif attrvalue is sameas(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not sameas(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} {% endspaceless %} {% endblock widget_container_attributes %} {% block button_attributes %} {% spaceless %} - id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif %} - {% for attrname, attrvalue in attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %} + id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif -%} + {%- for attrname, attrvalue in attr -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}" + {%- elseif attrvalue is sameas(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not sameas(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} {% endspaceless %} {% endblock button_attributes %} diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php new file mode 100644 index 0000000000000..3fe54cbabf547 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Command; + +use Symfony\Bridge\Twig\Command\LintCommand; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * @covers \Symfony\Bridge\Twig\Command\LintCommand + */ +class LintCommandTest extends \PHPUnit_Framework_TestCase +{ + private $files; + + public function testLintCorrectFile() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('{{ foo }}'); + + $ret = $tester->execute(array('filename' => $filename), array('verbosity' => OutputInterface::VERBOSITY_VERBOSE)); + + $this->assertEquals(0, $ret, 'Returns 0 in case of success'); + $this->assertRegExp('/^OK in /', $tester->getDisplay()); + } + + public function testLintIncorrectFile() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('{{ foo'); + + $ret = $tester->execute(array('filename' => $filename)); + + $this->assertEquals(1, $ret, 'Returns 1 in case of error'); + $this->assertRegExp('/^KO in /', $tester->getDisplay()); + } + + /** + * @expectedException \RuntimeException + */ + public function testLintFileNotReadable() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile(''); + unlink($filename); + + $ret = $tester->execute(array('filename' => $filename)); + } + + /** + * @return CommandTester + */ + private function createCommandTester() + { + $twig = new \Twig_Environment(new \Twig_Loader_Filesystem()); + + $command = new LintCommand(); + $command->setTwigEnvironment($twig); + + $application = new Application(); + $application->add($command); + $command = $application->find('twig:lint'); + + return new CommandTester($command); + } + + /** + * @return string Path to the new file + */ + private function createFile($content) + { + $filename = tempnam(sys_get_temp_dir(), 'sf-'); + file_put_contents($filename, $content); + + $this->files[] = $filename; + + return $filename; + } + + public function setUp() + { + $this->files = array(); + } + + public function tearDown() + { + foreach ($this->files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/ExpressionExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/ExpressionExtensionTest.php new file mode 100644 index 0000000000000..749133c65c5a4 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/ExpressionExtensionTest.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\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\ExpressionExtension; +use Symfony\Component\ExpressionLanguage\Expression; + +class ExpressionExtensionTest extends \PHPUnit_Framework_TestCase +{ + protected $helper; + + public function testExpressionCreation() + { + $template = "{{ expression('1 == 1') }}"; + $twig = new \Twig_Environment(new \Twig_Loader_String(), array('debug' => true, 'cache' => false, 'autoescape' => true, 'optimizations' => 0)); + $twig->addExtension(new ExpressionExtension()); + + $output = $twig->render($template); + $this->assertEquals('1 == 1', $output); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php index 36c61cd6cca08..5a63537a2fa57 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php @@ -11,20 +11,13 @@ namespace Symfony\Bridge\Twig\Tests\Extension\Fixtures; -// Preventing autoloader throwing E_FATAL when Twig is now available -if (!class_exists('Twig_Environment')) { - class StubFilesystemLoader +class StubFilesystemLoader extends \Twig_Loader_Filesystem +{ + protected function findTemplate($name) { - } -} else { - class StubFilesystemLoader extends \Twig_Loader_Filesystem - { - protected function findTemplate($name) - { - // strip away bundle name - $parts = explode(':', $name); + // strip away bundle name + $parts = explode(':', $name); - return parent::findTemplate(end($parts)); - } + return parent::findTemplate(end($parts)); } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index 5eb67ea730182..47e7deec3245c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -30,22 +30,6 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest protected function setUp() { - if (!class_exists('Symfony\Component\Locale\Locale')) { - $this->markTestSkipped('The "Locale" component is not available'); - } - - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\Form\Form')) { - $this->markTestSkipped('The "Form" component is not available'); - } - - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - parent::setUp(); $rendererEngine = new TwigRendererEngine(array( diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php index 99a782178a9a4..b5a08e47a57dd 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php @@ -29,22 +29,6 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest protected function setUp() { - if (!class_exists('Symfony\Component\Locale\Locale')) { - $this->markTestSkipped('The "Locale" component is not available'); - } - - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\Form\Form')) { - $this->markTestSkipped('The "Form" component is not available'); - } - - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - parent::setUp(); $rendererEngine = new TwigRendererEngine(array( diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index 077927cd6d5fc..48bebdc13f8f5 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -12,37 +12,41 @@ namespace Symfony\Bridge\Twig\Tests\Extension; use Symfony\Bridge\Twig\Extension\HttpKernelExtension; -use Symfony\Bridge\Twig\Tests\TestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; -class HttpKernelExtensionTest extends TestCase +class HttpKernelExtensionTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - parent::setUp(); - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - } - /** * @expectedException \Twig_Error_Runtime */ public function testFragmentWithError() { - $kernel = $this->getFragmentHandler($this->throwException(new \Exception('foo'))); + $renderer = $this->getFragmentHandler($this->throwException(new \Exception('foo'))); - $loader = new \Twig_Loader_Array(array('index' => '{{ fragment("foo") }}')); - $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false)); - $twig->addExtension(new HttpKernelExtension($kernel)); + $this->renderTemplate($renderer); + } + + public function testRenderFragment() + { + $renderer = $this->getFragmentHandler($this->returnValue(new Response('html'))); + + $response = $this->renderTemplate($renderer); + + $this->assertEquals('html', $response); + } - $this->renderTemplate($kernel); + public function testUnknownFragmentRenderer() + { + $context = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack') + ->disableOriginalConstructor() + ->getMock() + ; + $renderer = new FragmentHandler(array(), false, $context); + + $this->setExpectedException('InvalidArgumentException', 'The "inline" renderer does not exist.'); + $renderer->render('/foo'); } protected function getFragmentHandler($return) @@ -51,8 +55,14 @@ protected function getFragmentHandler($return) $strategy->expects($this->once())->method('getName')->will($this->returnValue('inline')); $strategy->expects($this->once())->method('render')->will($return); - $renderer = new FragmentHandler(array($strategy)); - $renderer->setRequest(Request::create('/')); + $context = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack') + ->disableOriginalConstructor() + ->getMock() + ; + + $context->expects($this->any())->method('getCurrentRequest')->will($this->returnValue(Request::create('/'))); + + $renderer = new FragmentHandler(array($strategy), false, $context); return $renderer; } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php index 3c5d762ca008e..cd0bbdf0ec7d4 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php @@ -12,19 +12,9 @@ namespace Symfony\Bridge\Twig\Tests\Extension; use Symfony\Bridge\Twig\Extension\RoutingExtension; -use Symfony\Bridge\Twig\Tests\TestCase; -class RoutingExtensionTest extends TestCase +class RoutingExtensionTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - parent::setUp(); - - if (!class_exists('Symfony\Component\Routing\Route')) { - $this->markTestSkipped('The "Routing" component is not available'); - } - } - /** * @dataProvider getEscapingTemplates */ diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php new file mode 100644 index 0000000000000..bac855390f440 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\StopwatchExtension; + +class StopwatchExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \Twig_Error_Syntax + */ + public function testFailIfStoppingWrongEvent() + { + $this->testTiming('{% stopwatch "foo" %}{% endstopwatch "bar" %}', array()); + } + + /** + * @dataProvider getTimingTemplates + */ + public function testTiming($template, $events) + { + $twig = new \Twig_Environment(new \Twig_Loader_String(), array('debug' => true, 'cache' => false, 'autoescape' => true, 'optimizations' => 0)); + $twig->addExtension(new StopwatchExtension($this->getStopwatch($events))); + + try { + $nodes = $twig->render($template); + } catch (\Twig_Error_Runtime $e) { + throw $e->getPrevious(); + } + } + + public function getTimingTemplates() + { + return array( + array('{% stopwatch "foo" %}something{% endstopwatch %}', 'foo'), + array('{% stopwatch "foo" %}symfony2 is fun{% endstopwatch %}{% stopwatch "bar" %}something{% endstopwatch %}', array('foo', 'bar')), + array('{% set foo = "foo" %}{% stopwatch foo %}something{% endstopwatch %}', 'foo'), + array('{% set foo = "foo" %}{% stopwatch foo %}something {% set foo = "bar" %}{% endstopwatch %}', 'foo'), + array('{% stopwatch "foo.bar" %}something{% endstopwatch %}', 'foo.bar'), + array('{% stopwatch "foo" %}something{% endstopwatch %}{% stopwatch "foo" %}something else{% endstopwatch %}', array('foo', 'foo')), + ); + } + + protected function getStopwatch($events = array()) + { + $events = is_array($events) ? $events : array($events); + $stopwatch = $this->getMock('Symfony\Component\Stopwatch\Stopwatch'); + + $i = -1; + foreach ($events as $eventName) { + $stopwatch->expects($this->at(++$i)) + ->method('start') + ->with($this->equalTo($eventName), 'template') + ; + $stopwatch->expects($this->at(++$i)) + ->method('stop') + ->with($this->equalTo($eventName)) + ; + } + + return $stopwatch; + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php index 524b86bc62e9b..c2e6089dbdefa 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -15,23 +15,9 @@ use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\MessageSelector; use Symfony\Component\Translation\Loader\ArrayLoader; -use Symfony\Bridge\Twig\Tests\TestCase; -class TranslationExtensionTest extends TestCase +class TranslationExtensionTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - parent::setUp(); - - if (!class_exists('Symfony\Component\Translation\Translator')) { - $this->markTestSkipped('The "Translation" component is not available'); - } - - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - } - public function testEscaping() { $output = $this->getTemplate('{% trans %}Percent: %value%%% (%msg%){% endtrans %}')->render(array('value' => 12, 'msg' => 'approx.')); diff --git a/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php index ffbec162d69a9..3c89791c956e7 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php @@ -11,10 +11,9 @@ namespace Symfony\Bridge\Twig\Tests\Node; -use Symfony\Bridge\Twig\Tests\TestCase; use Symfony\Bridge\Twig\Node\FormThemeNode; -class FormThemeTest extends TestCase +class FormThemeTest extends \PHPUnit_Framework_TestCase { public function testConstructor() { diff --git a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php index 91c2f66b73716..f58da757848d1 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php @@ -11,10 +11,9 @@ namespace Symfony\Bridge\Twig\Tests\Node; -use Symfony\Bridge\Twig\Tests\TestCase; use Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode; -class SearchAndRenderBlockNodeTest extends TestCase +class SearchAndRenderBlockNodeTest extends \PHPUnit_Framework_TestCase { public function testCompileWidget() { diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/ScopeTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/ScopeTest.php index bcae5919b597c..aa9d204e5606f 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/ScopeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/ScopeTest.php @@ -12,9 +12,8 @@ namespace Symfony\Bridge\Twig\Tests\NodeVisitor; use Symfony\Bridge\Twig\NodeVisitor\Scope; -use Symfony\Bridge\Twig\Tests\TestCase; -class ScopeTest extends TestCase +class ScopeTest extends \PHPUnit_Framework_TestCase { public function testScopeInitiation() { diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php index 24a6215e6772c..65e0dd80184cf 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php @@ -13,9 +13,8 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationDefaultDomainNodeVisitor; use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; -use Symfony\Bridge\Twig\Tests\TestCase; -class TranslationDefaultDomainNodeVisitorTest extends TestCase +class TranslationDefaultDomainNodeVisitorTest extends \PHPUnit_Framework_TestCase { private static $message = 'message'; private static $domain = 'domain'; diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php index 4e3ee6fdfa37f..0f3ec40091d1f 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php @@ -12,9 +12,8 @@ namespace Symfony\Bridge\Twig\Tests\NodeVisitor; use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; -use Symfony\Bridge\Twig\Tests\TestCase; -class TranslationNodeVisitorTest extends TestCase +class TranslationNodeVisitorTest extends \PHPUnit_Framework_TestCase { /** @dataProvider getMessagesExtractionTestData */ public function testMessagesExtraction(\Twig_Node $node, array $expectedMessages) diff --git a/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php b/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php index 1559df1cc36fd..28f27e9dbb3de 100644 --- a/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php +++ b/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php @@ -11,11 +11,10 @@ namespace Symfony\Bridge\Twig\Tests\TokenParser; -use Symfony\Bridge\Twig\Tests\TestCase; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Symfony\Bridge\Twig\Node\FormThemeNode; -class FormThemeTokenParserTest extends TestCase +class FormThemeTokenParserTest extends \PHPUnit_Framework_TestCase { /** * @dataProvider getTestsForFormTheme diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index a2c5cd3d030b7..52ff1682b5911 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -14,17 +14,9 @@ use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Translation\TwigExtractor; use Symfony\Component\Translation\MessageCatalogue; -use Symfony\Bridge\Twig\Tests\TestCase; -class TwigExtractorTest extends TestCase +class TwigExtractorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Translation\Translator')) { - $this->markTestSkipped('The "Translation" component is not available'); - } - } - /** * @dataProvider getExtractData */ diff --git a/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php b/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php index 1e6a3c4933675..e7047c354d080 100644 --- a/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php +++ b/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php @@ -12,8 +12,9 @@ namespace Symfony\Bridge\Twig\Tests; use Symfony\Bridge\Twig\TwigEngine; +use Symfony\Component\Templating\TemplateReference; -class TwigEngineTest extends TestCase +class TwigEngineTest extends \PHPUnit_Framework_TestCase { public function testExistsWithTemplateInstances() { @@ -27,6 +28,7 @@ public function testExistsWithNonExistentTemplates() $engine = $this->getTwig(); $this->assertFalse($engine->exists('foobar')); + $this->assertFalse($engine->exists(new TemplateReference('foorbar'))); } public function testExistsWithTemplateWithSyntaxErrors() @@ -34,6 +36,7 @@ public function testExistsWithTemplateWithSyntaxErrors() $engine = $this->getTwig(); $this->assertTrue($engine->exists('error')); + $this->assertTrue($engine->exists(new TemplateReference('error'))); } public function testExists() @@ -41,6 +44,25 @@ public function testExists() $engine = $this->getTwig(); $this->assertTrue($engine->exists('index')); + $this->assertTrue($engine->exists(new TemplateReference('index'))); + } + + public function testRender() + { + $engine = $this->getTwig(); + + $this->assertSame('foo', $engine->render('index')); + $this->assertSame('foo', $engine->render(new TemplateReference('index'))); + } + + /** + * @expectedException \Twig_Error_Syntax + */ + public function testRenderWithError() + { + $engine = $this->getTwig(); + + $engine->render(new TemplateReference('error')); } protected function getTwig() diff --git a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php new file mode 100644 index 0000000000000..2983e4cb6b03b --- /dev/null +++ b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.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\Bridge\Twig\TokenParser; + +use Symfony\Bridge\Twig\Node\StopwatchNode; + +/** + * Token Parser for the stopwatch tag. + * + * @author Wouter J + */ +class StopwatchTokenParser extends \Twig_TokenParser +{ + protected $stopwatchIsAvailable; + + public function __construct($stopwatchIsAvailable) + { + $this->stopwatchIsAvailable = $stopwatchIsAvailable; + } + + public function parse(\Twig_Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + // {% stopwatch 'bar' %} + $name = $this->parser->getExpressionParser()->parseExpression(); + + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + // {% endstopwatch %} + $body = $this->parser->subparse(array($this, 'decideStopwatchEnd'), true); + $stream->expect(\Twig_Token::BLOCK_END_TYPE); + + if ($this->stopwatchIsAvailable) { + return new StopwatchNode($name, $body, new \Twig_Node_Expression_AssignName($this->parser->getVarName(), $token->getLine()), $lineno, $this->getTag()); + } + + return $body; + } + + public function decideStopwatchEnd(\Twig_Token $token) + { + return $token->test('endstopwatch'); + } + + public function getTag() + { + return 'stopwatch'; + } +} diff --git a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php index b93193f293c96..7f8d687308cbd 100644 --- a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php +++ b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php @@ -56,7 +56,7 @@ public function extract($directory, MessageCatalogue $catalogue) { // load any existing translation files $finder = new Finder(); - $files = $finder->files()->name('*.twig')->in($directory); + $files = $finder->files()->name('*.twig')->sortByName()->in($directory); foreach ($files as $file) { $this->extractTemplate(file_get_contents($file->getPathname()), $catalogue); } diff --git a/src/Symfony/Bridge/Twig/TwigEngine.php b/src/Symfony/Bridge/Twig/TwigEngine.php index 41e15bacaa612..3e3257e7fa0f5 100644 --- a/src/Symfony/Bridge/Twig/TwigEngine.php +++ b/src/Symfony/Bridge/Twig/TwigEngine.php @@ -14,6 +14,7 @@ use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Templating\StreamingEngineInterface; use Symfony\Component\Templating\TemplateNameParserInterface; +use Symfony\Component\Templating\TemplateReferenceInterface; /** * This engine knows how to render Twig templates. @@ -38,15 +39,11 @@ public function __construct(\Twig_Environment $environment, TemplateNameParserIn } /** - * Renders a template. + * {@inheritdoc} * - * @param mixed $name A template name - * @param array $parameters An array of parameters to pass to the template + * It also supports \Twig_Template as name parameter. * - * @return string The evaluated template as a string - * - * @throws \InvalidArgumentException if the template does not exist - * @throws \RuntimeException if the template cannot be rendered + * @throws \Twig_Error if something went wrong like a thrown exception while rendering the template */ public function render($name, array $parameters = array()) { @@ -54,12 +51,11 @@ public function render($name, array $parameters = array()) } /** - * Streams a template. + * {@inheritdoc} * - * @param mixed $name A template name or a TemplateReferenceInterface instance - * @param array $parameters An array of parameters to pass to the template + * It also supports \Twig_Template as name parameter. * - * @throws \RuntimeException if the template cannot be rendered + * @throws \Twig_Error if something went wrong like a thrown exception while rendering the template */ public function stream($name, array $parameters = array()) { @@ -67,11 +63,9 @@ public function stream($name, array $parameters = array()) } /** - * Returns true if the template exists. + * {@inheritdoc} * - * @param mixed $name A template name - * - * @return Boolean true if the template exists, false otherwise + * It also supports \Twig_Template as name parameter. */ public function exists($name) { @@ -82,11 +76,13 @@ public function exists($name) $loader = $this->environment->getLoader(); if ($loader instanceof \Twig_ExistsLoaderInterface) { - return $loader->exists($name); + return $loader->exists((string) $name); } try { - $loader->getSource($name); + // cast possible TemplateReferenceInterface to string because the + // EngineInterface supports them but Twig_LoaderInterface does not + $loader->getSource((string) $name); } catch (\Twig_Error_Loader $e) { return false; } @@ -95,11 +91,9 @@ public function exists($name) } /** - * Returns true if this class is able to render the given template. - * - * @param string $name A template name + * {@inheritdoc} * - * @return Boolean True if this class supports the given resource, false otherwise + * It also supports \Twig_Template as name parameter. */ public function supports($name) { @@ -115,7 +109,8 @@ public function supports($name) /** * Loads the given template. * - * @param mixed $name A template name or an instance of Twig_Template + * @param string|TemplateReferenceInterface|\Twig_Template $name A template name or an instance of + * TemplateReferenceInterface or \Twig_Template * * @return \Twig_TemplateInterface A \Twig_TemplateInterface instance * @@ -128,7 +123,7 @@ protected function load($name) } try { - return $this->environment->loadTemplate($name); + return $this->environment->loadTemplate((string) $name); } catch (\Twig_Error_Loader $e) { throw new \InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index fc3b1ae0e4b0e..f4df370eb04e7 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=5.3.3", + "symfony/security-csrf": "~2.4", "twig/twig": "~1.12" }, "require-dev": { @@ -26,16 +27,21 @@ "symfony/templating": "~2.1", "symfony/translation": "~2.2", "symfony/yaml": "~2.0", - "symfony/security": "~2.0" + "symfony/security": "~2.4", + "symfony/stopwatch": "~2.2", + "symfony/console": "~2.2", + "symfony/expression-language": "~2.4" }, "suggest": { - "symfony/form": "", - "symfony/http-kernel": "", - "symfony/routing": "", - "symfony/templating": "", - "symfony/translation": "", - "symfony/yaml": "", - "symfony/security": "" + "symfony/form": "For using the FormExtension", + "symfony/http-kernel": "For using the HttpKernelExtension", + "symfony/routing": "For using the RoutingExtension", + "symfony/templating": "For using the TwigEngine", + "symfony/translation": "For using the TranslationExtension", + "symfony/yaml": "For using the YamlExtension", + "symfony/security": "For using the SecurityExtension", + "symfony/stopwatch": "For using the StopwatchExtension", + "symfony/expression": "For using the ExpressionExtension" }, "autoload": { "psr-0": { "Symfony\\Bridge\\Twig\\": "" } @@ -44,7 +50,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 11ab65cff4831..3845ac5f7c0f0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,20 @@ CHANGELOG ========= +2.5.0 +----- + + * Added `yaml:lint` command + * Deprecated the `RouterApacheDumperCommand` which will be removed in Symfony 3.0. + +2.4.0 +----- + + * allowed multiple IP addresses in profiler matcher settings + * added stopwatch helper to time templates with the WebProfilerBundle + * added service definition for "security.secure_random" service + * added service definitions for the new Security CSRF sub-component + 2.3.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index b7e4bfd0c6336..28dac98e35916 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -77,9 +77,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $warmupDir = substr($realCacheDir, 0, -1).'_'; if ($filesystem->exists($warmupDir)) { + if ($output->isVerbose()) { + $output->writeln(' Clearing outdated warmup directory'); + } $filesystem->remove($warmupDir); } + if ($output->isVerbose()) { + $output->writeln(' Warming up cache'); + } $this->warmup($warmupDir, $realCacheDir, !$input->getOption('no-optional-warmers')); $filesystem->rename($realCacheDir, $oldCacheDir); @@ -89,7 +95,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $filesystem->rename($warmupDir, $realCacheDir); } + if ($output->isVerbose()) { + $output->writeln(' Removing old cache directory'); + } + $filesystem->remove($oldCacheDir); + + if ($output->isVerbose()) { + $output->writeln(' Done'); + } } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php index f9e2d403c3dfa..c94d167bc04c5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php @@ -11,8 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Command; -use Symfony\Component\Config\Definition\ReferenceDumper; +use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; +use Symfony\Component\Config\Definition\Dumper\XmlReferenceDumper; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -21,6 +23,7 @@ * A console command for dumping available configuration reference * * @author Kevin Bond + * @author Wouter J */ class ConfigDumpReferenceCommand extends ContainerDebugCommand { @@ -32,21 +35,21 @@ protected function configure() $this ->setName('config:dump-reference') ->setDefinition(array( - new InputArgument('name', InputArgument::OPTIONAL, 'The Bundle or extension alias') + new InputArgument('name', InputArgument::OPTIONAL, 'The Bundle name or the extension alias'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The format, either yaml or xml', 'yaml'), )) - ->setDescription('Dumps default configuration for an extension') + ->setDescription('Dumps the default configuration for an extension') ->setHelp(<<%command.name% command dumps the default configuration for an extension/bundle. +The %command.name% command dumps the default configuration for an +extension/bundle. The extension alias or bundle name can be used: -Example: - php %command.full_name% framework - -or - php %command.full_name% FrameworkBundle + +With the format option specifies the format of the configuration, +this is either yaml or xml. When the option is not provided, yaml is used. EOF ) ; @@ -66,10 +69,14 @@ protected function execute(InputInterface $input, OutputInterface $output) if (empty($name)) { $output->writeln('Available registered bundles with their extension alias if available:'); + + $table = $this->getHelperSet()->get('table'); + $table->setHeaders(array('Bundle name', 'Extension alias')); foreach ($bundles as $bundle) { $extension = $bundle->getContainerExtension(); - $output->writeln($bundle->getName().($extension ? ': '.$extension->getAlias() : '')); + $table->addRow(array($bundle->getName(), $extension ? $extension->getAlias() : '')); } + $table->render($output); return; } @@ -116,9 +123,20 @@ protected function execute(InputInterface $input, OutputInterface $output) throw new \LogicException(sprintf('Configuration class "%s" should implement ConfigurationInterface in order to be dumpable', get_class($configuration))); } - $output->writeln($message); + switch ($input->getOption('format')) { + case 'yaml': + $output->writeln(sprintf('# %s', $message)); + $dumper = new YamlReferenceDumper(); + break; + case 'xml': + $output->writeln(sprintf('', $message)); + $dumper = new XmlReferenceDumper(); + break; + default: + $output->writeln($message); + throw new \InvalidArgumentException('Only the yaml and xml formats are supported.'); + } - $dumper = new ReferenceDumper(); - $output->writeln($dumper->dump($configuration)); + $output->writeln($dumper->dump($configuration, $extension->getNamespace())); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php index 81469e984e065..fee2c6543e306 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -11,12 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\Alias; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; @@ -46,7 +45,9 @@ protected function configure() new InputOption('tag', null, InputOption::VALUE_REQUIRED, 'Show all services with a specific tag'), new InputOption('tags', null, InputOption::VALUE_NONE, 'Displays tagged services for an application'), new InputOption('parameter', null, InputOption::VALUE_REQUIRED, 'Displays a specific parameter for an application'), - new InputOption('parameters', null, InputOption::VALUE_NONE, 'Displays parameters for an application') + new InputOption('parameters', null, InputOption::VALUE_NONE, 'Displays parameters for an application'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output description in other formats', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), )) ->setDescription('Displays current services for an application') ->setHelp(<<validateInput($input); - $this->containerBuilder = $this->getContainerBuilder(); - if ($input->getOption('parameters')) { - $parameters = $this->getContainerBuilder()->getParameterBag()->all(); - - // Sort parameters alphabetically - ksort($parameters); - - $this->outputParameters($output, $parameters); - - return; - } - - $parameter = $input->getOption('parameter'); - if (null !== $parameter) { - $output->write($this->formatParameter($this->getContainerBuilder()->getParameter($parameter))); - - return; - } - - if ($input->getOption('tags')) { - $this->outputTags($output, $input->getOption('show-private')); - - return; - } - - $tag = $input->getOption('tag'); - if (null !== $tag) { - $serviceIds = array_keys($this->containerBuilder->findTaggedServiceIds($tag)); + $object = $this->getContainerBuilder()->getParameterBag(); + $options = array(); + } elseif ($parameter = $input->getOption('parameter')) { + $object = $this->getContainerBuilder(); + $options = array('parameter' => $parameter); + } elseif ($input->getOption('tags')) { + $object = $this->getContainerBuilder(); + $options = array('group_by' => 'tags', 'show_private' => $input->getOption('show-private')); + } elseif ($tag = $input->getOption('tag')) { + $object = $this->getContainerBuilder(); + $options = array('tag' => $tag, 'show_private' => $input->getOption('show-private')); + } elseif ($name = $input->getArgument('name')) { + $object = $this->getContainerBuilder(); + $options = array('id' => $name); } else { - $serviceIds = $this->containerBuilder->getServiceIds(); + $object = $this->getContainerBuilder(); + $options = array('show_private' => $input->getOption('show-private')); } - // sort so that it reads like an index of services - asort($serviceIds); - - $name = $input->getArgument('name'); - if ($name) { - $this->outputService($output, $name); - } else { - $this->outputServices($output, $serviceIds, $input->getOption('show-private'), $tag); - } + $helper = new DescriptorHelper(); + $options['format'] = $input->getOption('format'); + $options['raw_text'] = $input->getOption('raw'); + $helper->describe($output, $object, $options); } + /** + * Validates input arguments and options. + * + * @param InputInterface $input + * + * @throws \InvalidArgumentException + */ protected function validateInput(InputInterface $input) { $options = array('tags', 'tag', 'parameters', 'parameter'); @@ -155,211 +145,6 @@ protected function validateInput(InputInterface $input) } } - protected function outputServices(OutputInterface $output, $serviceIds, $showPrivate = false, $showTagAttributes = null) - { - // set the label to specify public or public+private - if ($showPrivate) { - $label = 'Public and private services'; - } else { - $label = 'Public services'; - } - if ($showTagAttributes) { - $label .= ' with tag '.$showTagAttributes.''; - } - - $output->writeln($this->getHelper('formatter')->formatSection('container', $label)); - - // loop through to get space needed and filter private services - $maxName = 4; - $maxScope = 6; - $maxTags = array(); - foreach ($serviceIds as $key => $serviceId) { - $definition = $this->resolveServiceDefinition($serviceId); - - if ($definition instanceof Definition) { - // filter out private services unless shown explicitly - if (!$showPrivate && !$definition->isPublic()) { - unset($serviceIds[$key]); - continue; - } - - if (strlen($definition->getScope()) > $maxScope) { - $maxScope = strlen($definition->getScope()); - } - - if (null !== $showTagAttributes) { - $tags = $definition->getTag($showTagAttributes); - foreach ($tags as $tag) { - foreach ($tag as $key => $value) { - if (!isset($maxTags[$key])) { - $maxTags[$key] = strlen($key); - } - if (strlen($value) > $maxTags[$key]) { - $maxTags[$key] = strlen($value); - } - } - } - } - } - - if (strlen($serviceId) > $maxName) { - $maxName = strlen($serviceId); - } - } - $format = '%-'.$maxName.'s '; - $format .= implode("", array_map(function ($length) { return "%-{$length}s "; }, $maxTags)); - $format .= '%-'.$maxScope.'s %s'; - - // the title field needs extra space to make up for comment tags - $format1 = '%-'.($maxName + 19).'s '; - $format1 .= implode("", array_map(function ($length) { return '%-'.($length + 19).'s '; }, $maxTags)); - $format1 .= '%-'.($maxScope + 19).'s %s'; - - $tags = array(); - foreach ($maxTags as $tagName => $length) { - $tags[] = ''.$tagName.''; - } - $output->writeln(vsprintf($format1, $this->buildArgumentsArray('Service Id', 'Scope', 'Class Name', $tags))); - - foreach ($serviceIds as $serviceId) { - $definition = $this->resolveServiceDefinition($serviceId); - - if ($definition instanceof Definition) { - $lines = array(); - if (null !== $showTagAttributes) { - foreach ($definition->getTag($showTagAttributes) as $key => $tag) { - $tagValues = array(); - foreach (array_keys($maxTags) as $tagName) { - $tagValues[] = isset($tag[$tagName]) ? $tag[$tagName] : ""; - } - if (0 === $key) { - $lines[] = $this->buildArgumentsArray($serviceId, $definition->getScope(), $definition->getClass(), $tagValues); - } else { - $lines[] = $this->buildArgumentsArray(' "', '', '', $tagValues); - } - } - } else { - $lines[] = $this->buildArgumentsArray($serviceId, $definition->getScope(), $definition->getClass()); - } - - foreach ($lines as $arguments) { - $output->writeln(vsprintf($format, $arguments)); - } - } elseif ($definition instanceof Alias) { - $alias = $definition; - $output->writeln(vsprintf($format, $this->buildArgumentsArray($serviceId, 'n/a', sprintf('alias for %s', (string) $alias), count($maxTags) ? array_fill(0, count($maxTags), "") : array()))); - } else { - // we have no information (happens with "service_container") - $service = $definition; - $output->writeln(vsprintf($format, $this->buildArgumentsArray($serviceId, '', get_class($service), count($maxTags) ? array_fill(0, count($maxTags), "") : array()))); - } - } - } - - protected function buildArgumentsArray($serviceId, $scope, $className, array $tagAttributes = array()) - { - $arguments = array($serviceId); - foreach ($tagAttributes as $tagAttribute) { - $arguments[] = $tagAttribute; - } - $arguments[] = $scope; - $arguments[] = $className; - - return $arguments; - } - - /** - * Renders detailed service information about one service - */ - protected function outputService(OutputInterface $output, $serviceId) - { - $definition = $this->resolveServiceDefinition($serviceId); - - $label = sprintf('Information for service %s', $serviceId); - $output->writeln($this->getHelper('formatter')->formatSection('container', $label)); - $output->writeln(''); - - if ($definition instanceof Definition) { - $output->writeln(sprintf('Service Id %s', $serviceId)); - $output->writeln(sprintf('Class %s', $definition->getClass() ?: "-")); - - $tags = $definition->getTags(); - if (count($tags)) { - $output->writeln('Tags'); - foreach ($tags as $tagName => $tagData) { - foreach ($tagData as $singleTagData) { - $output->writeln(sprintf(' - %-30s (%s)', $tagName, implode(', ', array_map(function ($key, $value) { - return sprintf('%s: %s', $key, $value); - }, array_keys($singleTagData), array_values($singleTagData))))); - } - } - } else { - $output->writeln('Tags -'); - } - - $output->writeln(sprintf('Scope %s', $definition->getScope())); - - $public = $definition->isPublic() ? 'yes' : 'no'; - $output->writeln(sprintf('Public %s', $public)); - - $synthetic = $definition->isSynthetic() ? 'yes' : 'no'; - $output->writeln(sprintf('Synthetic %s', $synthetic)); - - $file = $definition->getFile() ? $definition->getFile() : '-'; - $output->writeln(sprintf('Required File %s', $file)); - } elseif ($definition instanceof Alias) { - $alias = $definition; - $output->writeln(sprintf('This service is an alias for the service %s', (string) $alias)); - } else { - // edge case (but true for "service_container", all we have is the service itself - $service = $definition; - $output->writeln(sprintf('Service Id %s', $serviceId)); - $output->writeln(sprintf('Class %s', get_class($service))); - } - } - - protected function outputParameters(OutputInterface $output, $parameters) - { - $output->writeln($this->getHelper('formatter')->formatSection('container', 'List of parameters')); - - $terminalDimensions = $this->getApplication()->getTerminalDimensions(); - $maxTerminalWidth = $terminalDimensions[0]; - $maxParameterWidth = 0; - $maxValueWidth = 0; - - // Determine max parameter & value length - foreach ($parameters as $parameter => $value) { - $parameterWidth = strlen($parameter); - if ($parameterWidth > $maxParameterWidth) { - $maxParameterWidth = $parameterWidth; - } - - $valueWith = strlen($this->formatParameter($value)); - if ($valueWith > $maxValueWidth) { - $maxValueWidth = $valueWith; - } - } - - $maxValueWidth = min($maxValueWidth, $maxTerminalWidth - $maxParameterWidth - 1); - - $formatTitle = '%-'.($maxParameterWidth + 19).'s %-'.($maxValueWidth + 19).'s'; - $format = '%-'.$maxParameterWidth.'s %-'.$maxValueWidth.'s'; - - $output->writeln(sprintf($formatTitle, 'Parameter', 'Value')); - - foreach ($parameters as $parameter => $value) { - $splits = str_split($this->formatParameter($value), $maxValueWidth); - - foreach ($splits as $index => $split) { - if (0 === $index) { - $output->writeln(sprintf($format, $parameter, $split)); - } else { - $output->writeln(sprintf($format, ' ', $split)); - } - } - } - } - /** * Loads the ContainerBuilder from the cache. * @@ -384,77 +169,4 @@ protected function getContainerBuilder() return $container; } - - /** - * Given an array of service IDs, this returns the array of corresponding - * Definition and Alias objects that those ids represent. - * - * @param string $serviceId The service id to resolve - * - * @return Definition|Alias - */ - protected function resolveServiceDefinition($serviceId) - { - if ($this->containerBuilder->hasDefinition($serviceId)) { - return $this->containerBuilder->getDefinition($serviceId); - } - - // Some service IDs don't have a Definition, they're simply an Alias - if ($this->containerBuilder->hasAlias($serviceId)) { - return $this->containerBuilder->getAlias($serviceId); - } - - // the service has been injected in some special way, just return the service - return $this->containerBuilder->get($serviceId); - } - - /** - * Renders list of tagged services grouped by tag - * - * @param OutputInterface $output - * @param Boolean $showPrivate - */ - protected function outputTags(OutputInterface $output, $showPrivate = false) - { - $tags = $this->containerBuilder->findTags(); - asort($tags); - - $label = 'Tagged services'; - $output->writeln($this->getHelper('formatter')->formatSection('container', $label)); - - foreach ($tags as $tag) { - $serviceIds = $this->containerBuilder->findTaggedServiceIds($tag); - - foreach ($serviceIds as $serviceId => $attributes) { - $definition = $this->resolveServiceDefinition($serviceId); - if ($definition instanceof Definition) { - if (!$showPrivate && !$definition->isPublic()) { - unset($serviceIds[$serviceId]); - continue; - } - } - } - - if (count($serviceIds) === 0) { - continue; - } - - $output->writeln($this->getHelper('formatter')->formatSection('tag', $tag)); - - foreach ($serviceIds as $serviceId => $attributes) { - $output->writeln($serviceId); - } - - $output->writeln(''); - } - } - - protected function formatParameter($value) - { - if (is_bool($value) || is_array($value) || (null === $value)) { - return json_encode($value); - } - - return $value; - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php index c8a17e8904b05..c122576fd5614 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php @@ -21,6 +21,10 @@ /** * RouterApacheDumperCommand. * + * @deprecated Deprecated since version 2.5, to be removed in 3.0. + * The performance gains are minimal and it's very hard to replicate + * the behavior of PHP implementation. + * * @author Fabien Potencier */ class RouterApacheDumperCommand extends ContainerAwareCommand diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index d581af54f92c8..e4b49a2679890 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -11,10 +11,13 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Routing\Route; /** * A console command for retrieving information about routes @@ -49,6 +52,9 @@ protected function configure() ->setName('router:debug') ->setDefinition(array( new InputArgument('name', InputArgument::OPTIONAL, 'A route name'), + new InputOption('show-controllers', null, InputOption::VALUE_NONE, 'Show assigned controllers in overview'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output route(s) in other formats', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), )) ->setDescription('Displays current routes for an application') ->setHelp(<<getArgument('name'); + $helper = new DescriptorHelper(); if ($name) { - $this->outputRoute($output, $name); + $route = $this->getContainer()->get('router')->getRouteCollection()->get($name); + if (!$route) { + throw new \InvalidArgumentException(sprintf('The route "%s" does not exist.', $name)); + } + $this->convertController($route); + $helper->describe($output, $route, array( + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'name' => $name, + )); } else { - $this->outputRoutes($output); - } - } - - protected function outputRoutes(OutputInterface $output, $routes = null) - { - if (null === $routes) { - $routes = $this->getContainer()->get('router')->getRouteCollection()->all(); - } - - $output->writeln($this->getHelper('formatter')->formatSection('router', 'Current routes')); - - $maxName = strlen('name'); - $maxMethod = strlen('method'); - $maxScheme = strlen('scheme'); - $maxHost = strlen('host'); - - foreach ($routes as $name => $route) { - $method = $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY'; - $scheme = $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'; - $host = '' !== $route->getHost() ? $route->getHost() : 'ANY'; - $maxName = max($maxName, strlen($name)); - $maxMethod = max($maxMethod, strlen($method)); - $maxScheme = max($maxScheme, strlen($scheme)); - $maxHost = max($maxHost, strlen($host)); - } - - $format = '%-'.$maxName.'s %-'.$maxMethod.'s %-'.$maxScheme.'s %-'.$maxHost.'s %s'; - $formatHeader = '%-'.($maxName + 19).'s %-'.($maxMethod + 19).'s %-'.($maxScheme + 19).'s %-'.($maxHost + 19).'s %s'; - $output->writeln(sprintf($formatHeader, 'Name', 'Method', 'Scheme', 'Host', 'Path')); - - foreach ($routes as $name => $route) { - $method = $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY'; - $scheme = $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'; - $host = '' !== $route->getHost() ? $route->getHost() : 'ANY'; - $output->writeln(sprintf($format, $name, $method, $scheme, $host, $route->getPath()), OutputInterface::OUTPUT_RAW); - } - } - - /** - * @throws \InvalidArgumentException When route does not exist - */ - protected function outputRoute(OutputInterface $output, $name) - { - $route = $this->getContainer()->get('router')->getRouteCollection()->get($name); - if (!$route) { - throw new \InvalidArgumentException(sprintf('The route "%s" does not exist.', $name)); - } + $routes = $this->getContainer()->get('router')->getRouteCollection(); - $output->writeln($this->getHelper('formatter')->formatSection('router', sprintf('Route "%s"', $name))); + foreach ($routes as $route) { + $this->convertController($route); + } - $method = $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY'; - $scheme = $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'; - $host = '' !== $route->getHost() ? $route->getHost() : 'ANY'; - - $output->write('Name '); - $output->writeln($name, OutputInterface::OUTPUT_RAW); - - $output->write('Path '); - $output->writeln($route->getPath(), OutputInterface::OUTPUT_RAW); - - $output->write('Host '); - $output->writeln($host, OutputInterface::OUTPUT_RAW); - - $output->write('Scheme '); - $output->writeln($scheme, OutputInterface::OUTPUT_RAW); - - $output->write('Method '); - $output->writeln($method, OutputInterface::OUTPUT_RAW); - - $output->write('Class '); - $output->writeln(get_class($route), OutputInterface::OUTPUT_RAW); - - $output->write('Defaults '); - $output->writeln($this->formatConfigs($route->getDefaults()), OutputInterface::OUTPUT_RAW); - - $output->write('Requirements '); - // we do not want to show the schemes and methods again that are also in the requirements for BC - $requirements = $route->getRequirements(); - unset($requirements['_scheme'], $requirements['_method']); - $output->writeln($this->formatConfigs($requirements) ?: 'NO CUSTOM', OutputInterface::OUTPUT_RAW); - - $output->write('Options '); - $output->writeln($this->formatConfigs($route->getOptions()), OutputInterface::OUTPUT_RAW); - - $output->write('Path-Regex '); - $output->writeln($route->compile()->getRegex(), OutputInterface::OUTPUT_RAW); - - if (null !== $route->compile()->getHostRegex()) { - $output->write('Host-Regex '); - $output->writeln($route->compile()->getHostRegex(), OutputInterface::OUTPUT_RAW); + $helper->describe($output, $routes, array( + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'show_controllers' => $input->getOption('show-controllers'), + )); } } - protected function formatValue($value) + private function convertController(Route $route) { - if (is_object($value)) { - return sprintf('object(%s)', get_class($value)); - } - - if (is_string($value)) { - return $value; + $nameParser = $this->getContainer()->get('controller_name_converter'); + if ($route->hasDefault('_controller')) { + try { + $route->setDefault('_controller', $nameParser->build($route->getDefault('_controller'))); + } catch (\InvalidArgumentException $e) { + } } - - return preg_replace("/\n\s*/s", '', var_export($value, true)); - } - - private function formatConfigs(array $array) - { - $string = ''; - ksort($array); - foreach ($array as $name => $value) { - $string .= ($string ? "\n".str_repeat(' ', 13) : '').$name.': '.$this->formatValue($value); - } - - return $string; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php new file mode 100644 index 0000000000000..b7f46d1e9d163 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.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\Bundle\FrameworkBundle\Command; + +if (!defined('JSON_PRETTY_PRINT')) { + define('JSON_PRETTY_PRINT', 128); +} + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Parser; + +/** + * Validates YAML files syntax and output encountered errors. + * + * @author Grégoire Pineau + */ +class YamlLintCommand extends Command +{ + protected function configure() + { + $this + ->setName('yaml:lint') + ->setDescription('Lints a file and outputs encountered errors') + ->addArgument('filename', null, 'A file or a directory or STDIN') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->setHelp(<<%command.name% command lints a YAML file and outputs to STDOUT +the first encountered syntax error. + +You can validate the syntax of a file: + +php %command.full_name% filename + +Or of a whole directory: + +php %command.full_name% dirname +php %command.full_name% dirname --format=json + +Or all YAML files in a bundle: + +php %command.full_name% @AcmeDemoBundle + +You can also pass the YAML contents from STDIN: + +cat filename | php %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $filename = $input->getArgument('filename'); + + if (!$filename) { + if (0 !== ftell(STDIN)) { + throw new \RuntimeException('Please provide a filename or pipe file content to STDIN.'); + } + + $content = ''; + while (!feof(STDIN)) { + $content .= fread(STDIN, 1024); + } + + return $this->display($input, $output, array($this->validate($content))); + } + + if (0 !== strpos($filename, '@') && !is_readable($filename)) { + throw new \RuntimeException(sprintf('File or directory "%s" is not readable', $filename)); + } + + $files = array(); + if (is_file($filename)) { + $files = array($filename); + } elseif (is_dir($filename)) { + $files = Finder::create()->files()->in($filename)->name('*.yml'); + } else { + $dir = $this->getApplication()->getKernel()->locateResource($filename); + $files = Finder::create()->files()->in($dir)->name('*.yml'); + } + + $filesInfo = array(); + foreach ($files as $file) { + $filesInfo[] = $this->validate(file_get_contents($file), $file); + } + + return $this->display($input, $output, $filesInfo); + } + + private function validate($content, $file = null) + { + $this->parser = new Parser(); + try { + $this->parser->parse($content); + } catch (ParseException $e) { + return array('file' => $file, 'valid' => false, 'message' => $e->getMessage()); + } + + return array('file' => $file, 'valid' => true); + } + + private function display(InputInterface $input, OutputInterface $output, $files) + { + switch ($input->getOption('format')) { + case 'txt': + return $this->displayTxt($output, $files); + case 'json': + return $this->displayJson($output, $files); + default: + throw new \InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); + } + } + + private function displayTxt(OutputInterface $output, $filesInfo) + { + $errors = 0; + + foreach ($filesInfo as $info) { + if ($info['valid'] && $output->isVerbose()) { + $output->writeln('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); + } elseif (!$info['valid']) { + $errors++; + $output->writeln(sprintf('KO in %s', $info['file'])); + $output->writeln(sprintf('>> %s', $info['message'])); + } + } + + $output->writeln(sprintf('%d/%d valid files', count($filesInfo) - $errors, count($filesInfo))); + + return min($errors, 1); + } + + private function displayJson(OutputInterface $output, $filesInfo) + { + $errors = 0; + + array_walk($filesInfo, function (&$v) use (&$errors) { + $v['file'] = (string) $v['file']; + if (!$v['valid']) { + $errors++; + } + }); + + $output->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT)); + + return min($errors, 1); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 9d7e4ff1a9996..224dbea2e09e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -98,6 +98,14 @@ public function doRun(InputInterface $input, OutputInterface $output) protected function registerCommands() { + $container = $this->kernel->getContainer(); + + if ($container->hasParameter('console.command.ids')) { + foreach ($container->getParameter('console.command.ids') as $id) { + $this->add($container->get($id)); + } + } + foreach ($this->kernel->getBundles() as $bundle) { if ($bundle instanceof Bundle) { $bundle->registerCommands($this); diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php new file mode 100644 index 0000000000000..51d6eeb0ce473 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php @@ -0,0 +1,283 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; + +use Symfony\Component\Console\Descriptor\DescriptorInterface; +use Symfony\Component\Console\Helper\TableHelper; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + */ +abstract class Descriptor implements DescriptorInterface +{ + /** + * @var OutputInterface + */ + private $output; + + /** + * {@inheritdoc} + */ + public function describe(OutputInterface $output, $object, array $options = array()) + { + $this->output = $output; + + switch (true) { + case $object instanceof RouteCollection: + $this->describeRouteCollection($object, $options); + break; + case $object instanceof Route: + $this->describeRoute($object, $options); + break; + case $object instanceof ParameterBag: + $this->describeContainerParameters($object, $options); + break; + case $object instanceof ContainerBuilder && isset($options['group_by']) && 'tags' === $options['group_by']: + $this->describeContainerTags($object, $options); + break; + case $object instanceof ContainerBuilder && isset($options['id']): + $this->describeContainerService($this->resolveServiceDefinition($object, $options['id']), $options); + break; + case $object instanceof ContainerBuilder && isset($options['parameter']): + $this->formatParameter($object->getParameter($options['parameter'])); + break; + case $object instanceof ContainerBuilder: + $this->describeContainerServices($object, $options); + break; + case $object instanceof Definition: + $this->describeContainerDefinition($object, $options); + break; + case $object instanceof Alias: + $this->describeContainerAlias($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + } + } + + /** + * Writes content to output. + * + * @param string $content + * @param boolean $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); + } + + /** + * Writes content to output. + * + * @param TableHelper $table + * @param boolean $decorated + */ + protected function renderTable(TableHelper $table, $decorated = false) + { + if (!$decorated) { + $table->setCellRowFormat('%s'); + $table->setCellHeaderFormat('%s'); + } + + $table->render($this->output); + } + + /** + * Describes an InputArgument instance. + * + * @param RouteCollection $routes + * @param array $options + */ + abstract protected function describeRouteCollection(RouteCollection $routes, array $options = array()); + + /** + * Describes an InputOption instance. + * + * @param Route $route + * @param array $options + */ + abstract protected function describeRoute(Route $route, array $options = array()); + + /** + * Describes container parameters. + * + * @param ParameterBag $parameters + * @param array $options + */ + abstract protected function describeContainerParameters(ParameterBag $parameters, array $options = array()); + + /** + * Describes container tags. + * + * @param ContainerBuilder $builder + * @param array $options + */ + abstract protected function describeContainerTags(ContainerBuilder $builder, array $options = array()); + + /** + * Describes a container service by its name. + * + * Common options are: + * * name: name of described service + * + * @param Definition|Alias|object $service + * @param array $options + */ + abstract protected function describeContainerService($service, array $options = array()); + + /** + * Describes container services. + * + * Common options are: + * * tag: filters described services by given tag + * + * @param ContainerBuilder $builder + * @param array $options + */ + abstract protected function describeContainerServices(ContainerBuilder $builder, array $options = array()); + + /** + * Describes a service definition. + * + * @param Definition $definition + * @param array $options + */ + abstract protected function describeContainerDefinition(Definition $definition, array $options = array()); + + /** + * Describes a service alias. + * + * @param Alias $alias + * @param array $options + */ + abstract protected function describeContainerAlias(Alias $alias, array $options = array()); + + /** + * Formats a value as string. + * + * @param mixed $value + * + * @return string + */ + protected function formatValue($value) + { + if (is_object($value)) { + return sprintf('object(%s)', get_class($value)); + } + + if (is_string($value)) { + return $value; + } + + return preg_replace("/\n\s*/s", '', var_export($value, true)); + } + + /** + * Formats a parameter. + * + * @param mixed $value + * + * @return string + */ + protected function formatParameter($value) + { + if (is_bool($value) || is_array($value) || (null === $value)) { + $jsonString = json_encode($value); + + if (!function_exists('mb_strlen')) { + return substr($jsonString, 0, 60).(strlen($jsonString) > 60 ? ' ...' : ''); + } + + if (mb_strlen($jsonString) > 60) { + return mb_substr($jsonString, 0, 60).' ...'; + } + + return $jsonString; + } + + return (string) $value; + } + + /** + * @param ContainerBuilder $builder + * @param string $serviceId + * + * @return mixed + */ + protected function resolveServiceDefinition(ContainerBuilder $builder, $serviceId) + { + if ($builder->hasDefinition($serviceId)) { + return $builder->getDefinition($serviceId); + } + + // Some service IDs don't have a Definition, they're simply an Alias + if ($builder->hasAlias($serviceId)) { + return $builder->getAlias($serviceId); + } + + // the service has been injected in some special way, just return the service + return $builder->get($serviceId); + } + + /** + * @param ContainerBuilder $builder + * @param boolean $showPrivate + * + * @return array + */ + protected function findDefinitionsByTag(ContainerBuilder $builder, $showPrivate) + { + $definitions = array(); + $tags = $builder->findTags(); + asort($tags); + + foreach ($tags as $tag) { + foreach ($builder->findTaggedServiceIds($tag) as $serviceId => $attributes) { + $definition = $this->resolveServiceDefinition($builder, $serviceId); + + if (!$definition instanceof Definition || !$showPrivate && !$definition->isPublic()) { + continue; + } + + if (!isset($definitions[$tag])) { + $definitions[$tag] = array(); + } + + $definitions[$tag][$serviceId] = $definition; + } + } + + return $definitions; + } + + protected function sortParameters(ParameterBag $parameters) + { + $parameters = $parameters->all(); + ksort($parameters); + + return $parameters; + } + + protected function sortServiceIds(array $serviceIds) + { + asort($serviceIds); + + return $serviceIds; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php new file mode 100644 index 0000000000000..b5c4908c66a85 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -0,0 +1,215 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; + +if (!defined('JSON_PRETTY_PRINT')) { + define('JSON_PRETTY_PRINT', 128); +} + +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + */ +class JsonDescriptor extends Descriptor +{ + /** + * {@inheritdoc} + */ + protected function describeRouteCollection(RouteCollection $routes, array $options = array()) + { + $data = array(); + foreach ($routes->all() as $name => $route) { + $data[$name] = $this->getRouteData($route); + } + + $this->writeData($data, $options); + } + + /** + * {@inheritdoc} + */ + protected function describeRoute(Route $route, array $options = array()) + { + $this->writeData($this->getRouteData($route), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameters(ParameterBag $parameters, array $options = array()) + { + $this->writeData($this->sortParameters($parameters), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerTags(ContainerBuilder $builder, array $options = array()) + { + $showPrivate = isset($options['show_private']) && $options['show_private']; + $data = array(); + + foreach ($this->findDefinitionsByTag($builder, $showPrivate) as $tag => $definitions) { + $data[$tag] = array(); + foreach ($definitions as $definition) { + $data[$tag][] = $this->getContainerDefinitionData($definition, true); + } + } + + $this->writeData($data, $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerService($service, array $options = array()) + { + if (!isset($options['id'])) { + throw new \InvalidArgumentException('An "id" option must be provided.'); + } + + if ($service instanceof Alias) { + $this->writeData($this->getContainerAliasData($service), $options); + } elseif ($service instanceof Definition) { + $this->writeData($this->getContainerDefinitionData($service), $options); + } else { + $this->writeData(get_class($service), $options); + } + } + + /** + * {@inheritdoc} + */ + protected function describeContainerServices(ContainerBuilder $builder, array $options = array()) + { + $serviceIds = isset($options['tag']) && $options['tag'] ? array_keys($builder->findTaggedServiceIds($options['tag'])) : $builder->getServiceIds(); + $showPrivate = isset($options['show_private']) && $options['show_private']; + $data = array('definitions' => array(), 'aliases' => array(), 'services' => array()); + + foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + $service = $this->resolveServiceDefinition($builder, $serviceId); + + if ($service instanceof Alias) { + $data['aliases'][$serviceId] = $this->getContainerAliasData($service); + } elseif ($service instanceof Definition) { + if (($showPrivate || $service->isPublic())) { + $data['definitions'][$serviceId] = $this->getContainerDefinitionData($service); + } + } else { + $data['services'][$serviceId] = get_class($service); + } + } + + $this->writeData($data, $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerDefinition(Definition $definition, array $options = array()) + { + $this->writeData($this->getContainerDefinitionData($definition, isset($options['omit_tags']) && $options['omit_tags']), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerAlias(Alias $alias, array $options = array()) + { + $this->writeData($this->getContainerAliasData($alias), $options); + } + + /** + * Writes data as json. + * + * @param array $data + * @param array $options + * + * @return array|string + */ + private function writeData(array $data, array $options) + { + $this->write(json_encode($data, (isset($options['json_encoding']) ? $options['json_encoding'] : 0) | JSON_PRETTY_PRINT)."\n"); + } + + /** + * @param Route $route + * + * @return array + */ + protected function getRouteData(Route $route) + { + $requirements = $route->getRequirements(); + unset($requirements['_scheme'], $requirements['_method']); + + return array( + 'path' => $route->getPath(), + 'host' => '' !== $route->getHost() ? $route->getHost() : 'ANY', + 'scheme' => $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY', + 'method' => $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY', + 'class' => get_class($route), + 'defaults' => $route->getDefaults(), + 'requirements' => $requirements ?: 'NO CUSTOM', + 'options' => $route->getOptions(), + 'pathRegex' => $route->compile()->getRegex(), + ); + } + + /** + * @param Definition $definition + * @param boolean $omitTags + * + * @return array + */ + private function getContainerDefinitionData(Definition $definition, $omitTags = false) + { + $data = array( + 'class' => (string) $definition->getClass(), + 'scope' => $definition->getScope(), + 'public' => $definition->isPublic(), + 'synthetic' => $definition->isSynthetic(), + 'file' => $definition->getFile(), + ); + + if (!$omitTags) { + $data['tags'] = array(); + if (count($definition->getTags())) { + foreach ($definition->getTags() as $tagName => $tagData) { + foreach ($tagData as $parameters) { + $data['tags'][] = array('name' => $tagName, 'parameters' => $parameters); + } + } + } + } + + return $data; + } + + /** + * @param Alias $alias + * + * @return array + */ + private function getContainerAliasData(Alias $alias) + { + return array( + 'service' => (string) $alias, + 'public' => $alias->isPublic(), + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php new file mode 100644 index 0000000000000..23a9a85719118 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; + +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + */ +class MarkdownDescriptor extends Descriptor +{ + /** + * {@inheritdoc} + */ + protected function describeRouteCollection(RouteCollection $routes, array $options = array()) + { + $first = true; + foreach ($routes->all() as $name => $route) { + if ($first) { + $first = false; + } else { + $this->write("\n\n"); + } + $this->describeRoute($route, array('name' => $name)); + } + $this->write("\n"); + } + + /** + * {@inheritdoc} + */ + protected function describeRoute(Route $route, array $options = array()) + { + $requirements = $route->getRequirements(); + unset($requirements['_scheme'], $requirements['_method']); + + $output = '- Path: '.$route->getPath() + ."\n".'- Host: '.('' !== $route->getHost() ? $route->getHost() : 'ANY') + ."\n".'- Scheme: '.($route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY') + ."\n".'- Method: '.($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY') + ."\n".'- Class: '.get_class($route) + ."\n".'- Defaults: '.$this->formatRouterConfig($route->getDefaults()) + ."\n".'- Requirements: '.$this->formatRouterConfig($requirements) ?: 'NONE' + ."\n".'- Options: '.$this->formatRouterConfig($route->getOptions()) + ."\n".'- Path-Regex: '.$route->compile()->getRegex(); + + $this->write(isset($options['name']) + ? $options['name']."\n".str_repeat('-', strlen($options['name']))."\n\n".$output + : $output); + $this->write("\n"); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameters(ParameterBag $parameters, array $options = array()) + { + $this->write("Container parameters\n====================\n"); + foreach ($this->sortParameters($parameters) as $key => $value) { + $this->write(sprintf("\n- `%s`: `%s`", $key, $this->formatParameter($value))); + } + } + + /** + * {@inheritdoc} + */ + protected function describeContainerTags(ContainerBuilder $builder, array $options = array()) + { + $showPrivate = isset($options['show_private']) && $options['show_private']; + $this->write("Container tags\n=============="); + + foreach ($this->findDefinitionsByTag($builder, $showPrivate) as $tag => $definitions) { + $this->write("\n\n".$tag."\n".str_repeat('-', strlen($tag))); + foreach ($definitions as $serviceId => $definition) { + $this->write("\n\n"); + $this->describeContainerDefinition($definition, array('omit_tags' => true, 'id' => $serviceId)); + } + } + } + + /** + * {@inheritdoc} + */ + protected function describeContainerService($service, array $options = array()) + { + if (!isset($options['id'])) { + throw new \InvalidArgumentException('An "id" option must be provided.'); + } + + $childOptions = array('id' => $options['id'], 'as_array' => true); + + if ($service instanceof Alias) { + $this->describeContainerAlias($service, $childOptions); + } elseif ($service instanceof Definition) { + $this->describeContainerDefinition($service, $childOptions); + } else { + $this->write(sprintf("**`%s`:** `%s`", $options['id'], get_class($service))); + } + } + + /** + * {@inheritdoc} + */ + protected function describeContainerServices(ContainerBuilder $builder, array $options = array()) + { + $showPrivate = isset($options['show_private']) && $options['show_private']; + + $title = $showPrivate ? 'Public and private services' : 'Public services'; + if (isset($options['tag'])) { + $title .= ' with tag `'.$options['tag'].'`'; + } + $this->write($title."\n".str_repeat('=', strlen($title))); + + $serviceIds = isset($options['tag']) && $options['tag'] ? array_keys($builder->findTaggedServiceIds($options['tag'])) : $builder->getServiceIds(); + $showPrivate = isset($options['show_private']) && $options['show_private']; + $services = array('definitions' => array(), 'aliases' => array(), 'services' => array()); + + foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + $service = $this->resolveServiceDefinition($builder, $serviceId); + + if ($service instanceof Alias) { + $services['aliases'][$serviceId] = $service; + } elseif ($service instanceof Definition) { + if (($showPrivate || $service->isPublic())) { + $services['definitions'][$serviceId] = $service; + } + } else { + $services['services'][$serviceId] = $service; + } + } + + if (!empty($services['definitions'])) { + $this->write("\n\nDefinitions\n-----------\n"); + foreach ($services['definitions'] as $id => $service) { + $this->write("\n"); + $this->describeContainerDefinition($service, array('id' => $id)); + } + } + + if (!empty($services['aliases'])) { + $this->write("\n\nAliases\n-------\n"); + foreach ($services['aliases'] as $id => $service) { + $this->write("\n"); + $this->describeContainerAlias($service, array('id' => $id)); + } + } + + if (!empty($services['services'])) { + $this->write("\n\nServices\n--------\n"); + foreach ($services['services'] as $id => $service) { + $this->write("\n"); + $this->write(sprintf('- `%s`: `%s`', $id, get_class($service))); + } + } + } + + /** + * {@inheritdoc} + */ + protected function describeContainerDefinition(Definition $definition, array $options = array()) + { + $output = '- Class: `'.$definition->getClass().'`' + ."\n".'- Scope: `'.$definition->getScope().'`' + ."\n".'- Public: '.($definition->isPublic() ? 'yes' : 'no') + ."\n".'- Synthetic: '.($definition->isSynthetic() ? 'yes' : 'no'); + + if ($definition->getFile()) { + $output .= "\n".'- File: `'.$definition->getFile().'`'; + } + + if (!(isset($options['omit_tags']) && $options['omit_tags'])) { + foreach ($definition->getTags() as $tagName => $tagData) { + foreach ($tagData as $parameters) { + $output .= "\n".'- Tag: `'.$tagName.'`'; + foreach ($parameters as $name => $value) { + $output .= "\n".' - '.ucfirst($name).': '.$value; + } + } + } + } + + $this->write(isset($options['id']) ? sprintf("%s\n%s\n\n%s\n", $options['id'], str_repeat('~', strlen($options['id'])), $output) : $output); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerAlias(Alias $alias, array $options = array()) + { + $output = '- Service: `'.$alias.'`' + ."\n".'- Public: '.($alias->isPublic() ? 'yes' : 'no'); + + $this->write(isset($options['id']) ? sprintf("%s\n%s\n\n%s\n", $options['id'], str_repeat('~', strlen($options['id'])), $output) : $output); + } + + private function formatRouterConfig(array $array) + { + if (!count($array)) { + return 'NONE'; + } + + $string = ''; + ksort($array); + foreach ($array as $name => $value) { + $string .= "\n".' - `'.$name.'`: '.$this->formatValue($value); + } + + return $string; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php new file mode 100644 index 0000000000000..d34d42ef8a524 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -0,0 +1,317 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; + +use Symfony\Component\Console\Helper\TableHelper; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + */ +class TextDescriptor extends Descriptor +{ + /** + * {@inheritdoc} + */ + protected function describeRouteCollection(RouteCollection $routes, array $options = array()) + { + $showControllers = isset($options['show_controllers']) && $options['show_controllers']; + $headers = array('Name', 'Method', 'Scheme', 'Host', 'Path'); + $table = new TableHelper(); + $table->setLayout(TableHelper::LAYOUT_COMPACT); + $table->setHeaders($showControllers ? array_merge($headers, array('Controller')) : $headers); + + foreach ($routes->all() as $name => $route) { + $row = array( + $name, + $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY', + $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY', + '' !== $route->getHost() ? $route->getHost() : 'ANY', + $route->getPath(), + ); + + if ($showControllers) { + $controller = $route->getDefault('_controller'); + if ($controller instanceof \Closure) { + $controller = 'Closure'; + } elseif (is_object($controller)) { + $controller = get_class($controller); + } + $row[] = $controller; + } + + $table->addRow($row); + } + + $this->writeText($this->formatSection('router', 'Current routes')."\n", $options); + $this->renderTable($table, !(isset($options['raw_output']) && $options['raw_output'])); + } + + /** + * {@inheritdoc} + */ + protected function describeRoute(Route $route, array $options = array()) + { + $requirements = $route->getRequirements(); + unset($requirements['_scheme'], $requirements['_method']); + + // fixme: values were originally written as raw + $description = array( + 'Path '.$route->getPath(), + 'Host '.('' !== $route->getHost() ? $route->getHost() : 'ANY'), + 'Scheme '.($route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'), + 'Method '.($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY'), + 'Class '.get_class($route), + 'Defaults '.$this->formatRouterConfig($route->getDefaults()), + 'Requirements '.$this->formatRouterConfig($requirements) ?: 'NO CUSTOM', + 'Options '.$this->formatRouterConfig($route->getOptions()), + 'Path-Regex '.$route->compile()->getRegex(), + ); + + if (isset($options['name'])) { + array_unshift($description, 'Name '.$options['name']); + array_unshift($description, $this->formatSection('router', sprintf('Route "%s"', $options['name']))); + } + + if (null !== $route->compile()->getHostRegex()) { + $description[] = 'Host-Regex '.$route->compile()->getHostRegex(); + } + + $this->writeText(implode("\n", $description)."\n", $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameters(ParameterBag $parameters, array $options = array()) + { + $table = new TableHelper(); + $table->setLayout(TableHelper::LAYOUT_COMPACT); + $table->setHeaders(array('Parameter', 'Value')); + + foreach ($this->sortParameters($parameters) as $parameter => $value) { + $table->addRow(array($parameter, $this->formatParameter($value))); + } + + $this->writeText($this->formatSection('container', 'List of parameters')."\n", $options); + $this->renderTable($table, !(isset($options['raw_output']) && $options['raw_output'])); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerTags(ContainerBuilder $builder, array $options = array()) + { + $showPrivate = isset($options['show_private']) && $options['show_private']; + $description = array($this->formatSection('container', 'Tagged services')); + + foreach ($this->findDefinitionsByTag($builder, $showPrivate) as $tag => $definitions) { + $description[] = $this->formatSection('tag', $tag); + $description = array_merge($description, array_keys($definitions)); + $description[] = ''; + } + + $this->writeText(implode("\n", $description), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerService($service, array $options = array()) + { + if (!isset($options['id'])) { + throw new \InvalidArgumentException('An "id" option must be provided.'); + } + + if ($service instanceof Alias) { + $this->describeContainerAlias($service, $options); + } elseif ($service instanceof Definition) { + $this->describeContainerDefinition($service, $options); + } else { + $description = $this->formatSection('container', sprintf('Information for service %s', $options['id'])) + ."\n".sprintf('Service Id %s', isset($options['id']) ? $options['id'] : '-') + ."\n".sprintf('Class %s', get_class($service)); + + $this->writeText($description, $options); + } + } + + /** + * {@inheritdoc} + */ + protected function describeContainerServices(ContainerBuilder $builder, array $options = array()) + { + $showPrivate = isset($options['show_private']) && $options['show_private']; + $showTag = isset($options['tag']) ? $options['tag'] : null; + + if ($showPrivate) { + $label = 'Public and private services'; + } else { + $label = 'Public services'; + } + + if ($showTag) { + $label .= ' with tag '.$options['tag'].''; + } + + $this->writeText($this->formatSection('container', $label)."\n", $options); + + $serviceIds = isset($options['tag']) && $options['tag'] ? array_keys($builder->findTaggedServiceIds($options['tag'])) : $builder->getServiceIds(); + $maxTags = array(); + + foreach ($serviceIds as $key => $serviceId) { + $definition = $this->resolveServiceDefinition($builder, $serviceId); + if ($definition instanceof Definition) { + // filter out private services unless shown explicitly + if (!$showPrivate && !$definition->isPublic()) { + unset($serviceIds[$key]); + continue; + } + if ($showTag) { + $tags = $definition->getTag($showTag); + foreach ($tags as $tag) { + foreach ($tag as $key => $value) { + if (!isset($maxTags[$key])) { + $maxTags[$key] = strlen($key); + } + if (strlen($value) > $maxTags[$key]) { + $maxTags[$key] = strlen($value); + } + } + } + } + } + } + + $tagsCount = count($maxTags); + $tagsNames = array_keys($maxTags); + + $table = new TableHelper(); + $table->setLayout(TableHelper::LAYOUT_COMPACT); + $table->setHeaders(array_merge(array('Service ID'), $tagsNames, array('Scope', 'Class name'))); + + foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + $definition = $this->resolveServiceDefinition($builder, $serviceId); + if ($definition instanceof Definition) { + if ($showTag) { + foreach ($definition->getTag($showTag) as $key => $tag) { + $tagValues = array(); + foreach ($tagsNames as $tagName) { + $tagValues[] = isset($tag[$tagName]) ? $tag[$tagName] : ""; + } + if (0 === $key) { + $table->addRow(array_merge(array($serviceId), $tagValues, array($definition->getScope(), $definition->getClass()))); + } else { + $table->addRow(array_merge(array(' "'), $tagValues, array('', ''))); + } + } + } else { + $table->addRow(array($serviceId, $definition->getScope(), $definition->getClass())); + } + } elseif ($definition instanceof Alias) { + $alias = $definition; + $table->addRow(array_merge(array($serviceId, 'n/a', sprintf('alias for "%s"', $alias)), $tagsCount ? array_fill(0, $tagsCount, "") : array())); + } else { + // we have no information (happens with "service_container") + $table->addRow(array_merge(array($serviceId, '', get_class($definition)), $tagsCount ? array_fill(0, $tagsCount, "") : array())); + } + } + + $this->renderTable($table); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerDefinition(Definition $definition, array $options = array()) + { + $description = isset($options['id']) + ? array($this->formatSection('container', sprintf('Information for service %s', $options['id']))) + : array(); + + $description[] = sprintf('Service Id %s', isset($options['id']) ? $options['id'] : '-'); + $description[] = sprintf('Class %s', $definition->getClass() ?: "-"); + + $tags = $definition->getTags(); + if (count($tags)) { + $description[] = 'Tags'; + foreach ($tags as $tagName => $tagData) { + foreach ($tagData as $parameters) { + $description[] = sprintf(' - %-30s (%s)', $tagName, implode(', ', array_map(function ($key, $value) { + return sprintf('%s: %s', $key, $value); + }, array_keys($parameters), array_values($parameters)))); + } + } + } else { + $description[] = 'Tags -'; + } + + $description[] = sprintf('Scope %s', $definition->getScope()); + $description[] = sprintf('Public %s', $definition->isPublic() ? 'yes' : 'no'); + $description[] = sprintf('Synthetic %s', $definition->isSynthetic() ? 'yes' : 'no'); + $description[] = sprintf('Required File %s', $definition->getFile() ? $definition->getFile() : '-'); + + $this->writeText(implode("\n", $description)."\n", $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerAlias(Alias $alias, array $options = array()) + { + $this->writeText(sprintf('This service is an alias for the service %s', (string) $alias), $options); + } + + /** + * @param array $array + * + * @return string + */ + private function formatRouterConfig(array $array) + { + $string = ''; + ksort($array); + foreach ($array as $name => $value) { + $string .= ($string ? "\n".str_repeat(' ', 13) : '').$name.': '.$this->formatValue($value); + } + + return $string; + } + + /** + * @param string $section + * @param string $message + * + * @return string + */ + private function formatSection($section, $message) + { + return sprintf('[%s] %s', $section, $message); + } + + /** + * @param string $content + * @param array $options + */ + private function writeText($content, array $options = array()) + { + $this->write( + isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, + isset($options['raw_output']) ? !$options['raw_output'] : true + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php new file mode 100644 index 0000000000000..b10f2816a0462 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -0,0 +1,368 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; + +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + */ +class XmlDescriptor extends Descriptor +{ + /** + * {@inheritdoc} + */ + protected function describeRouteCollection(RouteCollection $routes, array $options = array()) + { + $this->writeDocument($this->getRouteCollectionDocument($routes)); + } + + /** + * {@inheritdoc} + */ + protected function describeRoute(Route $route, array $options = array()) + { + $this->writeDocument($this->getRouteDocument($route, isset($options['name']) ? $options['name'] : null)); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameters(ParameterBag $parameters, array $options = array()) + { + $this->writeDocument($this->getContainerParametersDocument($parameters)); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerTags(ContainerBuilder $builder, array $options = array()) + { + $this->writeDocument($this->getContainerTagsDocument($builder, isset($options['show_private']) && $options['show_private'])); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerService($service, array $options = array()) + { + if (!isset($options['id'])) { + throw new \InvalidArgumentException('An "id" option must be provided.'); + } + + $this->writeDocument($this->getContainerServiceDocument($service, $options['id'])); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerServices(ContainerBuilder $builder, array $options = array()) + { + $this->writeDocument($this->getContainerServicesDocument($builder, isset($options['tag']) ? $options['tag'] : null, isset($options['show_private']) && $options['show_private'])); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerDefinition(Definition $definition, array $options = array()) + { + $this->writeDocument($this->getContainerDefinitionDocument($definition, isset($options['id']) ? $options['id'] : null, isset($options['omit_tags']) && $options['omit_tags'])); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerAlias(Alias $alias, array $options = array()) + { + $this->writeDocument($this->getContainerAliasDocument($alias, isset($options['id']) ? $options['id'] : null)); + } + + /** + * Writes DOM document. + * + * @param \DOMDocument $dom + * + * @return \DOMDocument|string + */ + private function writeDocument(\DOMDocument $dom) + { + $dom->formatOutput = true; + $this->write($dom->saveXML()); + } + + /** + * @param RouteCollection $routes + * + * @return \DOMDocument + */ + private function getRouteCollectionDocument(RouteCollection $routes) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($routesXML = $dom->createElement('routes')); + + foreach ($routes->all() as $name => $route) { + $routeXML = $this->getRouteDocument($route, $name); + $routesXML->appendChild($routesXML->ownerDocument->importNode($routeXML->childNodes->item(0), true)); + } + + return $dom; + } + + /** + * @param Route $route + * @param string|null $name + * + * @return \DOMDocument + */ + private function getRouteDocument(Route $route, $name = null) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($routeXML = $dom->createElement('route')); + + if ($name) { + $routeXML->setAttribute('name', $name); + } + + $routeXML->setAttribute('class', get_class($route)); + + $routeXML->appendChild($pathXML = $dom->createElement('path')); + $pathXML->setAttribute('regex', $route->compile()->getRegex()); + $pathXML->appendChild(new \DOMText($route->getPath())); + + if ('' !== $route->getHost()) { + $routeXML->appendChild($hostXML = $dom->createElement('host')); + $hostXML->setAttribute('regex', $route->compile()->getHostRegex()); + $hostXML->appendChild(new \DOMText($route->getHost())); + } + + foreach ($route->getSchemes() as $scheme) { + $routeXML->appendChild($schemeXML = $dom->createElement('scheme')); + $schemeXML->appendChild(new \DOMText($scheme)); + } + + foreach ($route->getMethods() as $method) { + $routeXML->appendChild($methodXML = $dom->createElement('method')); + $methodXML->appendChild(new \DOMText($method)); + } + + if (count($route->getDefaults())) { + $routeXML->appendChild($defaultsXML = $dom->createElement('defaults')); + foreach ($route->getDefaults() as $attribute => $value) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->setAttribute('key', $attribute); + $defaultXML->appendChild(new \DOMText($this->formatValue($value))); + } + } + + $requirements = $route->getRequirements(); + unset($requirements['_scheme'], $requirements['_method']); + if (count($requirements)) { + $routeXML->appendChild($requirementsXML = $dom->createElement('requirements')); + foreach ($requirements as $attribute => $pattern) { + $requirementsXML->appendChild($requirementXML = $dom->createElement('requirement')); + $requirementXML->setAttribute('key', $attribute); + $requirementXML->appendChild(new \DOMText($pattern)); + } + } + + if (count($route->getOptions())) { + $routeXML->appendChild($optionsXML = $dom->createElement('options')); + foreach ($route->getOptions() as $name => $value) { + $optionsXML->appendChild($optionXML = $dom->createElement('option')); + $optionXML->setAttribute('key', $name); + $optionXML->appendChild(new \DOMText($this->formatValue($value))); + } + } + + return $dom; + } + + /** + * @param ParameterBag $parameters + * + * @return \DOMDocument + */ + private function getContainerParametersDocument(ParameterBag $parameters) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($parametersXML = $dom->createElement('parameters')); + + foreach ($this->sortParameters($parameters) as $key => $value) { + $parametersXML->appendChild($parameterXML = $dom->createElement('parameter')); + $parameterXML->setAttribute('key', $key); + $parameterXML->appendChild(new \DOMText($this->formatParameter($value))); + } + + return $dom; + } + + /** + * @param ContainerBuilder $builder + * @param boolean $showPrivate + * + * @return \DOMDocument + */ + private function getContainerTagsDocument(ContainerBuilder $builder, $showPrivate = false) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($containerXML = $dom->createElement('container')); + + foreach ($this->findDefinitionsByTag($builder, $showPrivate) as $tag => $definitions) { + $containerXML->appendChild($tagXML = $dom->createElement('tag')); + $tagXML->setAttribute('name', $tag); + + foreach ($definitions as $serviceId => $definition) { + $definitionXML = $this->getContainerDefinitionDocument($definition, $serviceId, true); + $tagXML->appendChild($dom->importNode($definitionXML->childNodes->item(0), true)); + } + } + + return $dom; + } + + /** + * @param mixed $service + * @param string $id + * + * @return \DOMDocument + */ + private function getContainerServiceDocument($service, $id) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + + if ($service instanceof Alias) { + $dom->appendChild($dom->importNode($this->getContainerAliasDocument($service, $id)->childNodes->item(0), true)); + } elseif ($service instanceof Definition) { + $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($service, $id)->childNodes->item(0), true)); + } else { + $dom->appendChild($serviceXML = $dom->createElement('service')); + $serviceXML->setAttribute('id', $id); + $serviceXML->setAttribute('class', get_class($service)); + } + + return $dom; + } + + /** + * @param ContainerBuilder $builder + * @param string|null $tag + * @param boolean $showPrivate + * + * @return \DOMDocument + */ + private function getContainerServicesDocument(ContainerBuilder $builder, $tag = null, $showPrivate = false) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($containerXML = $dom->createElement('container')); + + $serviceIds = $tag ? array_keys($builder->findTaggedServiceIds($tag)) : $builder->getServiceIds(); + + foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + $service = $this->resolveServiceDefinition($builder, $serviceId); + + if ($service instanceof Definition && !($showPrivate || $service->isPublic())) { + continue; + } + + $serviceXML = $this->getContainerServiceDocument($service, $serviceId); + $containerXML->appendChild($containerXML->ownerDocument->importNode($serviceXML->childNodes->item(0), true)); + } + + return $dom; + } + + /** + * @param Definition $definition + * @param string|null $id + * @param boolean $omitTags + * + * @return \DOMDocument + */ + private function getContainerDefinitionDocument(Definition $definition, $id = null, $omitTags = false) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($serviceXML = $dom->createElement('definition')); + + if ($id) { + $serviceXML->setAttribute('id', $id); + } + + $serviceXML->setAttribute('class', $definition->getClass()); + + if ($definition->getFactoryClass()) { + $serviceXML->setAttribute('factory-class', $definition->getFactoryClass()); + } + + if ($definition->getFactoryService()) { + $serviceXML->setAttribute('factory-service', $definition->getFactoryService()); + } + + if ($definition->getFactoryMethod()) { + $serviceXML->setAttribute('factory-method', $definition->getFactoryMethod()); + } + + $serviceXML->setAttribute('scope', $definition->getScope()); + $serviceXML->setAttribute('public', $definition->isPublic() ? 'true' : 'false'); + $serviceXML->setAttribute('synthetic', $definition->isSynthetic() ? 'true' : 'false'); + $serviceXML->setAttribute('lazy', $definition->isLazy() ? 'true' : 'false'); + $serviceXML->setAttribute('synchronized', $definition->isSynchronized() ? 'true' : 'false'); + $serviceXML->setAttribute('abstract', $definition->isAbstract() ? 'true' : 'false'); + $serviceXML->setAttribute('file', $definition->getFile()); + + if (!$omitTags) { + $tags = $definition->getTags(); + + if (count($tags) > 0) { + $serviceXML->appendChild($tagsXML = $dom->createElement('tags')); + foreach ($tags as $tagName => $tagData) { + foreach ($tagData as $parameters) { + $tagsXML->appendChild($tagXML = $dom->createElement('tag')); + $tagXML->setAttribute('name', $tagName); + foreach ($parameters as $name => $value) { + $tagXML->appendChild($parameterXML = $dom->createElement('parameter')); + $parameterXML->setAttribute('name', $name); + $parameterXML->appendChild(new \DOMText($this->formatParameter($value))); + } + } + } + } + } + + return $dom; + } + + /** + * @param Alias $alias + * @param string|null $id + * + * @return \DOMDocument + */ + private function getContainerAliasDocument(Alias $alias, $id = null) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($aliasXML = $dom->createElement('alias')); + + if ($id) { + $aliasXML->setAttribute('id', $id); + } + + $aliasXML->setAttribute('service', (string) $alias); + $aliasXML->setAttribute('public', $alias->isPublic() ? 'true' : 'false'); + + return $dom; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Helper/DescriptorHelper.php b/src/Symfony/Bundle/FrameworkBundle/Console/Helper/DescriptorHelper.php new file mode 100644 index 0000000000000..acb2a517ab931 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Helper/DescriptorHelper.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\Console\Helper; + +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\JsonDescriptor; +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\MarkdownDescriptor; +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\TextDescriptor; +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\Helper\DescriptorHelper as BaseDescriptorHelper; + +/** + * @author Jean-François Simon + */ +class DescriptorHelper extends BaseDescriptorHelper +{ + /** + * Constructor. + */ + public function __construct() + { + $this + ->register('txt', new TextDescriptor()) + ->register('xml', new XmlDescriptor()) + ->register('json', new JsonDescriptor()) + ->register('md', new MarkdownDescriptor()) + ; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index 5655595a68531..186c55326c879 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormBuilder; @@ -139,8 +140,8 @@ public function stream($view, array $parameters = array(), StreamedResponse $res * * throw $this->createNotFoundException('Page not found!'); * - * @param string $message A message - * @param \Exception $previous The previous exception + * @param string $message A message + * @param \Exception|null $previous The previous exception * * @return NotFoundHttpException */ @@ -149,6 +150,23 @@ public function createNotFoundException($message = 'Not Found', \Exception $prev return new NotFoundHttpException($message, $previous); } + /** + * Returns an AccessDeniedException. + * + * This will result in a 403 response code. Usage example: + * + * throw $this->createAccessDeniedException('Unable to access this page!'); + * + * @param string $message A message + * @param \Exception|null $previous The previous exception + * + * @return AccessDeniedException + */ + public function createAccessDeniedException($message = 'Access Denied', \Exception $previous = null) + { + return new AccessDeniedException($message, $previous); + } + /** * Creates and returns a Form instance from the type of the form. * @@ -180,10 +198,14 @@ public function createFormBuilder($data = null, array $options = array()) * Shortcut to return the request service. * * @return Request + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. Ask + * Symfony to inject the Request object into your controller + * method instead by type hinting it in the method's signature. */ public function getRequest() { - return $this->container->get('request'); + return $this->container->get('request_stack')->getCurrentRequest(); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddConsoleCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddConsoleCommandPass.php new file mode 100644 index 0000000000000..3c96761ba3a6c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddConsoleCommandPass.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * AddConsoleCommandPass. + * + * @author Grégoire Pineau + */ +class AddConsoleCommandPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $commandServices = $container->findTaggedServiceIds('console.command'); + + foreach ($commandServices as $id => $tags) { + $definition = $container->getDefinition($id); + + if (!$definition->isPublic()) { + throw new \InvalidArgumentException(sprintf('The service "%s" tagged "console.command" must be public.', $id)); + } + + if ($definition->isAbstract()) { + throw new \InvalidArgumentException(sprintf('The service "%s" tagged "console.command" must not be abstract.', $id)); + } + + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + $r = new \ReflectionClass($class); + if (!$r->isSubclassOf('Symfony\\Component\\Console\\Command\\Command')) { + throw new \InvalidArgumentException(sprintf('The service "%s" tagged "console.command" must be a subclass of "Symfony\\Component\\Console\\Command\\Command".', $id)); + } + $container->setAlias('console.command.'.strtolower(str_replace('\\', '_', $class)), $id); + } + + $container->setParameter('console.command.ids', array_keys($commandServices)); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 852f8ba4246c4..0c1d1578c6f0a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -36,7 +36,7 @@ public function getConfigTreeBuilder() ->children() ->scalarNode('secret')->end() ->scalarNode('http_method_override') - ->info("Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests.") + ->info("Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. Note: When using the HttpCache, you need to call the method in your front controller instead") ->defaultTrue() ->end() ->arrayNode('trusted_proxies') @@ -78,6 +78,7 @@ public function getConfigTreeBuilder() ->end() ; + $this->addCsrfSection($rootNode); $this->addFormSection($rootNode); $this->addEsiSection($rootNode); $this->addFragmentsSection($rootNode); @@ -93,6 +94,23 @@ public function getConfigTreeBuilder() return $treeBuilder; } + private function addCsrfSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('csrf_protection') + ->canBeEnabled() + ->children() + ->scalarNode('field_name') + ->defaultValue('_token') + ->info('Deprecated since 2.4, to be removed in 3.0. Use form.csrf_protection.field_name instead') + ->end() + ->end() + ->end() + ->end() + ; + } + private function addFormSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -100,11 +118,17 @@ private function addFormSection(ArrayNodeDefinition $rootNode) ->arrayNode('form') ->info('form configuration') ->canBeEnabled() - ->end() - ->arrayNode('csrf_protection') - ->canBeDisabled() ->children() - ->scalarNode('field_name')->defaultValue('_token')->end() + ->arrayNode('csrf_protection') + ->treatFalseLike(array('enabled' => false)) + ->treatTrueLike(array('enabled' => true)) + ->treatNullLike(array('enabled' => true)) + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled')->defaultNull()->end() // defaults to framework.csrf_protection.enabled + ->scalarNode('field_name')->defaultNull()->end() + ->end() + ->end() ->end() ->end() ->end() @@ -156,13 +180,17 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode) ->arrayNode('matcher') ->canBeUnset() ->performNoDeepMerging() + ->fixXmlConfig('ip') ->children() - ->scalarNode('ip')->end() ->scalarNode('path') ->info('use the urldecoded format') ->example('^/path to resource/') ->end() ->scalarNode('service')->end() + ->arrayNode('ips') + ->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end() + ->prototype('scalar')->end() + ->end() ->end() ->end() ->end() @@ -218,6 +246,10 @@ private function addSessionSection(ArrayNodeDefinition $rootNode) ->scalarNode('gc_probability')->end() ->scalarNode('gc_maxlifetime')->end() ->scalarNode('save_path')->defaultValue('%kernel.cache_dir%/sessions')->end() + ->integerNode('metadata_update_threshold') + ->defaultValue('0') + ->info('seconds to wait between 2 session metadata updates, it will also prevent the session handler to write if the session has not changed') + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 0c22c27e86e14..5dc5c05513f06 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -29,6 +30,9 @@ */ class FrameworkExtension extends Extension { + private $formConfigEnabled = false; + private $sessionConfigEnabled = false; + /** * Responds to the app.config configuration parameter. * @@ -48,17 +52,22 @@ public function load(array $configs, ContainerBuilder $container) // will be used and everything will still work as expected. $loader->load('translation.xml'); + // Property access is used by both the Form and the Validator component + $loader->load('property_access.xml'); + $loader->load('debug_prod.xml'); if ($container->getParameter('kernel.debug')) { $loader->load('debug.xml'); - // only HttpKernel needs the debug event dispatcher $definition = $container->findDefinition('http_kernel'); - $arguments = $definition->getArguments(); - $arguments[0] = new Reference('debug.event_dispatcher'); - $arguments[2] = new Reference('debug.controller_resolver'); - $definition->setArguments($arguments); + $definition->replaceArgument(2, new Reference('debug.controller_resolver')); + + // replace the regular event_dispatcher service with the debug one + $definition = $container->findDefinition('event_dispatcher'); + $definition->setPublic(false); + $container->setDefinition('debug.event_dispatcher.parent', $definition); + $container->setAlias('event_dispatcher', 'debug.event_dispatcher'); } $configuration = $this->getConfiguration($configs, $container); @@ -78,14 +87,24 @@ public function load(array $configs, ContainerBuilder $container) } if (isset($config['session'])) { + $this->sessionConfigEnabled = true; $this->registerSessionConfiguration($config['session'], $container, $loader); } + $loader->load('security.xml'); + if ($this->isConfigEnabled($container, $config['form'])) { + $this->formConfigEnabled = true; $this->registerFormConfiguration($config, $container, $loader); $config['validation']['enabled'] = true; + + if ($this->isConfigEnabled($container, $config['form']['csrf_protection'])) { + $config['csrf_protection']['enabled'] = true; + } } + $this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader); + if (isset($config['templating'])) { $this->registerTemplatingConfiguration($config['templating'], $config['ide'], $container, $loader); } @@ -143,17 +162,20 @@ public function load(array $configs, ContainerBuilder $container) private function registerFormConfiguration($config, ContainerBuilder $container, XmlFileLoader $loader) { $loader->load('form.xml'); - if ($this->isConfigEnabled($container, $config['csrf_protection'])) { - if (!isset($config['session'])) { - throw new \LogicException('CSRF protection needs that sessions are enabled.'); - } - if (!isset($config['secret'])) { - throw new \LogicException('CSRF protection needs a secret to be set.'); - } + if (null === $config['form']['csrf_protection']['enabled']) { + $config['form']['csrf_protection']['enabled'] = $config['csrf_protection']['enabled']; + } + + if ($this->isConfigEnabled($container, $config['form']['csrf_protection'])) { $loader->load('form_csrf.xml'); $container->setParameter('form.type_extension.csrf.enabled', true); - $container->setParameter('form.type_extension.csrf.field_name', $config['csrf_protection']['field_name']); + + if (null !== $config['form']['csrf_protection']['field_name']) { + $container->setParameter('form.type_extension.csrf.field_name', $config['form']['csrf_protection']['field_name']); + } else { + $container->setParameter('form.type_extension.csrf.field_name', $config['csrf_protection']['field_name']); + } } else { $container->setParameter('form.type_extension.csrf.enabled', false); } @@ -210,6 +232,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ return; } + if (true === $this->formConfigEnabled) { + $loader->load('form_debug.xml'); + } + $loader->load('profiling.xml'); $loader->load('collectors.xml'); @@ -241,7 +267,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ if (isset($config['matcher'])) { if (isset($config['matcher']['service'])) { $container->setAlias('profiler.request_matcher', $config['matcher']['service']); - } elseif (isset($config['matcher']['ip']) || isset($config['matcher']['path'])) { + } elseif (isset($config['matcher']['ip']) || isset($config['matcher']['path']) || isset($config['matcher']['ips'])) { $definition = $container->register('profiler.request_matcher', 'Symfony\\Component\\HttpFoundation\\RequestMatcher'); $definition->setPublic(false); @@ -249,6 +275,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $definition->addMethodCall('matchIp', array($config['matcher']['ip'])); } + if (isset($config['matcher']['ips'])) { + $definition->addMethodCall('matchIps', array($config['matcher']['ips'])); + } + if (isset($config['matcher']['path'])) { $definition->addMethodCall('matchPath', array($config['matcher']['path'])); } @@ -321,7 +351,14 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c $container->getDefinition('session.storage.native')->replaceArgument(1, null); $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); } else { - $container->setAlias('session.handler', $config['handler_id']); + $handlerId = $config['handler_id']; + + if ($config['metadata_update_threshold'] > 0) { + $container->getDefinition('session.handler.write_check')->addArgument(new Reference($handlerId)); + $handlerId = 'session.handler.write_check'; + } + + $container->setAlias('session.handler', $handlerId); } $container->setParameter('session.save_path', $config['save_path']); @@ -341,6 +378,8 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c $container->findDefinition('session.storage')->getClass(), )); } + + $container->setParameter('session.metadata.update_threshold', $config['metadata_update_threshold']); } /** @@ -368,6 +407,15 @@ private function registerTemplatingConfiguration(array $config, $ide, ContainerB if ($container->getParameter('kernel.debug')) { $loader->load('templating_debug.xml'); + $logger = new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE); + + $container->getDefinition('templating.loader.cache') + ->addTag('monolog.logger', array('channel' => 'templating')) + ->addMethodCall('setLogger', array($logger)); + $container->getDefinition('templating.loader.chain') + ->addTag('monolog.logger', array('channel' => 'templating')) + ->addMethodCall('setLogger', array($logger)); + $container->setDefinition('templating.engine.php', $container->findDefinition('debug.templating.engine.php')); $container->setAlias('debug.templating.engine.php', 'templating.engine.php'); } @@ -552,7 +600,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder if (class_exists('Symfony\Component\Security\Core\Exception\AuthenticationException')) { $r = new \ReflectionClass('Symfony\Component\Security\Core\Exception\AuthenticationException'); - $dirs[] = dirname($r->getFilename()).'/../../Resources/translations'; + $dirs[] = dirname($r->getFilename()).'/../Resources/translations'; } $overridePath = $container->getParameter('kernel.root_dir').'/Resources/%s/translations'; foreach ($container->getParameter('kernel.bundles') as $bundle => $class) { @@ -620,7 +668,7 @@ private function registerValidationConfiguration(array $config, ContainerBuilder ->replaceArgument(1, new Reference('validator.mapping.cache.'.$config['cache'])); $container->setParameter( 'validator.mapping.cache.prefix', - 'validator_'.md5($container->getParameter('kernel.root_dir')) + 'validator_'.hash('sha256', $container->getParameter('kernel.root_dir')) ); } } @@ -661,7 +709,7 @@ private function getValidatorYamlMappingFiles(ContainerBuilder $container) return $files; } - private function registerAnnotationsConfiguration(array $config, ContainerBuilder $container,$loader) + private function registerAnnotationsConfiguration(array $config, ContainerBuilder $container, $loader) { $loader->load('annotations.xml'); @@ -687,6 +735,29 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde } } + /** + * Loads the security configuration. + * + * @param array $config A CSRF configuration array + * @param ContainerBuilder $container A ContainerBuilder instance + * @param XmlFileLoader $loader An XmlFileLoader instance + * + * @throws \LogicException + */ + private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!$this->isConfigEnabled($container, $config)) { + return; + } + + if (!$this->sessionConfigEnabled) { + throw new \LogicException('CSRF protection needs sessions to be enabled.'); + } + + // Enable services for CSRF protection (even without forms) + $loader->load('security_csrf.xml'); + } + /** * Returns the base path for the XSD files. * diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/SessionListener.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/SessionListener.php index 7b5ce51db997d..2fa5846c30575 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/SessionListener.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/SessionListener.php @@ -11,19 +11,15 @@ namespace Symfony\Bundle\FrameworkBundle\EventListener; +use Symfony\Component\HttpKernel\EventListener\SessionListener as BaseSessionListener; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - /** * Sets the session in the request. * - * @author Johannes M. Schmitt + * @author Fabien Potencier */ -class SessionListener implements EventSubscriberInterface +class SessionListener extends BaseSessionListener { /** * @var ContainerInterface @@ -35,24 +31,12 @@ public function __construct(ContainerInterface $container) $this->container = $container; } - public function onKernelRequest(GetResponseEvent $event) + protected function getSession() { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { - return; - } - - $request = $event->getRequest(); - if (!$this->container->has('session') || $request->hasSession()) { - return; + if (!$this->container->has('session')) { + return null; } - $request->setSession($this->container->get('session')); - } - - public static function getSubscribedEvents() - { - return array( - KernelEvents::REQUEST => array('onKernelRequest', 128), - ); + return $this->container->get('session'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/TestSessionListener.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/TestSessionListener.php index d0360c3e2c969..80d940a0e1297 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/TestSessionListener.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/TestSessionListener.php @@ -11,23 +11,15 @@ namespace Symfony\Bundle\FrameworkBundle\EventListener; -use Symfony\Component\HttpFoundation\Cookie; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\EventListener\TestSessionListener as BaseTestSessionListener; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * TestSessionListener. * - * Saves session in test environment. - * - * @author Bulat Shakirzyanov * @author Fabien Potencier */ -class TestSessionListener implements EventSubscriberInterface +class TestSessionListener extends BaseTestSessionListener { protected $container; @@ -36,50 +28,12 @@ public function __construct(ContainerInterface $container) $this->container = $container; } - public function onKernelRequest(GetResponseEvent $event) + protected function getSession() { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { - return; - } - - // bootstrap the session if (!$this->container->has('session')) { - return; - } - - $session = $this->container->get('session'); - $cookies = $event->getRequest()->cookies; - - if ($cookies->has($session->getName())) { - $session->setId($cookies->get($session->getName())); - } - } - - /** - * Checks if session was initialized and saves if current request is master - * Runs on 'kernel.response' in test environment - * - * @param FilterResponseEvent $event - */ - public function onKernelResponse(FilterResponseEvent $event) - { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { - return; + return null; } - $session = $event->getRequest()->getSession(); - if ($session && $session->isStarted()) { - $session->save(); - $params = session_get_cookie_params(); - $event->getResponse()->headers->setCookie(new Cookie($session->getName(), $session->getId(), 0 === $params['lifetime'] ? 0 : time() + $params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly'])); - } - } - - public static function getSubscribedEvents() - { - return array( - KernelEvents::REQUEST => array('onKernelRequest', 192), - KernelEvents::RESPONSE => array('onKernelResponse', -128), - ); + return $this->container->get('session'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 3aaf22a623ab0..30d06fd7af952 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -13,6 +13,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddValidatorInitializersPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConsoleCommandPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingResolverPass; @@ -29,9 +30,9 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Scope; +use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Bundle\Bundle; -use Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass; /** * Bundle. @@ -71,6 +72,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new TemplatingPass()); $container->addCompilerPass(new AddConstraintValidatorsPass()); $container->addCompilerPass(new AddValidatorInitializersPass()); + $container->addCompilerPass(new AddConsoleCommandPass()); $container->addCompilerPass(new FormPass()); $container->addCompilerPass(new TranslatorPass()); $container->addCompilerPass(new AddCacheWarmerPass()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index 0e07cdb5d9f63..def9aea920699 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -13,6 +13,8 @@ Symfony\Component\HttpKernel\DataCollector\TimeDataCollector Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector + Symfony\Component\Form\Extension\DataCollector\FormDataCollector + Symfony\Component\Form\Extension\DataCollector\FormDataExtractor @@ -32,6 +34,7 @@ + @@ -43,6 +46,7 @@ + @@ -53,5 +57,13 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml index e7d1c3c7d47b5..85cced76d5dad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml @@ -16,10 +16,9 @@ - + - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml index dd8e80107525a..3038f40e97b46 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml @@ -7,7 +7,6 @@ Symfony\Component\HttpKernel\HttpCache\Esi Symfony\Component\HttpKernel\EventListener\EsiListener - Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer @@ -17,12 +16,5 @@ - - - - - - %fragment.path% - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 569100fce75fb..93fa5235871cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -10,7 +10,6 @@ Symfony\Component\Form\FormFactory Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser - Symfony\Component\PropertyAccess\PropertyAccessor @@ -54,9 +53,6 @@ - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml index 57cad204aa386..4903004f32bac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml @@ -4,14 +4,9 @@ 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\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider - - - - - %kernel.secret% + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml new file mode 100644 index 0000000000000..5d4faac4acf71 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml @@ -0,0 +1,26 @@ + + + + + + Symfony\Component\Form\Extension\DataCollector\Proxy\ResolvedTypeFactoryDataCollectorProxy + Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml index 595db6d2741c6..a1beee30a1fb8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml @@ -9,6 +9,7 @@ Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer Symfony\Bundle\FrameworkBundle\Fragment\ContainerAwareHIncludeFragmentRenderer + Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer /_fragment @@ -16,7 +17,7 @@ %kernel.debug% - + @@ -33,5 +34,12 @@ %fragment.renderer.hinclude.global_template% %fragment.path% + + + + + + %fragment.path% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml index a8666606e150f..cee86b3b72cfa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml @@ -29,6 +29,7 @@ %profiler_listener.only_exceptions% %profiler_listener.only_master_requests% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml new file mode 100644 index 0000000000000..18026144e4573 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml @@ -0,0 +1,14 @@ + + + + + + Symfony\Component\PropertyAccess\PropertyAccessor + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 9e21db4519151..6b2f7c9688699 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -94,7 +94,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 4b235051f82d5..92c8952e48be9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -31,7 +31,15 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml new file mode 100644 index 0000000000000..2b6307a9ef5e1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml @@ -0,0 +1,19 @@ + + + + + + Symfony\Component\Security\Core\Util\SecureRandom + + + + + + + %kernel.cache_dir%/secure_random.seed + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml new file mode 100644 index 0000000000000..143c8a68efe83 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml @@ -0,0 +1,27 @@ + + + + + + Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator + Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage + Symfony\Component\Security\Csrf\CsrfTokenManager + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 674e28f1c98a0..4457dbb27669c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -12,6 +12,7 @@ Symfony\Component\HttpKernel\CacheClearer\ChainCacheClearer Symfony\Component\HttpKernel\Config\FileLocator Symfony\Component\HttpKernel\UriSigner + Symfony\Component\HttpFoundation\RequestStack @@ -23,8 +24,11 @@ + + + @@ -39,6 +43,8 @@ YourRequestClass to the Kernel. This service definition only defines the scope of the request. It is used to check references scope. + + This service is deprecated, you should use the request_stack service instead. --> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml index de45365751be9..873e099ff9160 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml @@ -8,10 +8,13 @@ Symfony\Component\HttpFoundation\Session\Session Symfony\Component\HttpFoundation\Session\Flash\FlashBag Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag + Symfony\Component\HttpFoundation\Session\Storage\MetadataBag + _sf2_meta Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler + Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler Symfony\Bundle\FrameworkBundle\EventListener\SessionListener @@ -22,13 +25,20 @@ + + %session.storage.options% + + @@ -37,12 +47,16 @@ %kernel.cache_dir%/sessions + MOCKSESSID + %session.save_path% + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml index b06c9af82f952..59da78fc41689 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml @@ -52,11 +52,9 @@ %templating.loader.cache.path% - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_debug.xml index 23da774223b45..054a27b8cd85c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_debug.xml @@ -5,15 +5,10 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Bundle\FrameworkBundle\Templating\Debugger Symfony\Bundle\FrameworkBundle\Templating\TimedPhpEngine - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml index 3caa29fb84332..0ebb44bbd2708 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml @@ -15,6 +15,7 @@ Symfony\Bundle\FrameworkBundle\Templating\Helper\CodeHelper Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper + Symfony\Bundle\FrameworkBundle\Templating\Helper\StopwatchHelper Symfony\Component\Form\Extension\Templating\TemplatingRendererEngine Symfony\Component\Form\FormRenderer Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables @@ -66,12 +67,12 @@ - + - + @@ -101,6 +102,11 @@ + + + + + %templating.helper.form.resources% diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index 5e6ec40ff4cb9..300f24da7f6e7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -18,6 +18,7 @@ Symfony\Component\Translation\Loader\IcuResFileLoader Symfony\Component\Translation\Loader\IcuDatFileLoader Symfony\Component\Translation\Loader\IniFileLoader + Symfony\Component\Translation\Loader\JsonFileLoader Symfony\Component\Translation\Dumper\PhpFileDumper Symfony\Component\Translation\Dumper\XliffFileDumper Symfony\Component\Translation\Dumper\PoFileDumper @@ -26,6 +27,7 @@ Symfony\Component\Translation\Dumper\QtFileDumper Symfony\Component\Translation\Dumper\CsvFileDumper Symfony\Component\Translation\Dumper\IniFileDumper + Symfony\Component\Translation\Dumper\JsonFileDumper Symfony\Component\Translation\Dumper\IcuResFileDumper Symfony\Bundle\FrameworkBundle\Translation\PhpExtractor Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader @@ -90,6 +92,10 @@ + + + + @@ -122,6 +128,10 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index 0bc70040a2068..4c1ac47d1f393 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -17,13 +17,14 @@ Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory + Symfony\Component\Validator\Constraints\ExpressionValidator - + %validator.translation_domain% @@ -63,5 +64,10 @@ %validator.mapping.loader.yaml_files_loader.mapping_files% + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 177821a5afb24..6c1dd73e2e615 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -38,7 +38,7 @@ %kernel.default_locale% - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php index 63d16bd357dc6..ac1077a205ae3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php @@ -1,6 +1,10 @@ -id="escape($id) ?>" -name="escape($full_name) ?>" -disabled="disabled" +id="escape($id) ?>" name="escape($full_name) ?>" disabled="disabled" $v): ?> - escape($k), $view->escape($v)) ?> - + +escape($k), $view->escape($view['translator']->trans($v, array(), $translation_domain))) ?> + +escape($k), $view->escape($k)) ?> + +escape($k), $view->escape($v)) ?> + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php index 9c3af35ffb9aa..98fc0da732fbd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php @@ -1,6 +1,6 @@ - $v) { printf(' %s="%s"', $view->escape($k), $view->escape($v)); } ?> enctype="multipart/form-data"> + $v) { printf(' %s="%s"', $view->escape($k), $view->escape($v)); } ?> enctype="multipart/form-data"> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_simple.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_simple.html.php index 2b3e5abdaef36..5d7654f54cdc6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_simple.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_simple.html.php @@ -1,5 +1 @@ -value="escape($value) ?>" - block($form, 'widget_attributes') ?> -/> +block($form, 'widget_attributes') ?> value="escape($value) ?>" /> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php index 210b84cad5a4a..292dbb9aa8ae4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php @@ -1,10 +1,14 @@ -id="escape($id) ?>" -name="escape($full_name) ?>" -readonly="readonly" +id="escape($id) ?>" name="escape($full_name) ?>" readonly="readonly" disabled="disabled" required="required" maxlength="escape($max_length) ?>" pattern="escape($pattern) ?>" $v): ?> - escape($k), $view->escape(in_array($v, array('placeholder', 'title')) ? $view['translator']->trans($v, array(), $translation_domain) : $v)) ?> - + +escape($k), $view->escape($view['translator']->trans($v, array(), $translation_domain))) ?> + +escape($k), $view->escape($k)) ?> + +escape($k), $view->escape($v)) ?> + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php index 2a8e979b7f904..327925a537196 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php @@ -1,2 +1,10 @@ -id="escape($id) ?>" - $v) { printf('%s="%s" ', $view->escape($k), $view->escape($v)); } ?> +id="escape($id) ?>" + $v): ?> + +escape($k), $view->escape($view['translator']->trans($v, array(), $translation_domain))) ?> + +escape($k), $view->escape($k)) ?> + +escape($k), $view->escape($v)) ?> + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index 7620abca2d320..5dcaaf2f697e4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -41,7 +41,7 @@ public function __construct(ContainerInterface $container, $resource, array $opt $this->container = $container; $this->resource = $resource; - $this->context = null === $context ? new RequestContext() : $context; + $this->context = $context ?: new RequestContext(); $this->setOptions($options); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php index 19a59381d0c56..ff2d5edc4b3a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php @@ -18,6 +18,8 @@ * Binds the Symfony templating loader debugger to the Symfony logger. * * @author Fabien Potencier + * + * @deprecated Deprecated in 2.4, to be removed in 3.0. Use Psr\Log\LoggerInterface instead. */ class Debugger implements DebuggerInterface { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php index 0493d43d29499..c204ffa2f03b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php @@ -39,50 +39,42 @@ public function __construct(ContainerInterface $container, array $engineIds) /** * {@inheritdoc} */ - public function supports($name) + public function getEngine($name) { - foreach ($this->engines as $i => $engine) { - if (is_string($engine)) { - $engine = $this->engines[$i] = $this->container->get($engine); - } - - if ($engine->supports($name)) { - return true; - } - } + $this->resolveEngines(); - return false; + return parent::getEngine($name); } /** * {@inheritdoc} */ - protected function getEngine($name) + public function renderResponse($view, array $parameters = array(), Response $response = null) { - foreach ($this->engines as $i => $engine) { - if (is_string($engine)) { - $engine = $this->engines[$i] = $this->container->get($engine); - } + $engine = $this->getEngine($view); - if ($engine->supports($name)) { - return $engine; - } + if ($engine instanceof EngineInterface) { + return $engine->renderResponse($view, $parameters, $response); } - throw new \RuntimeException(sprintf('No engine is able to work with the template "%s".', $name)); + if (null === $response) { + $response = new Response(); + } + + $response->setContent($engine->render($view, $parameters)); + + return $response; } /** - * Renders a view and returns a Response. - * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * @param Response $response A Response instance - * - * @return Response A Response instance + * Resolved engine ids to their real engine instances from the container. */ - public function renderResponse($view, array $parameters = array(), Response $response = null) + private function resolveEngines() { - return $this->getEngine($view)->renderResponse($view, $parameters, $response); + foreach ($this->engines as $i => $engine) { + if (is_string($engine)) { + $this->engines[$i] = $this->container->get($engine); + } + } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php b/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php index edc087e867da8..dcf58975176bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php @@ -29,6 +29,8 @@ interface EngineInterface extends BaseEngineInterface * @param Response $response A Response instance * * @return Response A Response instance + * + * @throws \RuntimeException if the template cannot be rendered */ public function renderResponse($view, array $parameters = array(), Response $response = null); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php b/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php index 942ebf8f0af77..5d86efb0d8c39 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php @@ -78,8 +78,8 @@ public function getUser() */ public function getRequest() { - if ($this->container->has('request') && $request = $this->container->get('request')) { - return $request; + if ($this->container->has('request_stack')) { + return $this->container->get('request_stack')->getCurrentRequest(); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php index edbf7a878aac9..a8075f18e17b3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php @@ -125,7 +125,7 @@ public function fileExcerpt($file, $line) if (extension_loaded('fileinfo')) { $finfo = new \Finfo(); - // Check if the file is an application/octet-stream (eg. Phar file) because hightlight_file cannot parse these files + // Check if the file is an application/octet-stream (eg. Phar file) because highlight_file cannot parse these files if ('application/octet-stream' === $finfo->file($file, FILEINFO_MIME_TYPE)) { return; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index 060a6ce6bf6e5..7bfa1e72bfc53 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -247,7 +247,7 @@ public function block(FormView $view, $blockName, array $variables = array()) * Check the token in your action using the same intention. * * - * $csrfProvider = $this->get('form.csrf_provider'); + * $csrfProvider = $this->get('security.csrf.token_generator'); * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) { * throw new \RuntimeException('CSRF attack detected.'); * } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php index 4533bf172bc6c..d071a586c02e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php @@ -13,6 +13,7 @@ use Symfony\Component\Templating\Helper\Helper; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; /** * RequestHelper provides access to the current request parameters. @@ -22,15 +23,24 @@ class RequestHelper extends Helper { protected $request; + protected $requestStack; /** * Constructor. * - * @param Request $request A Request instance + * @param Request|RequestStack $requestStack A RequestStack instance or a Request instance + * + * @deprecated since 2.5, passing a Request instance is deprecated and support for it will be removed in 3.0 */ - public function __construct(Request $request) + public function __construct($requestStack) { - $this->request = $request; + if ($requestStack instanceof Request) { + $this->request = $requestStack; + } elseif ($requestStack instanceof RequestStack) { + $this->requestStack = $requestStack; + } else { + throw new \InvalidArgumentException('RequestHelper only accepts a Request or a RequestStack instance.'); + } } /** @@ -45,7 +55,7 @@ public function __construct(Request $request) */ public function getParameter($key, $default = null) { - return $this->request->get($key, $default); + return $this->getRequest()->get($key, $default); } /** @@ -55,7 +65,20 @@ public function getParameter($key, $default = null) */ public function getLocale() { - return $this->request->getLocale(); + return $this->getRequest()->getLocale(); + } + + private function getRequest() + { + if ($this->requestStack) { + if (!$this->requestStack->getCurrentRequest()) { + throw new \LogicException('A Request must be available.'); + } + + return $this->requestStack->getCurrentRequest(); + } + + return $this->request; } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php index aac3f6dd7b1ee..691dde3d21c19 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php @@ -13,6 +13,7 @@ use Symfony\Component\Templating\Helper\Helper; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; /** * SessionHelper provides read-only access to the session attributes. @@ -22,15 +23,24 @@ class SessionHelper extends Helper { protected $session; + protected $requestStack; /** * Constructor. * - * @param Request $request A Request instance + * @param Request|RequestStack $requestStack A RequestStack instance or a Request instance + * + * @deprecated since 2.5, passing a Request instance is deprecated and support for it will be removed in 3.0 */ - public function __construct(Request $request) + public function __construct($requestStack) { - $this->session = $request->getSession(); + if ($requestStack instanceof Request) { + $this->session = $requestStack->getSession(); + } elseif ($requestStack instanceof RequestStack) { + $this->requestStack = $requestStack; + } else { + throw new \InvalidArgumentException('RequestHelper only accepts a Request or a RequestStack instance.'); + } } /** @@ -43,22 +53,35 @@ public function __construct(Request $request) */ public function get($name, $default = null) { - return $this->session->get($name, $default); + return $this->getSession()->get($name, $default); } public function getFlash($name, array $default = array()) { - return $this->session->getFlashBag()->get($name, $default); + return $this->getSession()->getFlashBag()->get($name, $default); } public function getFlashes() { - return $this->session->getFlashBag()->all(); + return $this->getSession()->getFlashBag()->all(); } public function hasFlash($name) { - return $this->session->getFlashBag()->has($name); + return $this->getSession()->getFlashBag()->has($name); + } + + private function getSession() + { + if (null === $this->session) { + if (!$this->requestStack->getMasterRequest()) { + throw new \LogicException('A Request must be available.'); + } + + $this->session = $this->requestStack->getMasterRequest()->getSession(); + } + + return $this->session; } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php new file mode 100644 index 0000000000000..47a9cf80ebcb4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.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\Bundle\FrameworkBundle\Templating\Helper; + +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Templating\Helper\Helper; + +/** + * StopwatchHelper provides methods time your PHP templates. + * + * @author Wouter J + */ +class StopwatchHelper extends Helper +{ + private $stopwatch; + + public function __construct(Stopwatch $stopwatch = null) + { + $this->stopwatch = $stopwatch; + } + + public function getName() + { + return 'stopwatch'; + } + + public function __call($method, $arguments = array()) + { + if (null !== $this->stopwatch) { + if (method_exists($this->stopwatch, $method)) { + return call_user_func_array(array($this->stopwatch, $method), $arguments); + } + + throw new \BadMethodCallException(sprintf('Method "%s" of Stopwatch does not exist', $method)); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php index ac917e790838a..4f6cffc103b8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php @@ -36,11 +36,7 @@ public function __construct(FileLocatorInterface $locator) } /** - * Loads a template. - * - * @param TemplateReferenceInterface $template A template - * - * @return FileStorage|Boolean false if the template cannot be loaded, a Storage instance otherwise + * {@inheritdoc} */ public function load(TemplateReferenceInterface $template) { @@ -54,12 +50,7 @@ public function load(TemplateReferenceInterface $template) } /** - * Returns true if the template is still fresh. - * - * @param TemplateReferenceInterface $template The template name as an array - * @param integer $time The last modification time of the cached template (timestamp) - * - * @return Boolean + * {@inheritdoc} */ public function isFresh(TemplateReferenceInterface $template, $time) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php index 0ff6b8cbb7b29..e9500de27e4b0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php @@ -66,7 +66,7 @@ protected function getCacheKey($template) public function locate($template, $currentPath = null, $first = true) { if (!$template instanceof TemplateReferenceInterface) { - throw new \InvalidArgumentException("The template must be an instance of TemplateReferenceInterface."); + throw new \InvalidArgumentException('The template must be an instance of TemplateReferenceInterface.'); } $key = $this->getCacheKey($template); diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php index a93098eb842c3..41382f769c65f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php @@ -46,7 +46,7 @@ public function __construct(TemplateNameParserInterface $parser, ContainerInterf } /** - * @throws \InvalidArgumentException When the helper is not defined + * {@inheritdoc} */ public function get($name) { @@ -71,13 +71,7 @@ public function setHelpers(array $helpers) } /** - * Renders a view and returns a Response. - * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * @param Response $response A Response instance - * - * @return Response A Response instance + * {@inheritdoc} */ public function renderResponse($view, array $parameters = array(), Response $response = null) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php index befb3e8f85771..a4a5e92cf3369 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; use Symfony\Component\Templating\TemplateNameParserInterface; +use Symfony\Component\Templating\TemplateReferenceInterface; /** * TemplateFilenameParser converts template filenames to @@ -24,9 +25,13 @@ class TemplateFilenameParser implements TemplateNameParserInterface /** * {@inheritdoc} */ - public function parse($file) + public function parse($name) { - $parts = explode('/', strtr($file, '\\', '/')); + if ($name instanceof TemplateReferenceInterface) { + return $name; + } + + $parts = explode('/', strtr($name, '\\', '/')); $elements = explode('.', array_pop($parts)); if (3 > count($elements)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php index ef2ae0800886a..8481dc78e7a9b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php @@ -25,7 +25,7 @@ class TemplateNameParser implements TemplateNameParserInterface { protected $kernel; - protected $cache; + protected $cache = array(); /** * Constructor. @@ -35,7 +35,6 @@ class TemplateNameParser implements TemplateNameParserInterface public function __construct(KernelInterface $kernel) { $this->kernel = $kernel; - $this->cache = array(); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php index fcaf935a547dc..a685c0abeacec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php @@ -124,7 +124,15 @@ private static function getPhpUnitCliConfigArgument() */ protected static function getKernelClass() { - $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir(); + $dir = $phpUnitDir = static::getPhpUnitXmlDir(); + + if (isset($_SERVER['KERNEL_DIR'])) { + $dir = $_SERVER['KERNEL_DIR']; + + if (!is_dir($dir) && is_dir("$phpUnitDir/$dir")) { + $dir = "$phpUnitDir/$dir"; + } + } $finder = new Finder(); $finder->name('*Kernel.php')->depth(0)->in($dir); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 42fcb6f597dd7..612d6325dd26d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -74,6 +74,18 @@ private function getKernel(array $bundles) ->with($this->equalTo('event_dispatcher')) ->will($this->returnValue($dispatcher)) ; + $container + ->expects($this->once()) + ->method('hasParameter') + ->with($this->equalTo('console.command.ids')) + ->will($this->returnValue(true)) + ; + $container + ->expects($this->once()) + ->method('getParameter') + ->with($this->equalTo('console.command.ids')) + ->will($this->returnValue(array())) + ; $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); $kernel diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php new file mode 100644 index 0000000000000..a8e64e3797ff5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.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\Bundle\FrameworkBundle\Tests\Console\Descriptor; + +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +abstract class AbstractDescriptorTest extends \PHPUnit_Framework_TestCase +{ + /** @dataProvider getDescribeRouteCollectionTestData */ + public function testDescribeRouteCollection(RouteCollection $routes, $expectedDescription) + { + $this->assertDescription($expectedDescription, $routes); + } + + public function getDescribeRouteCollectionTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getRouteCollections()); + } + + /** @dataProvider getDescribeRouteTestData */ + public function testDescribeRoute(Route $route, $expectedDescription) + { + $this->assertDescription($expectedDescription, $route); + } + + public function getDescribeRouteTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getRoutes()); + } + + /** @dataProvider getDescribeContainerParametersTestData */ + public function testDescribeContainerParameters(ParameterBag $parameters, $expectedDescription) + { + $this->assertDescription($expectedDescription, $parameters); + } + + public function getDescribeContainerParametersTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getContainerParameters()); + } + + /** @dataProvider getDescribeContainerBuilderTestData */ + public function testDescribeContainerBuilder(ContainerBuilder $builder, $expectedDescription, array $options) + { + $this->assertDescription($expectedDescription, $builder, $options); + } + + public function getDescribeContainerBuilderTestData() + { + return $this->getContainerBuilderDescriptionTestData(ObjectsProvider::getContainerBuilders()); + } + + /** @dataProvider getDescribeContainerDefinitionTestData */ + public function testDescribeContainerDefinition(Definition $definition, $expectedDescription) + { + $this->assertDescription($expectedDescription, $definition); + } + + public function getDescribeContainerDefinitionTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getContainerDefinitions()); + } + + /** @dataProvider getDescribeContainerAliasTestData */ + public function testDescribeContainerAlias(Alias $alias, $expectedDescription) + { + $this->assertDescription($expectedDescription, $alias); + } + + public function getDescribeContainerAliasTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getContainerAliases()); + } + + abstract protected function getDescriptor(); + abstract protected function getFormat(); + + private function assertDescription($expectedDescription, $describedObject, array $options = array()) + { + $options['raw_output'] = true; + $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); + $this->getDescriptor()->describe($output, $describedObject, $options); + $this->assertEquals(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $output->fetch()))); + } + + private function getDescriptionTestData(array $objects) + { + $data = array(); + foreach ($objects as $name => $object) { + $description = file_get_contents(sprintf('%s/../../Fixtures/Descriptor/%s.%s', __DIR__, $name, $this->getFormat())); + $data[] = array($object, $description); + } + + return $data; + } + + private function getContainerBuilderDescriptionTestData(array $objects) + { + $variations = array( + 'services' => array('show_private' => true), + 'public' => array('show_private' => false), + 'tag1' => array('show_private' => true, 'tag' => 'tag1'), + 'tags' => array('group_by' => 'tags', 'show_private' => true) + ); + + $data = array(); + foreach ($objects as $name => $object) { + foreach ($variations as $suffix => $options) { + $description = file_get_contents(sprintf('%s/../../Fixtures/Descriptor/%s_%s.%s', __DIR__, $name, $suffix, $this->getFormat())); + $data[] = array($object, $description, $options); + } + } + + return $data; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/JsonDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/JsonDescriptorTest.php new file mode 100644 index 0000000000000..5d6dafe46bf41 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/JsonDescriptorTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor; + +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\JsonDescriptor; + +class JsonDescriptorTest extends AbstractDescriptorTest +{ + protected function setUp() + { + if (version_compare(phpversion(), '5.4.0', '<')) { + $this->markTestSkipped('Test skipped on PHP 5.3 as JSON_PRETTY_PRINT does not exist.'); + } + } + + protected function getDescriptor() + { + return new JsonDescriptor(); + } + + protected function getFormat() + { + return 'json'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/MarkdownDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/MarkdownDescriptorTest.php new file mode 100644 index 0000000000000..fbb5aaa962689 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/MarkdownDescriptorTest.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\Bundle\FrameworkBundle\Tests\Console\Descriptor; + +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\MarkdownDescriptor; + +class MarkdownDescriptorTest extends AbstractDescriptorTest +{ + protected function getDescriptor() + { + return new MarkdownDescriptor(); + } + + protected function getFormat() + { + return 'md'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php new file mode 100644 index 0000000000000..e6a2617a6031b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.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\Bundle\FrameworkBundle\Tests\Console\Descriptor; + +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +class ObjectsProvider +{ + public static function getRouteCollections() + { + $collection1 = new RouteCollection(); + foreach (self::getRoutes() as $name => $route) { + $collection1->add($name, $route); + } + + return array('route_collection_1' => $collection1); + } + + public static function getRoutes() + { + return array( + 'route_1' => new Route( + '/hello/{name}', + array('name' => 'Joseph'), + array('name' => '[a-z]+'), + array('opt1' => 'val1', 'opt2' => 'val2'), + 'localhost', + array('http', 'https'), + array('get', 'head') + ), + 'route_2' => new Route( + '/name/add', + array(), + array(), + array('opt1' => 'val1', 'opt2' => 'val2'), + 'localhost', + array('http', 'https'), + array('put', 'post') + ), + ); + } + + public static function getContainerParameters() + { + return array( + 'parameters_1' => new ParameterBag(array( + 'integer' => 12, + 'string' => 'Hello world!', + 'boolean' => true, + 'array' => array(12, 'Hello world!', true), + )), + ); + } + + public static function getContainerBuilders() + { + $builder1 = new ContainerBuilder(); + $builder1->setDefinitions(self::getContainerDefinitions()); + $builder1->setAliases(self::getContainerAliases()); + + return array('builder_1' => $builder1); + } + + public static function getContainerDefinitions() + { + $definition1 = new Definition('Full\\Qualified\\Class1'); + $definition2 = new Definition('Full\\Qualified\\Class2'); + + return array( + 'definition_1' => $definition1 + ->setPublic(true) + ->setSynthetic(false) + ->setLazy(true) + ->setSynchronized(true) + ->setAbstract(true) + ->setFactoryClass('Full\\Qualified\\FactoryClass') + ->setFactoryMethod('get'), + 'definition_2' => $definition2 + ->setPublic(false) + ->setSynthetic(true) + ->setFile('/path/to/file') + ->setLazy(false) + ->setSynchronized(false) + ->setAbstract(false) + ->addTag('tag1', array('attr1' => 'val1', 'attr2' => 'val2')) + ->addTag('tag1', array('attr3' => 'val3')) + ->addTag('tag2') + ->setFactoryService('factory.service') + ->setFactoryMethod('get'), + ); + } + + public static function getContainerAliases() + { + return array( + 'alias_1' => new Alias('service_1', true), + 'alias_2' => new Alias('service_2', false), + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php new file mode 100644 index 0000000000000..ce4f377c508fd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.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\Bundle\FrameworkBundle\Tests\Console\Descriptor; + +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\TextDescriptor; + +class TextDescriptorTest extends AbstractDescriptorTest +{ + protected function getDescriptor() + { + return new TextDescriptor(); + } + + protected function getFormat() + { + return 'txt'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/XmlDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/XmlDescriptorTest.php new file mode 100644 index 0000000000000..8cb9a71697f6b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/XmlDescriptorTest.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\Bundle\FrameworkBundle\Tests\Console\Descriptor; + +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\XmlDescriptor; + +class XmlDescriptorTest extends AbstractDescriptorTest +{ + protected function getDescriptor() + { + return new XmlDescriptor(); + } + + protected function getFormat() + { + return 'xml'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddConsoleCommandPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddConsoleCommandPassTest.php new file mode 100644 index 0000000000000..0606bb53bad8f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddConsoleCommandPassTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConsoleCommandPass; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +class AddConsoleCommandPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddConsoleCommandPass()); + $container->setParameter('my-command.class', 'Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\MyCommand'); + + $definition = new Definition('%my-command.class%'); + $definition->addTag('console.command'); + $container->setDefinition('my-command', $definition); + + $container->compile(); + + $alias = 'console.command.symfony_bundle_frameworkbundle_tests_dependencyinjection_compiler_mycommand'; + $this->assertTrue($container->hasAlias($alias)); + $this->assertSame('my-command', (string) $container->getAlias($alias)); + + $this->assertTrue($container->hasParameter('console.command.ids')); + $this->assertSame(array('my-command'), $container->getParameter('console.command.ids')); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage The service "my-command" tagged "console.command" must be public. + */ + public function testProcessThrowAnExceptionIfTheServiceIsNotPublic() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddConsoleCommandPass()); + + $definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\MyCommand'); + $definition->addTag('console.command'); + $definition->setPublic(false); + $container->setDefinition('my-command', $definition); + + $container->compile(); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage The service "my-command" tagged "console.command" must not be abstract. + */ + public function testProcessThrowAnExceptionIfTheServiceIsAbstract() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddConsoleCommandPass()); + + $definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\MyCommand'); + $definition->addTag('console.command'); + $definition->setAbstract(true); + $container->setDefinition('my-command', $definition); + + $container->compile(); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage The service "my-command" tagged "console.command" must be a subclass of "Symfony\Component\Console\Command\Command". + */ + public function testProcessThrowAnExceptionIfTheServiceIsNotASubclassOfCommand() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddConsoleCommandPass()); + + $definition = new Definition('SplObjectStorage'); + $definition->addTag('console.command'); + $container->setDefinition('my-command', $definition); + + $container->compile(); + } +} + +class MyCommand extends Command +{ + +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 598622a1da7d1..6741bfed7dc02 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -93,9 +93,15 @@ protected static function getBundleDefaultConfig() 'trusted_proxies' => array(), 'ide' => null, 'default_locale' => 'en', - 'form' => array('enabled' => false), + 'form' => array( + 'enabled' => false, + 'csrf_protection' => array( + 'enabled' => null, // defaults to csrf_protection.enabled + 'field_name' => null, + ), + ), 'csrf_protection' => array( - 'enabled' => true, + 'enabled' => false, 'field_name' => '_token', ), 'esi' => array('enabled' => false), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php new file mode 100644 index 0000000000000..ff5286c7a2c62 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php @@ -0,0 +1,16 @@ +loadFromExtension('framework', array( + 'csrf_protection' => array( + 'enabled' => false, + ), + 'form' => array( + 'enabled' => true, + 'csrf_protection' => array( + 'enabled' => true, + ), + ), + 'session' => array( + 'handler_id' => null, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf_disabled.php new file mode 100644 index 0000000000000..b7bbd8bf3c886 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf_disabled.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', array( + 'csrf_protection' => array( + 'enabled' => false, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf_needs_session.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf_needs_session.php new file mode 100644 index 0000000000000..d1df1c596233c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf_needs_session.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', array( + 'csrf_protection' => array( + 'enabled' => true, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_sets_field_name.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_sets_field_name.php new file mode 100644 index 0000000000000..2eed747003944 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_sets_field_name.php @@ -0,0 +1,14 @@ +loadFromExtension('framework', array( + 'csrf_protection' => array( + 'enabled' => true, + 'field_name' => '_custom' + ), + 'form' => array( + 'enabled' => true, + ), + 'session' => array( + 'handler_id' => null, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_under_form_sets_field_name.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_under_form_sets_field_name.php new file mode 100644 index 0000000000000..39bb6fa7826c3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_under_form_sets_field_name.php @@ -0,0 +1,17 @@ +loadFromExtension('framework', array( + 'csrf_protection' => array( + 'enabled' => true, + 'field_name' => '_custom' + ), + 'form' => array( + 'enabled' => true, + 'csrf_protection' => array( + 'field_name' => '_custom_form' + ), + ), + 'session' => array( + 'handler_id' => null, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_no_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_no_csrf.php new file mode 100644 index 0000000000000..db7a6b93dcaae --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_no_csrf.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', array( + 'form' => array( + 'enabled' => true, + 'csrf_protection' => false, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml new file mode 100644 index 0000000000000..4484c353ede26 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf_disabled.xml new file mode 100644 index 0000000000000..635557f938acb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf_disabled.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf_needs_session.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf_needs_session.xml new file mode 100644 index 0000000000000..0a08d2351aaf2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf_needs_session.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml new file mode 100644 index 0000000000000..8e1803a6c7217 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml new file mode 100644 index 0000000000000..d08ac9e542784 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml new file mode 100644 index 0000000000000..fdeb60acf9934 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml index ce5fc591edbfe..c08db90f5b7ae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml @@ -1,6 +1,8 @@ framework: + csrf_protection: false secret: s3cr3t - form: ~ + form: + csrf_protection: true session: ~ - # CSRF should be enabled by default + # CSRF is disabled by default # csrf_protection: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf_disabled.yml new file mode 100644 index 0000000000000..b094cc481fea8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf_disabled.yml @@ -0,0 +1,2 @@ +framework: + csrf_protection: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf_needs_session.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf_needs_session.yml new file mode 100644 index 0000000000000..b8065b6fb678b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf_needs_session.yml @@ -0,0 +1,2 @@ +framework: + csrf_protection: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_sets_field_name.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_sets_field_name.yml new file mode 100644 index 0000000000000..3347cbfaf1bf5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_sets_field_name.yml @@ -0,0 +1,5 @@ +framework: + csrf_protection: + field_name: _custom + form: ~ + session: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_under_form_sets_field_name.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_under_form_sets_field_name.yml new file mode 100644 index 0000000000000..a4bb34259f58a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_under_form_sets_field_name.yml @@ -0,0 +1,7 @@ +framework: + csrf_protection: + field_name: _custom + form: + csrf_protection: + field_name: _custom_form + session: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_no_csrf.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_no_csrf.yml new file mode 100644 index 0000000000000..e3ac7e8daf42d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_no_csrf.yml @@ -0,0 +1,4 @@ +framework: + form: + csrf_protection: + enabled: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 346962e2ac02f..94ba71d448575 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -30,7 +30,29 @@ public function testCsrfProtection() $this->assertEquals('%form.type_extension.csrf.enabled%', $def->getArgument(1)); $this->assertEquals('_csrf', $container->getParameter('form.type_extension.csrf.field_name')); $this->assertEquals('%form.type_extension.csrf.field_name%', $def->getArgument(2)); - $this->assertEquals('s3cr3t', $container->getParameterBag()->resolveValue($container->findDefinition('form.csrf_provider')->getArgument(1))); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage CSRF protection needs sessions to be enabled. + */ + public function testCsrfProtectionNeedsSessionToBeEnabled() + { + $this->createContainerFromFile('csrf_needs_session'); + } + + public function testCsrfProtectionForFormsEnablesCsrfProtectionAutomatically() + { + $container = $this->createContainerFromFile('csrf'); + + $this->assertTrue($container->hasDefinition('security.csrf.token_manager')); + } + + public function testSecureRandomIsAvailableIfCsrfIsDisabled() + { + $container = $this->createContainerFromFile('csrf_disabled'); + + $this->assertTrue($container->hasDefinition('security.secure_random')); } public function testProxies() @@ -184,19 +206,19 @@ public function testTranslator() $files = array_map(function ($resource) { return realpath($resource[1]); }, $resources); $ref = new \ReflectionClass('Symfony\Component\Validator\Validator'); $this->assertContains( - strtr(dirname($ref->getFileName()) . '/Resources/translations/validators.en.xlf', '/', DIRECTORY_SEPARATOR), + strtr(dirname($ref->getFileName()).'/Resources/translations/validators.en.xlf', '/', DIRECTORY_SEPARATOR), $files, '->registerTranslatorConfiguration() finds Validator translation resources' ); $ref = new \ReflectionClass('Symfony\Component\Form\Form'); $this->assertContains( - strtr(dirname($ref->getFileName()) . '/Resources/translations/validators.en.xlf', '/', DIRECTORY_SEPARATOR), + strtr(dirname($ref->getFileName()).'/Resources/translations/validators.en.xlf', '/', DIRECTORY_SEPARATOR), $files, '->registerTranslatorConfiguration() finds Form translation resources' ); $ref = new \ReflectionClass('Symfony\Component\Security\Core\SecurityContext'); $this->assertContains( - strtr(dirname(dirname($ref->getFileName())) . '/Resources/translations/security.en.xlf', '/', DIRECTORY_SEPARATOR), + strtr(dirname($ref->getFileName()).'/Resources/translations/security.en.xlf', '/', DIRECTORY_SEPARATOR), $files, '->registerTranslatorConfiguration() finds Security translation resources' ); @@ -234,10 +256,6 @@ public function testValidation() public function testAnnotations() { - if (!class_exists('Doctrine\\Common\\Version')) { - $this->markTestSkipped('Doctrine is not available.'); - } - $container = $this->createContainerFromFile('full'); $this->assertEquals($container->getParameter('kernel.cache_dir').'/annotations', $container->getDefinition('annotations.file_cache_reader')->getArgument(1)); @@ -284,6 +302,29 @@ public function testValidationPaths() $this->assertStringEndsWith('TestBundle'.DIRECTORY_SEPARATOR.'Resources'.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'validation.xml', $xmlArgs[1]); } + public function testFormsCanBeEnabledWithoutCsrfProtection() + { + $container = $this->createContainerFromFile('form_no_csrf'); + + $this->assertFalse($container->getParameter('form.type_extension.csrf.enabled')); + } + + public function testFormCsrfFieldNameCanBeSetUnderCsrfSettings() + { + $container = $this->createContainerFromFile('form_csrf_sets_field_name'); + + $this->assertTrue($container->getParameter('form.type_extension.csrf.enabled')); + $this->assertEquals('_custom', $container->getParameter('form.type_extension.csrf.field_name')); + } + + public function testFormCsrfFieldNameUnderFormSettingsTakesPrecedence() + { + $container = $this->createContainerFromFile('form_csrf_under_form_sets_field_name'); + + $this->assertTrue($container->getParameter('form.type_extension.csrf.enabled')); + $this->assertEquals('_custom_form', $container->getParameter('form.type_extension.csrf.field_name')); + } + protected function createContainer(array $data = array()) { return new ContainerBuilder(new ParameterBag(array_merge(array( diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/YamlFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/YamlFrameworkExtensionTest.php index b8dcefc558021..43070c00c9830 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/YamlFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/YamlFrameworkExtensionTest.php @@ -22,11 +22,4 @@ protected function loadFromFile(ContainerBuilder $container, $file) $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/Fixtures/yml')); $loader->load($file.'.yml'); } - - public function testCsrfProtectionShouldBeEnabledByDefault() - { - $container = $this->createContainerFromFile('csrf'); - - $this->assertTrue($container->getParameter('form.type_extension.csrf.enabled')); - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.json new file mode 100644 index 0000000000000..283ed13fa3302 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.json @@ -0,0 +1,4 @@ +{ + "service": "service_1", + "public": true +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.md new file mode 100644 index 0000000000000..ec63107b93b0f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.md @@ -0,0 +1,2 @@ +- Service: `service_1` +- Public: yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.txt new file mode 100644 index 0000000000000..3d40ba0084340 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.txt @@ -0,0 +1 @@ +This service is an alias for the service service_1 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.xml new file mode 100644 index 0000000000000..759f8a22aa1dc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.json new file mode 100644 index 0000000000000..6998dae2828a8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.json @@ -0,0 +1,4 @@ +{ + "service": "service_2", + "public": false +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.md new file mode 100644 index 0000000000000..f2a46087c162e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.md @@ -0,0 +1,2 @@ +- Service: `service_2` +- Public: no diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.txt new file mode 100644 index 0000000000000..df99e817c96b5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.txt @@ -0,0 +1 @@ +This service is an alias for the service service_2 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.xml new file mode 100644 index 0000000000000..847050b33a428 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.json new file mode 100644 index 0000000000000..33c5ed71d422f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.json @@ -0,0 +1,27 @@ +{ + "definitions": { + "definition_1": { + "class": "Full\\Qualified\\Class1", + "scope": "container", + "public": true, + "synthetic": false, + "file": null, + "tags": [ + + ] + } + }, + "aliases": { + "alias_1": { + "service": "service_1", + "public": true + }, + "alias_2": { + "service": "service_2", + "public": false + } + }, + "services": { + "service_container": "Symfony\\Component\\DependencyInjection\\ContainerBuilder" + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md new file mode 100644 index 0000000000000..196757b13b7a2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md @@ -0,0 +1,35 @@ +Public services +=============== + +Definitions +----------- + +definition_1 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class1` +- Scope: `container` +- Public: yes +- Synthetic: no + + +Aliases +------- + +alias_1 +~~~~~~~ + +- Service: `service_1` +- Public: yes + +alias_2 +~~~~~~~ + +- Service: `service_2` +- Public: no + + +Services +-------- + +- `service_container`: `Symfony\Component\DependencyInjection\ContainerBuilder` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.txt new file mode 100644 index 0000000000000..58380fdf4d218 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.txt @@ -0,0 +1,6 @@ +[container] Public services + Service ID Scope Class name + alias_1 n/a alias for "service_1" + alias_2 n/a alias for "service_2" + definition_1 container Full\Qualified\Class1 + service_container Symfony\Component\DependencyInjection\ContainerBuilder diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.xml new file mode 100644 index 0000000000000..16df428799fd0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.json new file mode 100644 index 0000000000000..6dd5929838f11 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.json @@ -0,0 +1,55 @@ +{ + "definitions": { + "definition_1": { + "class": "Full\\Qualified\\Class1", + "scope": "container", + "public": true, + "synthetic": false, + "file": null, + "tags": [ + + ] + }, + "definition_2": { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "file": "\/path\/to\/file", + "tags": [ + { + "name": "tag1", + "parameters": { + "attr1": "val1", + "attr2": "val2" + } + }, + { + "name": "tag1", + "parameters": { + "attr3": "val3" + } + }, + { + "name": "tag2", + "parameters": [ + + ] + } + ] + } + }, + "aliases": { + "alias_1": { + "service": "service_1", + "public": true + }, + "alias_2": { + "service": "service_2", + "public": false + } + }, + "services": { + "service_container": "Symfony\\Component\\DependencyInjection\\ContainerBuilder" + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md new file mode 100644 index 0000000000000..33cb0a93a74c9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md @@ -0,0 +1,50 @@ +Public and private services +=========================== + +Definitions +----------- + +definition_1 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class1` +- Scope: `container` +- Public: yes +- Synthetic: no + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- File: `/path/to/file` +- Tag: `tag1` + - Attr1: val1 + - Attr2: val2 +- Tag: `tag1` + - Attr3: val3 +- Tag: `tag2` + + +Aliases +------- + +alias_1 +~~~~~~~ + +- Service: `service_1` +- Public: yes + +alias_2 +~~~~~~~ + +- Service: `service_2` +- Public: no + + +Services +-------- + +- `service_container`: `Symfony\Component\DependencyInjection\ContainerBuilder` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt new file mode 100644 index 0000000000000..98a1f27aeb94e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt @@ -0,0 +1,8 @@ +[container] Public and private services + Service ID Scope Class name + alias_1 n/a alias for "service_1" + alias_2 n/a alias for "service_2" + definition_1 container Full\Qualified\Class1 + definition_2 container Full\Qualified\Class2 + service_container Symfony\Component\DependencyInjection\ContainerBuilder + \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.xml new file mode 100644 index 0000000000000..4636803bfb2cd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.xml @@ -0,0 +1,19 @@ + + + + + + + + + val1 + val2 + + + val3 + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.json new file mode 100644 index 0000000000000..d9a351b90b1ec --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.json @@ -0,0 +1,38 @@ +{ + "definitions": { + "definition_2": { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "file": "\/path\/to\/file", + "tags": [ + { + "name": "tag1", + "parameters": { + "attr1": "val1", + "attr2": "val2" + } + }, + { + "name": "tag1", + "parameters": { + "attr3": "val3" + } + }, + { + "name": "tag2", + "parameters": [ + + ] + } + ] + } + }, + "aliases": [ + + ], + "services": [ + + ] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.md new file mode 100644 index 0000000000000..2b32f9e84316d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.md @@ -0,0 +1,20 @@ +Public and private services with tag `tag1` +=========================================== + +Definitions +----------- + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- File: `/path/to/file` +- Tag: `tag1` + - Attr1: val1 + - Attr2: val2 +- Tag: `tag1` + - Attr3: val3 +- Tag: `tag2` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.txt new file mode 100644 index 0000000000000..f2d6135b0a7b3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.txt @@ -0,0 +1,4 @@ +[container] Public and private services with tag tag1 + Service ID attr1 attr2 attr3 Scope Class name + definition_2 val1 val2 container Full\Qualified\Class2 + " val3 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.xml new file mode 100644 index 0000000000000..c755e70ce76f9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.xml @@ -0,0 +1,15 @@ + + + + + + val1 + val2 + + + val3 + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.json new file mode 100644 index 0000000000000..e0a3c0f31ca7a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.json @@ -0,0 +1,20 @@ +{ + "tag1": [ + { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "file": "\/path\/to\/file" + } + ], + "tag2": [ + { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "file": "\/path\/to\/file" + } + ] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.md new file mode 100644 index 0000000000000..b69cf69ac7911 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.md @@ -0,0 +1,27 @@ +Container tags +============== + +tag1 +---- + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- File: `/path/to/file` + + +tag2 +---- + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- File: `/path/to/file` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.txt new file mode 100644 index 0000000000000..791c1d95c3c9e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.txt @@ -0,0 +1,6 @@ +[container] Tagged services +[tag] tag1 +definition_2 + +[tag] tag2 +definition_2 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.xml new file mode 100644 index 0000000000000..b0f6244487dae --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.json new file mode 100644 index 0000000000000..00a553ce23b7b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.json @@ -0,0 +1,10 @@ +{ + "class": "Full\\Qualified\\Class1", + "scope": "container", + "public": true, + "synthetic": false, + "file": null, + "tags": [ + + ] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.md new file mode 100644 index 0000000000000..49005b12a4a47 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.md @@ -0,0 +1,4 @@ +- Class: `Full\Qualified\Class1` +- Scope: `container` +- Public: yes +- Synthetic: no diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.txt new file mode 100644 index 0000000000000..74aabca95a27b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.txt @@ -0,0 +1,7 @@ +Service Id - +Class Full\Qualified\Class1 +Tags - +Scope container +Public yes +Synthetic no +Required File - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.xml new file mode 100644 index 0000000000000..75d0820244579 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.json new file mode 100644 index 0000000000000..d31f146a6ad2b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.json @@ -0,0 +1,28 @@ +{ + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "file": "\/path\/to\/file", + "tags": [ + { + "name": "tag1", + "parameters": { + "attr1": "val1", + "attr2": "val2" + } + }, + { + "name": "tag1", + "parameters": { + "attr3": "val3" + } + }, + { + "name": "tag2", + "parameters": [ + + ] + } + ] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.md new file mode 100644 index 0000000000000..521b496e07609 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.md @@ -0,0 +1,11 @@ +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- File: `/path/to/file` +- Tag: `tag1` + - Attr1: val1 + - Attr2: val2 +- Tag: `tag1` + - Attr3: val3 +- Tag: `tag2` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt new file mode 100644 index 0000000000000..06b6c327cf521 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt @@ -0,0 +1,10 @@ +Service Id - +Class Full\Qualified\Class2 +Tags + - tag1 (attr1: val1, attr2: val2) + - tag1 (attr3: val3) + - tag2 () +Scope container +Public no +Synthetic yes +Required File /path/to/file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.xml new file mode 100644 index 0000000000000..dd3e2e06d7174 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.xml @@ -0,0 +1,13 @@ + + + + + val1 + val2 + + + val3 + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.json new file mode 100644 index 0000000000000..686cbc9e52209 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.json @@ -0,0 +1,10 @@ +{ + "array": [ + 12, + "Hello world!", + true + ], + "boolean": true, + "integer": 12, + "string": "Hello world!" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.md new file mode 100644 index 0000000000000..2dfe5d640b858 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.md @@ -0,0 +1,7 @@ +Container parameters +==================== + +- `array`: `[12,"Hello world!",true]` +- `boolean`: `true` +- `integer`: `12` +- `string`: `Hello world!` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.txt new file mode 100644 index 0000000000000..1c9a2739ca63c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.txt @@ -0,0 +1,7 @@ +[container] List of parameters + Parameter Value + array [12,"Hello world!",true] + boolean true + integer 12 + string Hello world! + \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.xml new file mode 100644 index 0000000000000..e97f895730139 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.xml @@ -0,0 +1,7 @@ + + + [12,"Hello world!",true] + true + 12 + Hello world! + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.json new file mode 100644 index 0000000000000..7da10ed861438 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.json @@ -0,0 +1,19 @@ +{ + "path": "\/hello\/{name}", + "host": "localhost", + "scheme": "http|https", + "method": "GET|HEAD", + "class": "Symfony\\Component\\Routing\\Route", + "defaults": { + "name": "Joseph" + }, + "requirements": { + "name": "[a-z]+" + }, + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler", + "opt1": "val1", + "opt2": "val2" + }, + "pathRegex": "#^\/hello(?:\/(?P[a-z]+))?$#s" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.md new file mode 100644 index 0000000000000..4ac00a8929755 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.md @@ -0,0 +1,9 @@ +- Path: /hello/{name} +- Host: localhost +- Scheme: http|https +- Method: GET|HEAD +- Class: Symfony\Component\Routing\Route +- Defaults: + - `name`: Joseph +- Requirements: + - `name`: [a-z]+ \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt new file mode 100644 index 0000000000000..2552c1ed88993 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt @@ -0,0 +1,12 @@ +Path /hello/{name} +Host localhost +Scheme http|https +Method GET|HEAD +Class Symfony\Component\Routing\Route +Defaults name: Joseph +Requirements name: [a-z]+ +Options compiler_class: Symfony\Component\Routing\RouteCompiler + opt1: val1 + opt2: val2 +Path-Regex #^/hello(?:/(?P[a-z]+))?$#s +Host-Regex #^localhost$#s \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.xml new file mode 100644 index 0000000000000..164282c5f69ad --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.xml @@ -0,0 +1,20 @@ + + + /hello/{name} + localhost + http + https + GET + HEAD + + Joseph + + + [a-z]+ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.json new file mode 100644 index 0000000000000..276f8ca42a0a4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.json @@ -0,0 +1,17 @@ +{ + "path": "\/name\/add", + "host": "localhost", + "scheme": "http|https", + "method": "PUT|POST", + "class": "Symfony\\Component\\Routing\\Route", + "defaults": [ + + ], + "requirements": "NO CUSTOM", + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler", + "opt1": "val1", + "opt2": "val2" + }, + "pathRegex": "#^\/name\/add$#s" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.md new file mode 100644 index 0000000000000..c19d75f4f3b13 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.md @@ -0,0 +1,7 @@ +- Path: /name/add +- Host: localhost +- Scheme: http|https +- Method: PUT|POST +- Class: Symfony\Component\Routing\Route +- Defaults: NONE +- Requirements: NONE \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt new file mode 100644 index 0000000000000..99119b6cc4e90 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt @@ -0,0 +1,12 @@ +Path /name/add +Host localhost +Scheme http|https +Method PUT|POST +Class Symfony\Component\Routing\Route +Defaults +Requirements +Options compiler_class: Symfony\Component\Routing\RouteCompiler + opt1: val1 + opt2: val2 +Path-Regex #^/name/add$#s +Host-Regex #^localhost$#s \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.xml new file mode 100644 index 0000000000000..333eeaa1b4166 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.xml @@ -0,0 +1,14 @@ + + + /name/add + localhost + http + https + PUT + POST + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.json new file mode 100644 index 0000000000000..0cd1610f20ae8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.json @@ -0,0 +1,38 @@ +{ + "route_1": { + "path": "\/hello\/{name}", + "host": "localhost", + "scheme": "http|https", + "method": "GET|HEAD", + "class": "Symfony\\Component\\Routing\\Route", + "defaults": { + "name": "Joseph" + }, + "requirements": { + "name": "[a-z]+" + }, + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler", + "opt1": "val1", + "opt2": "val2" + }, + "pathRegex": "#^\/hello(?:\/(?P[a-z]+))?$#s" + }, + "route_2": { + "path": "\/name\/add", + "host": "localhost", + "scheme": "http|https", + "method": "PUT|POST", + "class": "Symfony\\Component\\Routing\\Route", + "defaults": [ + + ], + "requirements": "NO CUSTOM", + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler", + "opt1": "val1", + "opt2": "val2" + }, + "pathRegex": "#^\/name\/add$#s" + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.md new file mode 100644 index 0000000000000..a148c23210bad --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.md @@ -0,0 +1,24 @@ +route_1 +------- + +- Path: /hello/{name} +- Host: localhost +- Scheme: http|https +- Method: GET|HEAD +- Class: Symfony\Component\Routing\Route +- Defaults: + - `name`: Joseph +- Requirements: + - `name`: [a-z]+ + + +route_2 +------- + +- Path: /name/add +- Host: localhost +- Scheme: http|https +- Method: PUT|POST +- Class: Symfony\Component\Routing\Route +- Defaults: NONE +- Requirements: NONE diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.txt new file mode 100644 index 0000000000000..e0ade43e2afd3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.txt @@ -0,0 +1,5 @@ +[router] Current routes + Name Method Scheme Host Path + route_1 GET|HEAD http|https localhost /hello/{name} + route_2 PUT|POST http|https localhost /name/add + \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.xml new file mode 100644 index 0000000000000..aa5adb482aa76 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.xml @@ -0,0 +1,35 @@ + + + + /hello/{name} + localhost + http + https + GET + HEAD + + Joseph + + + [a-z]+ + + + + + + + + + /name/add + localhost + http + https + PUT + POST + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php index 8cf0dfead75dc..4ab0c7c9afbd6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/WebTestCase.php @@ -23,15 +23,6 @@ public static function assertRedirect($response, $location) self::assertEquals('http://localhost'.$location, $response->headers->get('Location')); } - protected function setUp() - { - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - - parent::setUp(); - } - protected function deleteTmpDir($testCase) { if (!file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/'.$testCase)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php index fe8fc709a6312..282f860262425 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php @@ -38,7 +38,7 @@ public function testRedirectWhenNoSlash() ); } - public function testSchemeRedirect() + public function testSchemeRedirectBC() { $coll = new RouteCollection(); $coll->add('foo', new Route('/foo', array(), array('_scheme' => 'https'))); @@ -57,4 +57,24 @@ public function testSchemeRedirect() $matcher->match('/foo') ); } + + public function testSchemeRedirect() + { + $coll = new RouteCollection(); + $coll->add('foo', new Route('/foo', array(), array(), array(), '', array('https'))); + + $matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext()); + + $this->assertEquals(array( + '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction', + 'path' => '/foo', + 'permanent' => true, + 'scheme' => 'https', + 'httpPort' => $context->getHttpPort(), + 'httpsPort' => $context->getHttpsPort(), + '_route' => 'foo', + ), + $matcher->match('/foo') + ); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php new file mode 100644 index 0000000000000..f3a46be70c039 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; + +use Symfony\Bundle\FrameworkBundle\Templating\DelegatingEngine; + +class DelegatingEngineTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportsRetrievesEngineFromTheContainer() + { + $container = $this->getContainerMock(array( + 'engine.first' => $this->getEngineMock('template.php', false), + 'engine.second' => $this->getEngineMock('template.php', true) + )); + + $delegatingEngine = new DelegatingEngine($container, array('engine.first', 'engine.second')); + + $this->assertTrue($delegatingEngine->supports('template.php')); + } + + public function testGetExistingEngine() + { + $firstEngine = $this->getEngineMock('template.php', false); + $secondEngine = $this->getEngineMock('template.php', true); + $container = $this->getContainerMock(array( + 'engine.first' => $firstEngine, + 'engine.second' => $secondEngine + )); + + $delegatingEngine = new DelegatingEngine($container, array('engine.first', 'engine.second')); + + $this->assertSame($secondEngine, $delegatingEngine->getEngine('template.php', array('foo' => 'bar'))); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage No engine is able to work with the template "template.php" + */ + public function testGetInvalidEngine() + { + $firstEngine = $this->getEngineMock('template.php', false); + $secondEngine = $this->getEngineMock('template.php', false); + $container = $this->getContainerMock(array( + 'engine.first' => $firstEngine, + 'engine.second' => $secondEngine + )); + + $delegatingEngine = new DelegatingEngine($container, array('engine.first', 'engine.second')); + $delegatingEngine->getEngine('template.php', array('foo' => 'bar')); + } + + public function testRenderResponseWithFrameworkEngine() + { + $response = $this->getMock('Symfony\Component\HttpFoundation\Response'); + $engine = $this->getFrameworkEngineMock('template.php', true); + $engine->expects($this->once()) + ->method('renderResponse') + ->with('template.php', array('foo' => 'bar')) + ->will($this->returnValue($response)); + $container = $this->getContainerMock(array('engine' => $engine)); + + $delegatingEngine = new DelegatingEngine($container, array('engine')); + + $this->assertSame($response, $delegatingEngine->renderResponse('template.php', array('foo' => 'bar'))); + } + + public function testRenderResponseWithTemplatingEngine() + { + $engine = $this->getEngineMock('template.php', true); + $container = $this->getContainerMock(array('engine' => $engine)); + $delegatingEngine = new DelegatingEngine($container, array('engine')); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $delegatingEngine->renderResponse('template.php', array('foo' => 'bar'))); + } + + private function getEngineMock($template, $supports) + { + $engine = $this->getMock('Symfony\Component\Templating\EngineInterface'); + + $engine->expects($this->once()) + ->method('supports') + ->with($template) + ->will($this->returnValue($supports)); + + return $engine; + } + + private function getFrameworkEngineMock($template, $supports) + { + $engine = $this->getMock('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface'); + + $engine->expects($this->once()) + ->method('supports') + ->with($template) + ->will($this->returnValue($supports)); + + return $engine; + } + + private function getContainerMock($services) + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + + $i = 0; + foreach ($services as $id => $service) { + $container->expects($this->at($i++)) + ->method('get') + ->with($id) + ->will($this->returnValue($service)); + } + + return $container; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php index 9a960e3bea010..4e244e2cf68be 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php @@ -29,19 +29,6 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest */ protected $engine; - protected function setUp() - { - if (!class_exists('Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper')) { - $this->markTestSkipped('The "FrameworkBundle" is not available'); - } - - if (!class_exists('Symfony\Component\Templating\PhpEngine')) { - $this->markTestSkipped('The "Templating" component is not available'); - } - - parent::setUp(); - } - protected function getExtensions() { // should be moved to the Form component once absolute file paths are supported @@ -59,7 +46,7 @@ protected function getExtensions() )); return array_merge(parent::getExtensions(), array( - new TemplatingExtension($this->engine, $this->csrfProvider, array( + new TemplatingExtension($this->engine, $this->csrfTokenManager, array( 'FrameworkBundle:Form', )), )); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php index 96c56b65064e1..e7e6e60bfe4b1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php @@ -29,19 +29,6 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest */ protected $engine; - protected function setUp() - { - if (!class_exists('Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper')) { - $this->markTestSkipped('The "FrameworkBundle" is not available'); - } - - if (!class_exists('Symfony\Component\Templating\PhpEngine')) { - $this->markTestSkipped('The "Templating" component is not available'); - } - - parent::setUp(); - } - protected function getExtensions() { // should be moved to the Form component once absolute file paths are supported @@ -59,7 +46,7 @@ protected function getExtensions() )); return array_merge(parent::getExtensions(), array( - new TemplatingExtension($this->engine, $this->csrfProvider, array( + new TemplatingExtension($this->engine, $this->csrfTokenManager, array( 'FrameworkBundle:Form', 'FrameworkBundle:FormTable', )), diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php index d3d4f22385c24..d626e63e41cfa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php @@ -12,26 +12,24 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Bundle\FrameworkBundle\Templating\Helper\RequestHelper; class RequestHelperTest extends \PHPUnit_Framework_TestCase { - protected $request; + protected $requestStack; protected function setUp() { - $this->request = new Request(); - $this->request->initialize(array('foobar' => 'bar')); - } - - protected function tearDown() - { - $this->request = null; + $this->requestStack = new RequestStack(); + $request = new Request(); + $request->initialize(array('foobar' => 'bar')); + $this->requestStack->push($request); } public function testGetParameter() { - $helper = new RequestHelper($this->request); + $helper = new RequestHelper($this->requestStack); $this->assertEquals('bar', $helper->getParameter('foobar')); $this->assertEquals('foo', $helper->getParameter('bar', 'foo')); @@ -41,14 +39,14 @@ public function testGetParameter() public function testGetLocale() { - $helper = new RequestHelper($this->request); + $helper = new RequestHelper($this->requestStack); $this->assertEquals('en', $helper->getLocale()); } public function testGetName() { - $helper = new RequestHelper($this->request); + $helper = new RequestHelper($this->requestStack); $this->assertEquals('request', $helper->getName()); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php new file mode 100644 index 0000000000000..1a0ff5f548cee --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; + +use Symfony\Bundle\FrameworkBundle\Templating\Helper\StopwatchHelper; + +class StopwatchHelperTest extends \PHPUnit_Framework_TestCase +{ + public function testDevEnvironment() + { + $stopwatch = $this->getMock('Symfony\Component\Stopwatch\Stopwatch'); + $stopwatch->expects($this->once()) + ->method('start') + ->with('foo'); + + $helper = new StopwatchHelper($stopwatch); + $helper->start('foo'); + } + + public function testProdEnvironment() + { + $helper = new StopwatchHelper(null); + + try { + $helper->start('foo'); + } catch (\BadMethodCallException $e) { + $this->fail('Assumed stopwatch is not called when not provided'); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php index 476b398e61b13..f943668da51ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Templating\PhpEngine; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\Templating\TemplateNameParser; @@ -37,7 +38,7 @@ public function testEvaluateWithoutAvailableRequest() $loader = $this->getMockForAbstractClass('Symfony\Component\Templating\Loader\Loader'); $engine = new PhpEngine(new TemplateNameParser(), $container, $loader, new GlobalVariables($container)); - $container->set('request', null); + $container->set('request_stack', null); $globals = $engine->getGlobals(); $this->assertEmpty($globals['app']->getRequest()); @@ -63,11 +64,13 @@ public function testGetInvalidHelper() protected function getContainer() { $container = new Container(); - $request = new Request(); - $session = new Session(new MockArraySessionStorage()); + $session = new Session(new MockArraySessionStorage()); + $request = new Request(); + $stack = new RequestStack(); + $stack->push($request); $request->setSession($session); - $container->set('request', $request); + $container->set('request_stack', $stack); return $container; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php index 9e2981f5e7046..e9d1406e069f7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php @@ -13,7 +13,6 @@ use Symfony\Bundle\FrameworkBundle\Templating\TimedPhpEngine; use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\Templating\TemplateNameParser; use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index 1705c1ac06c18..b4b3acdfb47b9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -24,7 +24,10 @@ class Translator extends BaseTranslator { protected $container; - protected $options; + protected $options = array( + 'cache_dir' => null, + 'debug' => false, + ); protected $loaderIds; /** @@ -47,11 +50,6 @@ public function __construct(ContainerInterface $container, MessageSelector $sele $this->container = $container; $this->loaderIds = $loaderIds; - $this->options = array( - 'cache_dir' => null, - 'debug' => false, - ); - // check option names if ($diff = array_diff(array_keys($options), array_keys($this->options))) { throw new \InvalidArgumentException(sprintf('The Translator does not support the following options: \'%s\'.', implode('\', \'', $diff))); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index daa1eee38b641..e1ec07d3989f1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -18,28 +18,32 @@ "require": { "php": ">=5.3.3", "symfony/dependency-injection" : "~2.2", - "symfony/config" : "~2.2", - "symfony/event-dispatcher": "~2.1", - "symfony/http-kernel": "~2.3", + "symfony/config" : "~2.4", + "symfony/event-dispatcher": "~2.5", + "symfony/http-foundation": "~2.4", + "symfony/http-kernel": "~2.4", "symfony/filesystem": "~2.3", "symfony/routing": "~2.2", + "symfony/security-core": "~2.4", + "symfony/security-csrf": "~2.4", "symfony/stopwatch": "~2.3", "symfony/templating": "~2.1", "symfony/translation": "~2.3", - "doctrine/common": "~2.2" + "doctrine/annotations": "~1.0" }, "require-dev": { "symfony/finder": "~2.0", - "symfony/security": "~2.3", + "symfony/security": "~2.4", "symfony/form": "~2.3", "symfony/class-loader": "~2.1", "symfony/validator": "~2.1" }, "suggest": { - "symfony/console": "", - "symfony/finder": "", - "symfony/form": "", - "symfony/validator": "" + "symfony/console": "For using the console commands", + "symfony/finder": "For using the translation loader and cache warmer", + "symfony/form": "For using forms", + "symfony/validator": "For using validation", + "doctrine/cache": "For using alternative cache drivers" }, "autoload": { "psr-0": { "Symfony\\Bundle\\FrameworkBundle\\": "" } @@ -48,7 +52,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 724254de78b4d..0791bcbc798b8 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +2.4.0 +----- + + * Added 'host' option to firewall configuration + * Added 'csrf_token_generator' and 'csrf_token_id' options to firewall logout + listener configuration to supercede/alias 'csrf_provider' and 'intention' + respectively + * Moved 'security.secure_random' service configuration to FrameworkBundle + 2.3.0 ----- @@ -74,9 +83,9 @@ CHANGELOG logout: path: /logout_path target: / - csrf_parameter: _csrf_token # Optional (defaults to "_csrf_token") - csrf_provider: form.csrf_provider # Required to enable protection - intention: logout # Optional (defaults to "logout") + csrf_parameter: _csrf_token # Optional (defaults to "_csrf_token") + csrf_provider: security.csrf.token_generator # Required to enable protection + intention: logout # Optional (defaults to "logout") ``` If the LogoutListener has CSRF protection enabled but cannot validate a token, diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 95f1ced93d359..66f50f119c31b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -169,6 +169,7 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode) ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end() ->prototype('scalar')->end() ->end() + ->scalarNode('allow_if')->defaultNull()->end() ->end() ->fixXmlConfig('role') ->children() @@ -199,6 +200,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto $firewallNodeBuilder ->scalarNode('pattern')->end() + ->scalarNode('host')->end() ->booleanNode('security')->defaultTrue()->end() ->scalarNode('request_matcher')->end() ->scalarNode('access_denied_url')->end() @@ -210,10 +212,36 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->arrayNode('logout') ->treatTrueLike(array()) ->canBeUnset() + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['csrf_provider']) && isset($v['csrf_token_generator']); }) + ->thenInvalid("You should define a value for only one of 'csrf_provider' and 'csrf_token_generator' on a security firewall. Use 'csrf_token_generator' as this replaces 'csrf_provider'.") + ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['intention']) && isset($v['csrf_token_id']); }) + ->thenInvalid("You should define a value for only one of 'intention' and 'csrf_token_id' on a security firewall. Use 'csrf_token_id' as this replaces 'intention'.") + ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['csrf_provider']); }) + ->then(function ($v) { + $v['csrf_token_generator'] = $v['csrf_provider']; + unset($v['csrf_provider']); + + return $v; + }) + ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['intention']); }) + ->then(function ($v) { + $v['csrf_token_id'] = $v['intention']; + unset($v['intention']); + + return $v; + }) + ->end() ->children() ->scalarNode('csrf_parameter')->defaultValue('_csrf_token')->end() - ->scalarNode('csrf_provider')->cannotBeEmpty()->end() - ->scalarNode('intention')->defaultValue('logout')->end() + ->scalarNode('csrf_token_generator')->cannotBeEmpty()->end() + ->scalarNode('csrf_token_id')->defaultValue('logout')->end() ->scalarNode('path')->defaultValue('/logout')->end() ->scalarNode('target')->defaultValue('/')->end() ->scalarNode('success_handler')->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index 27f08b0a1f9d9..10032c6aa134e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -174,7 +174,7 @@ protected function createAuthenticationSuccessHandler($container, $id, $config) return $config['success_handler']; } - $successHandlerId = 'security.authentication.success_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); + $successHandlerId = $this->getSuccessHandlerId($id); $successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler')); $successHandler->replaceArgument(1, array_intersect_key($config, $this->defaultSuccessHandlerOptions)); @@ -189,11 +189,21 @@ protected function createAuthenticationFailureHandler($container, $id, $config) return $config['failure_handler']; } - $id = 'security.authentication.failure_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); + $id = $this->getFailureHandlerId($id); $failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler')); $failureHandler->replaceArgument(2, array_intersect_key($config, $this->defaultFailureHandlerOptions)); return $id; } + + protected function getSuccessHandlerId($id) + { + return 'security.authentication.success_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); + } + + protected function getFailureHandlerId($id) + { + return 'security.authentication.failure_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index ce06bba7d281a..b674c47e15bf0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -75,12 +75,10 @@ protected function createListener($container, $id, $config, $userProvider) { $listenerId = parent::createListener($container, $id, $config, $userProvider); - if (isset($config['csrf_provider'])) { - $container - ->getDefinition($listenerId) - ->addArgument(new Reference($config['csrf_provider'])) - ; - } + $container + ->getDefinition($listenerId) + ->addArgument(isset($config['csrf_provider']) ? new Reference($config['csrf_provider']) : null) + ; return $listenerId; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php new file mode 100644 index 0000000000000..9da6601ff547b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Jordi Boggiano + */ +class SimpleFormFactory extends FormLoginFactory +{ + public function __construct() + { + parent::__construct(); + + $this->addOption('authenticator', null); + } + + public function getKey() + { + return 'simple-form'; + } + + public function addConfiguration(NodeDefinition $node) + { + parent::addConfiguration($node); + + $node->children() + ->scalarNode('authenticator')->cannotBeEmpty()->end() + ->end(); + } + + protected function getListenerId() + { + return 'security.authentication.listener.simple_form'; + } + + protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) + { + $provider = 'security.authentication.provider.simple_form.'.$id; + $container + ->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.simple')) + ->replaceArgument(0, new Reference($config['authenticator'])) + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(2, $id) + ; + + return $provider; + } + + protected function createListener($container, $id, $config, $userProvider) + { + $listenerId = parent::createListener($container, $id, $config, $userProvider); + + $simpleAuthHandlerId = 'security.authentication.simple_success_failure_handler.'.$id; + $simpleAuthHandler = $container->setDefinition($simpleAuthHandlerId, new DefinitionDecorator('security.authentication.simple_success_failure_handler')); + $simpleAuthHandler->replaceArgument(0, new Reference($config['authenticator'])); + $simpleAuthHandler->replaceArgument(1, new Reference($this->getSuccessHandlerId($id))); + $simpleAuthHandler->replaceArgument(2, new Reference($this->getFailureHandlerId($id))); + + $listener = $container->getDefinition($listenerId); + $listener->replaceArgument(5, new Reference($simpleAuthHandlerId)); + $listener->replaceArgument(6, new Reference($simpleAuthHandlerId)); + $listener->addArgument(new Reference($config['authenticator'])); + + return $listenerId; + } + + protected function createEntryPoint($container, $id, $config, $defaultEntryPoint) + { + $entryPointId = 'security.authentication.form_entry_point.'.$id; + $container + ->setDefinition($entryPointId, new DefinitionDecorator('security.authentication.form_entry_point')) + ->addArgument(new Reference('security.http_utils')) + ->addArgument($config['login_path']) + ->addArgument($config['use_forward']) + ; + + return $entryPointId; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php new file mode 100644 index 0000000000000..27d8c5f050ec5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.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\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Jordi Boggiano + */ +class SimplePreAuthenticationFactory implements SecurityFactoryInterface +{ + public function getPosition() + { + return 'pre_auth'; + } + + public function getKey() + { + return 'simple-preauth'; + } + + public function addConfiguration(NodeDefinition $node) + { + $node + ->children() + ->scalarNode('provider')->end() + ->scalarNode('authenticator')->cannotBeEmpty()->end() + ->end() + ; + } + + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + { + $provider = 'security.authentication.provider.simple_preauth.'.$id; + $container + ->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.simple')) + ->replaceArgument(0, new Reference($config['authenticator'])) + ->replaceArgument(1, new Reference($userProvider)) + ->replaceArgument(2, $id) + ; + + // listener + $listenerId = 'security.authentication.listener.simple_preauth.'.$id; + $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.simple_preauth')); + $listener->replaceArgument(2, $id); + $listener->replaceArgument(3, new Reference($config['authenticator'])); + + return array($provider, $listenerId, null); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 8dbeb38f1f6ee..a9b9b0e92ac00 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -14,7 +14,6 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\HttpKernel\DependencyInjection\Extension; @@ -23,6 +22,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; /** * SecurityExtension. @@ -33,10 +33,12 @@ class SecurityExtension extends Extension { private $requestMatchers = array(); + private $expressions = array(); private $contextListeners = array(); private $listenerPositions = array('pre_auth', 'form', 'http', 'remember_me'); private $factories = array(); private $userProviderFactories = array(); + private $expressionLanguage; public function __construct() { @@ -188,8 +190,13 @@ private function createAuthorization($config, ContainerBuilder $container) $access['ips'] ); + $attributes = $access['roles']; + if ($access['allow_if']) { + $attributes[] = $this->createExpression($container, $access['allow_if']); + } + $container->getDefinition('security.access_map') - ->addMethodCall('add', array($matcher, $access['roles'], $access['requires_channel'])); + ->addMethodCall('add', array($matcher, $attributes, $access['requires_channel'])); } } @@ -244,8 +251,10 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $matcher = null; if (isset($firewall['request_matcher'])) { $matcher = new Reference($firewall['request_matcher']); - } elseif (isset($firewall['pattern'])) { - $matcher = $this->createRequestMatcher($container, $firewall['pattern']); + } elseif (isset($firewall['pattern']) || isset($firewall['host'])) { + $pattern = isset($firewall['pattern']) ? $firewall['pattern'] : null; + $host = isset($firewall['host']) ? $firewall['host'] : null; + $matcher = $this->createRequestMatcher($container, $pattern, $host); } // Security disabled? @@ -282,7 +291,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener')); $listener->replaceArgument(3, array( 'csrf_parameter' => $firewall['logout']['csrf_parameter'], - 'intention' => $firewall['logout']['intention'], + 'intention' => $firewall['logout']['csrf_token_id'], 'logout_path' => $firewall['logout']['path'], )); $listeners[] = new Reference($listenerId); @@ -298,8 +307,8 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); // add CSRF provider - if (isset($firewall['logout']['csrf_provider'])) { - $listener->addArgument(new Reference($firewall['logout']['csrf_provider'])); + if (isset($firewall['logout']['csrf_token_generator'])) { + $listener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); } // add session logout handler @@ -327,9 +336,9 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a ->addMethodCall('registerListener', array( $id, $firewall['logout']['path'], - $firewall['logout']['intention'], + $firewall['logout']['csrf_token_id'], $firewall['logout']['csrf_parameter'], - isset($firewall['logout']['csrf_provider']) ? new Reference($firewall['logout']['csrf_provider']) : null, + isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null, )) ; } @@ -594,6 +603,22 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv return $switchUserListenerId; } + private function createExpression($container, $expression) + { + if (isset($this->expressions[$id = 'security.expression.'.sha1($expression)])) { + return $this->expressions[$id]; + } + + $container + ->register($id, 'Symfony\Component\ExpressionLanguage\SerializedParsedExpression') + ->setPublic(false) + ->addArgument($expression) + ->addArgument(serialize($this->getExpressionLanguage()->parse($expression, array('token', 'user', 'object', 'roles', 'request'))->getNodes())) + ; + + return $this->expressions[$id] = new Reference($id); + } + private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array()) { $serialized = serialize(array($path, $host, $methods, $ip, $attributes)); @@ -652,4 +677,16 @@ public function getConfiguration(array $config, ContainerBuilder $container) // first assemble the factories return new MainConfiguration($this->factories, $this->userProviderFactories); } + + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/AclSchemaListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/AclSchemaListener.php index 53758abb761b3..8faa9ac366fd6 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/AclSchemaListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/AclSchemaListener.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\EventListener; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Security\Acl\Dbal\Schema; use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; /** @@ -21,16 +21,16 @@ */ class AclSchemaListener { - private $container; + private $schema; - public function __construct(ContainerInterface $container) + public function __construct(Schema $schema) { - $this->container = $container; + $this->schema = $schema; } public function postGenerateSchema(GenerateSchemaEventArgs $args) { $schema = $args->getSchema(); - $this->container->get('security.acl.dbal.schema')->addToSchema($schema); + $this->schema->addToSchema($schema); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml index f6106f7f70513..84f836c6e74a0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -10,7 +10,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index dd2a7fc30d116..d90f3206dba07 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -32,17 +32,21 @@ Symfony\Component\Security\Core\Authorization\Voter\RoleVoter Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter + Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter Symfony\Component\Security\Http\Firewall Symfony\Bundle\SecurityBundle\Security\FirewallMap Symfony\Bundle\SecurityBundle\Security\FirewallContext Symfony\Component\HttpFoundation\RequestMatcher + Symfony\Component\HttpFoundation\ExpressionRequestMatcher Symfony\Component\Security\Core\Role\RoleHierarchy Symfony\Component\Security\Http\HttpUtils Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator + + Symfony\Component\Security\Core\Authorization\ExpressionLanguage @@ -78,6 +82,7 @@ + @@ -104,6 +109,13 @@ + + + + + + + @@ -139,12 +151,5 @@ - - - - - %kernel.cache_dir%/secure_random.seed - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_acl_dbal.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_acl_dbal.xml index aac84a3ce5311..a61d648b0b709 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_acl_dbal.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_acl_dbal.xml @@ -37,7 +37,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 7144f562f329b..a8d9bf316c8db 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -12,6 +12,10 @@ Symfony\Component\Security\Http\EntryPoint\FormAuthenticationEntryPoint Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener + Symfony\Component\Security\Http\Firewall\SimpleFormAuthenticationListener + + Symfony\Component\Security\Http\Firewall\SimplePreAuthenticationListener + Symfony\Component\Security\Http\Firewall\BasicAuthenticationListener Symfony\Component\Security\Http\EntryPoint\BasicAuthenticationEntryPoint @@ -35,12 +39,14 @@ Symfony\Component\Security\Http\Firewall\ContextListener Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider + Symfony\Component\Security\Core\Authentication\Provider\SimpleAuthenticationProvider Symfony\Component\Security\Core\Authentication\Provider\PreAuthenticatedAuthenticationProvider Symfony\Component\Security\Core\Authentication\Provider\AnonymousAuthenticationProvider Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler + Symfony\Component\Security\Http\Authentication\SimpleAuthenticationHandler @@ -133,6 +139,29 @@ abstract="true"> + + + + + + + + + + + + + + + + + + + + @@ -170,6 +199,12 @@ %security.authentication.hide_user_not_found% + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 7d810fde389f9..5de413658632e 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -19,6 +19,8 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpDigestFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimplePreAuthenticationFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimpleFormFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMemoryFactory; /** @@ -38,6 +40,8 @@ public function build(ContainerBuilder $container) $extension->addSecurityListenerFactory(new HttpDigestFactory()); $extension->addSecurityListenerFactory(new RememberMeFactory()); $extension->addSecurityListenerFactory(new X509Factory()); + $extension->addSecurityListenerFactory(new SimplePreAuthenticationFactory()); + $extension->addSecurityListenerFactory(new SimpleFormFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); $container->addCompilerPass(new AddSecurityVotersPass()); diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php index c7135f54e1f70..50434bda0befd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php @@ -12,8 +12,10 @@ namespace Symfony\Bundle\SecurityBundle\Templating\Helper; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Templating\Helper\Helper; /** @@ -24,7 +26,7 @@ class LogoutUrlHelper extends Helper { private $container; - private $listeners; + private $listeners = array(); private $router; /** @@ -37,21 +39,26 @@ public function __construct(ContainerInterface $container, UrlGeneratorInterface { $this->container = $container; $this->router = $router; - $this->listeners = array(); } /** * Registers a firewall's LogoutListener, allowing its URL to be generated. * - * @param string $key The firewall key - * @param string $logoutPath The path that starts the logout process - * @param string $intention The intention for CSRF token generation - * @param string $csrfParameter The CSRF token parameter name - * @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance + * @param string $key The firewall key + * @param string $logoutPath The path that starts the logout process + * @param string $csrfTokenId The ID of the CSRF token + * @param string $csrfParameter The CSRF token parameter name + * @param CsrfTokenManagerInterface $csrfTokenManager A CsrfTokenManagerInterface instance */ - public function registerListener($key, $logoutPath, $intention, $csrfParameter, CsrfProviderInterface $csrfProvider = null) + public function registerListener($key, $logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager = null) { - $this->listeners[$key] = array($logoutPath, $intention, $csrfParameter, $csrfProvider); + if ($csrfTokenManager instanceof CsrfProviderInterface) { + $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); + } elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) { + throw new \InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.'); + } + + $this->listeners[$key] = array($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager); } /** @@ -94,9 +101,9 @@ private function generateLogoutUrl($key, $referenceType) throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key)); } - list($logoutPath, $intention, $csrfParameter, $csrfProvider) = $this->listeners[$key]; + list($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager) = $this->listeners[$key]; - $parameters = null !== $csrfProvider ? array($csrfParameter => $csrfProvider->generateCsrfToken($intention)) : array(); + $parameters = null !== $csrfTokenManager ? array($csrfParameter => (string) $csrfTokenManager->getToken($csrfTokenId)) : array(); if ('/' === $logoutPath[0]) { $request = $this->container->get('request'); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 2e55643c7015f..b89214fa31e31 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -17,6 +17,7 @@ use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\ExpressionLanguage\Expression; abstract class CompleteConfigurationTest extends \PHPUnit_Framework_TestCase { @@ -85,9 +86,41 @@ public function testFirewalls() 'security.access_listener', 'security.authentication.switchuser_listener.secure', ), + array( + 'security.channel_listener', + 'security.context_listener.0', + 'security.authentication.listener.basic.host', + 'security.authentication.listener.anonymous.host', + 'security.access_listener', + ), ), $listeners); } + public function testFirewallRequestMatchers() + { + $container = $this->getContainer('container1'); + + $arguments = $container->getDefinition('security.firewall.map')->getArguments(); + $matchers = array(); + + foreach ($arguments[1] as $reference) { + if ($reference instanceof Reference) { + $definition = $container->getDefinition((string) $reference); + $matchers[] = $definition->getArguments(); + } + } + + $this->assertEquals(array( + array( + '/login', + ), + array( + '/test', + 'foo\\.example\\.org', + ), + ), $matchers); + } + public function testAccess() { $container = $this->getContainer('container1'); @@ -101,7 +134,7 @@ public function testAccess() $matcherIds = array(); foreach ($rules as $rule) { - list($matcherId, $roles, $channel) = $rule; + list($matcherId, $attributes, $channel) = $rule; $requestMatcher = $container->getDefinition($matcherId); $this->assertFalse(isset($matcherIds[$matcherId])); @@ -109,19 +142,23 @@ public function testAccess() $i = count($matcherIds); if (1 === $i) { - $this->assertEquals(array('ROLE_USER'), $roles); + $this->assertEquals(array('ROLE_USER'), $attributes); $this->assertEquals('https', $channel); $this->assertEquals( array('/blog/524', null, array('GET', 'POST')), $requestMatcher->getArguments() ); } elseif (2 === $i) { - $this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $roles); + $this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $attributes); $this->assertNull($channel); $this->assertEquals( array('/blog/.*'), $requestMatcher->getArguments() ); + } elseif (3 === $i) { + $this->assertEquals('IS_AUTHENTICATED_ANONYMOUSLY', $attributes[0]); + $expression = $container->getDefinition($attributes[1])->getArgument(0); + $this->assertEquals("token.getUsername() matches '/^admin/'", $expression); } } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php index bb79da3fb05a6..e2e523552cb56 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php @@ -71,11 +71,18 @@ 'x509' => true, 'logout' => true, ), + 'host' => array( + 'pattern' => '/test', + 'host' => 'foo\\.example\\.org', + 'anonymous' => true, + 'http_basic' => true, + ), ), 'access_control' => array( array('path' => '/blog/524', 'role' => 'ROLE_USER', 'requires_channel' => 'https', 'methods' => array('get', 'POST')), array('path' => '/blog/.*', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'), + array('path' => '/blog/524', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'allow_if' => "token.getUsername() matches '/^admin/'"), ), 'role_hierarchy' => array( diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml index cb452e9316693..04b19a3b88ac3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml @@ -57,11 +57,17 @@ + + + + + ROLE_USER ROLE_USER,ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH ROLE_USER,ROLE_ADMIN + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml index 169f7fa431261..76c2792fe7ad6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml @@ -53,6 +53,11 @@ security: switch_user: true x509: true logout: true + host: + pattern: /test + host: foo\.example\.org + anonymous: true + http_basic: true role_hierarchy: ROLE_ADMIN: ROLE_USER @@ -64,3 +69,4 @@ security: - path: /blog/.* role: IS_AUTHENTICATED_ANONYMOUSLY + - { path: /blog/524, role: IS_AUTHENTICATED_ANONYMOUSLY, allow_if: "token.getUsername() matches '/^admin/'" } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index 047821cfdb378..402b321968739 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php @@ -67,4 +67,49 @@ public function testManyConfigForProvider() $configuration = new MainConfiguration(array(), array()); $config = $processor->processConfiguration($configuration, array($config)); } + + public function testCsrfAliases() + { + $config = array( + 'firewalls' => array( + 'stub' => array( + 'logout' => array( + 'csrf_provider' => 'a_token_generator', + 'intention' => 'a_token_id', + ), + ), + ), + ); + $config = array_merge(static::$minimalConfig, $config); + + $processor = new Processor(); + $configuration = new MainConfiguration(array(), array()); + $processedConfig = $processor->processConfiguration($configuration, array($config)); + $this->assertTrue(isset($processedConfig['firewalls']['stub']['logout']['csrf_token_generator'])); + $this->assertEquals('a_token_generator', $processedConfig['firewalls']['stub']['logout']['csrf_token_generator']); + $this->assertTrue(isset($processedConfig['firewalls']['stub']['logout']['csrf_token_id'])); + $this->assertEquals('a_token_id', $processedConfig['firewalls']['stub']['logout']['csrf_token_id']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testCsrfOriginalAndAliasValueCausesException() + { + $config = array( + 'firewalls' => array( + 'stub' => array( + 'logout' => array( + 'csrf_token_id' => 'a_token_id', + 'intention' => 'old_name', + ), + ), + ), + ); + $config = array_merge(static::$minimalConfig, $config); + + $processor = new Processor(); + $configuration = new MainConfiguration(array(), array()); + $processedConfig = $processor->processConfiguration($configuration, array($config)); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 0605a1619ef9d..dd97208ef5461 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -83,9 +83,9 @@ protected function getRawContainer() protected function getContainer() { - $containter = $this->getRawContainer(); - $containter->compile(); + $container = $this->getRawContainer(); + $container->compile(); - return $containter; + return $container; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/config/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/config/routing.yml index 535df3576c2fb..6992f80a0a124 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/config/routing.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/config/routing.yml @@ -37,3 +37,6 @@ form_logout: form_secure_action: path: /secure-but-not-covered-by-access-control defaults: { _controller: FormLoginBundle:Login:secure } + +protected-via-expression: + path: /protected-via-expression diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php index bb16373c1bbf6..d059a0bff6b74 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php @@ -91,6 +91,28 @@ public function testSecurityConfigurationForMultipleIPAddresses($config) $this->assertRestricted($barredClient, '/secured-by-two-ips'); } + /** + * @dataProvider getConfigs + */ + public function testSecurityConfigurationForExpression($config) + { + $allowedClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array('HTTP_USER_AGENT' => 'Firefox 1.0')); + $this->assertAllowed($allowedClient, '/protected-via-expression'); + + $barredClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array()); + $this->assertRestricted($barredClient, '/protected-via-expression'); + + $allowedClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array()); + + $allowedClient->request('GET', '/protected-via-expression'); + $form = $allowedClient->followRedirect()->selectButton('login')->form(); + $form['_username'] = 'johannes'; + $form['_password'] = 'test'; + $allowedClient->submit($form); + $this->assertRedirect($allowedClient->getResponse(), '/protected-via-expression'); + $this->assertAllowed($allowedClient, '/protected-via-expression'); + } + private function assertAllowed($client, $path) { $client->request('GET', $path); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php index b4b906f172210..f1be0f042fc4e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php @@ -23,15 +23,6 @@ public static function assertRedirect($response, $location) self::assertEquals('http://localhost'.$location, $response->headers->get('Location')); } - protected function setUp() - { - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - - parent::setUp(); - } - protected function deleteTmpDir($testCase) { if (!file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/'.$testCase)) { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml index e0347e1dc4e00..e1e2b0e883933 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml @@ -37,12 +37,12 @@ security: username_parameter: "user_login[username]" password_parameter: "user_login[password]" csrf_parameter: "user_login[_token]" - csrf_provider: form.csrf_provider + csrf_provider: security.csrf.token_manager anonymous: ~ logout: path: /logout_path target: / - csrf_provider: form.csrf_provider + csrf_provider: security.csrf.token_manager access_control: - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml index 58bd9f2d8af3c..624637b0c82aa 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml @@ -31,4 +31,5 @@ security: - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/highly_protected_resource$, roles: IS_ADMIN } + - { path: ^/protected-via-expression$, allow_if: "(is_anonymous() and request.headers.get('user-agent') matches '/Firefox/i') or has_role('ROLE_USER')" } - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 776f590bc87c1..c54213c9bfb39 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -17,15 +17,15 @@ ], "require": { "php": ">=5.3.3", - "symfony/security": "~2.2", + "symfony/security": "~2.4", "symfony/http-kernel": "~2.2" }, "require-dev": { "symfony/framework-bundle": "~2.2", "symfony/twig-bundle": "~2.2", - "symfony/form": "~2.1", "symfony/validator": "~2.2", - "symfony/yaml": "~2.0" + "symfony/yaml": "~2.0", + "symfony/expression-language": "~2.4" }, "autoload": { "psr-0": { "Symfony\\Bundle\\SecurityBundle\\": "" } @@ -34,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php index aa01ce6e7b0aa..95c892cb0433b 100644 --- a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php +++ b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php @@ -11,141 +11,68 @@ namespace Symfony\Bundle\TwigBundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Bridge\Twig\Command\LintCommand as BaseLintCommand; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\Finder\Finder; /** * Command that will validate your template syntax and output encountered errors. * * @author Marc Weistroff + * @author Jérôme Tamarelle */ -class LintCommand extends ContainerAwareCommand +class LintCommand extends BaseLintCommand implements ContainerAwareInterface { - protected function configure() + /** + * @var ContainerInterface|null + */ + private $container; + + /** + * {@inheritdoc} + */ + public function setContainer(ContainerInterface $container = null) { - $this - ->setName('twig:lint') - ->setDescription('Lints a template and outputs encountered errors') - ->addArgument('filename') - ->setHelp(<<%command.name% command lints a template and outputs to stdout -the first encountered syntax error. - -php %command.full_name% filename - -The command gets the contents of filename and validates its syntax. - -php %command.full_name% dirname - -The command finds all twig templates in dirname and validates the syntax -of each Twig template. - -php %command.full_name% @AcmeMyBundle - -The command finds all twig templates in the AcmeMyBundle bundle and validates -the syntax of each Twig template. - -cat filename | php %command.full_name% - -The command gets the template contents from stdin and validates its syntax. -EOF - ) - ; + $this->container = $container; } - protected function execute(InputInterface $input, OutputInterface $output) + /** + * {@inheritdoc} + */ + protected function getTwigEnvironment() { - $twig = $this->getContainer()->get('twig'); - $template = null; - $filename = $input->getArgument('filename'); - - if (!$filename) { - if (0 !== ftell(STDIN)) { - throw new \RuntimeException("Please provide a filename or pipe template content to stdin."); - } - - while (!feof(STDIN)) { - $template .= fread(STDIN, 1024); - } - - return $this->validateTemplate($twig, $output, $template); - } - - if (0 !== strpos($filename, '@') && !is_readable($filename)) { - throw new \RuntimeException(sprintf('File or directory "%s" is not readable', $filename)); - } - - $files = array(); - if (is_file($filename)) { - $files = array($filename); - } elseif (is_dir($filename)) { - $files = Finder::create()->files()->in($filename)->name('*.twig'); - } else { - $dir = $this->getApplication()->getKernel()->locateResource($filename); - $files = Finder::create()->files()->in($dir)->name('*.twig'); - } - - $errors = 0; - foreach ($files as $file) { - $errors += $this->validateTemplate($twig, $output, file_get_contents($file), $file); - } - - return $errors > 0 ? 1 : 0; + return $this->container->get('twig'); } - protected function validateTemplate(\Twig_Environment $twig, OutputInterface $output, $template, $file = null) + /** + * {@inheritdoc} + */ + protected function configure() { - try { - $twig->parse($twig->tokenize($template, $file ? (string) $file : null)); - $output->writeln('OK'.($file ? sprintf(' in %s', $file) : '')); - } catch (\Twig_Error $e) { - $this->renderException($output, $template, $e, $file); + parent::configure(); - return 1; - } - - return 0; - } + $this + ->setHelp( + $this->getHelp().<<getTemplateLine(); - $lines = $this->getContext($template, $line); - if ($file) { - $output->writeln(sprintf("KO in %s (line %s)", $file, $line)); - } else { - $output->writeln(sprintf("KO (line %s)", $line)); - } +Or all template files in a bundle: - foreach ($lines as $no => $code) { - $output->writeln(sprintf( - "%s %-6s %s", - $no == $line ? '>>' : ' ', - $no, - $code - )); - if ($no == $line) { - $output->writeln(sprintf('>> %s ', $exception->getRawMessage())); - } - } +php %command.full_name% @AcmeDemoBundle +EOF + ) + ; } - protected function getContext($template, $line, $context = 3) + protected function findFiles($filename) { - $lines = explode("\n", $template); - - $position = max(0, $line - $context); - $max = min(count($lines), $line - 1 + $context); + if (0 === strpos($filename, '@')) { + $dir = $this->getApplication()->getKernel()->locateResource($filename); - $result = array(); - while ($position < $max) { - $result[$position + 1] = $lines[$position]; - $position++; + return Finder::create()->files()->in($dir)->name('*.twig'); } - return $result; + return parent::findFiles($filename); } } diff --git a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php index f76b5d0b8ec17..0fe62fbcf4d85 100644 --- a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php +++ b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php @@ -11,13 +11,13 @@ namespace Symfony\Bundle\TwigBundle\Loader; -use Symfony\Component\Templating\TemplateNameParserInterface; use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Templating\TemplateNameParserInterface; use Symfony\Component\Templating\TemplateReferenceInterface; /** * FilesystemLoader extends the default Twig filesystem loader - * to work with the Symfony2 paths. + * to work with the Symfony2 paths and template references. * * @author Fabien Potencier */ @@ -38,21 +38,22 @@ public function __construct(FileLocatorInterface $locator, TemplateNameParserInt $this->locator = $locator; $this->parser = $parser; - $this->cache = array(); } /** * {@inheritdoc} + * + * The name parameter might also be a TemplateReferenceInterface. */ - public function exists($template) + public function exists($name) { - if (parent::exists($template)) { + if (parent::exists((string) $name)) { return true; } // same logic as findTemplate below for the fallback try { - $this->cache[(string) $template] = $this->locator->locate($this->parser->parse($template)); + $this->cache[(string) $name] = $this->locator->locate($this->parser->parse($name)); } catch (\Exception $e) { return false; } @@ -84,7 +85,7 @@ protected function findTemplate($template) $file = null; $previous = null; try { - $file = parent::findTemplate($template); + $file = parent::findTemplate($logicalName); } catch (\Twig_Error_Loader $e) { $previous = $e; diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 971f4f17ebefd..5efbe9d6d4f41 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -18,6 +18,8 @@ Symfony\Bridge\Twig\Extension\YamlExtension Symfony\Bridge\Twig\Extension\FormExtension Symfony\Bridge\Twig\Extension\HttpKernelExtension + Symfony\Bridge\Twig\Extension\StopwatchExtension + Symfony\Bridge\Twig\Extension\ExpressionExtension Symfony\Bridge\Twig\Form\TwigRendererEngine Symfony\Bridge\Twig\Form\TwigRenderer Symfony\Bridge\Twig\Translation\TwigExtractor @@ -86,6 +88,15 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php index ce2e821f46f19..c6d5e277a2486 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php @@ -11,11 +11,9 @@ namespace Symfony\Bundle\TwigBundle\Tests\Loader; -use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader; -use Symfony\Component\Config\FileLocatorInterface; use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; -use Symfony\Component\Templating\TemplateNameParserInterface; +use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader; +use Symfony\Bundle\TwigBundle\Tests\TestCase; class FilesystemLoaderTest extends TestCase { diff --git a/src/Symfony/Bundle/TwigBundle/Tests/TestCase.php b/src/Symfony/Bundle/TwigBundle/Tests/TestCase.php index a3848ef472f3d..0c7af8ae4b859 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/TestCase.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/TestCase.php @@ -13,10 +13,4 @@ class TestCase extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - } } diff --git a/src/Symfony/Bundle/TwigBundle/TwigEngine.php b/src/Symfony/Bundle/TwigBundle/TwigEngine.php index 72ea0c49c9001..5ce5c9da9d320 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigEngine.php +++ b/src/Symfony/Bundle/TwigBundle/TwigEngine.php @@ -66,16 +66,7 @@ public function guessDefaultEscapingStrategy($filename) } /** - * Renders a template. - * - * @param mixed $name A template name - * @param array $parameters An array of parameters to pass to the template - * - * @return string The evaluated template as a string - * - * @throws \InvalidArgumentException if the template does not exist - * @throws \RuntimeException if the template cannot be rendered - * @throws \Twig_Error + * {@inheritdoc} */ public function render($name, array $parameters = array()) { @@ -95,13 +86,9 @@ public function render($name, array $parameters = array()) } /** - * Renders a view and returns a Response. - * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * @param Response $response A Response instance + * {@inheritdoc} * - * @return Response A Response instance + * @throws \Twig_Error if something went wrong like a thrown exception while rendering the template */ public function renderResponse($view, array $parameters = array(), Response $response = null) { diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 65933fcf89531..88d866989ad20 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=5.3.3", - "symfony/twig-bridge": "~2.2", + "symfony/twig-bridge": "~2.5", "symfony/http-kernel": "~2.1" }, "require-dev": { @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php index e4b4cb72a078d..d98042dcf5162 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php @@ -44,18 +44,13 @@ public function load(array $configs, ContainerBuilder $container) $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('profiler.xml'); - $loader->load('toolbar.xml'); - - $container->setParameter('web_profiler.debug_toolbar.intercept_redirects', $config['intercept_redirects']); + $container->setParameter('web_profiler.debug_toolbar.position', $config['position']); - if (!$config['toolbar']) { - $mode = WebDebugToolbarListener::DISABLED; - } else { - $mode = WebDebugToolbarListener::ENABLED; + if ($config['toolbar'] || $config['intercept_redirects']) { + $loader->load('toolbar.xml'); + $container->setParameter('web_profiler.debug_toolbar.intercept_redirects', $config['intercept_redirects']); + $container->setParameter('web_profiler.debug_toolbar.mode', $config['toolbar'] ? WebDebugToolbarListener::ENABLED : WebDebugToolbarListener::DISABLED); } - - $container->setParameter('web_profiler.debug_toolbar.mode', $mode); - $container->setParameter('web_profiler.debug_toolbar.position', $config['position']); } /** diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index 8835e9d98e65e..8e3ac9d808a9f 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -13,10 +13,10 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; /** * WebDebugToolbarListener injects the Web Debug Toolbar. @@ -34,13 +34,15 @@ class WebDebugToolbarListener implements EventSubscriberInterface const ENABLED = 2; protected $twig; + protected $urlGenerator; protected $interceptRedirects; protected $mode; protected $position; - public function __construct(\Twig_Environment $twig, $interceptRedirects = false, $mode = self::ENABLED, $position = 'bottom') + public function __construct(\Twig_Environment $twig, $interceptRedirects = false, $mode = self::ENABLED, $position = 'bottom', UrlGeneratorInterface $urlGenerator = null) { $this->twig = $twig; + $this->urlGenerator = $urlGenerator; $this->interceptRedirects = (Boolean) $interceptRedirects; $this->mode = (integer) $mode; $this->position = $position; @@ -53,13 +55,20 @@ public function isEnabled() public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { - return; - } - $response = $event->getResponse(); $request = $event->getRequest(); + if ($response->headers->has('X-Debug-Token') && null !== $this->urlGenerator) { + $response->headers->set( + 'X-Debug-Token-Link', + $this->urlGenerator->generate('_profiler', array('token' => $response->headers->get('X-Debug-Token'))) + ); + } + + if (!$event->isMasterRequest()) { + return; + } + // do not capture redirects or modify XML HTTP Requests if ($request->isXmlHttpRequest()) { return; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml index 1372b0a61f10a..091127eaf9532 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml @@ -15,6 +15,7 @@ %web_profiler.debug_toolbar.intercept_redirects% %web_profiler.debug_toolbar.mode% %web_profiler.debug_toolbar.position% + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig new file mode 100644 index 0000000000000..6d6d07d240ab0 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -0,0 +1,659 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% from _self import form_tree_entry, form_tree_details %} + +{% block toolbar %} + {% if collector.data|length %} + {% set icon %} + Forms + {% if collector.data.nb_errors %}{{ collector.data.nb_errors }}{% else %}{{ collector.data.forms|length }}{% endif %} + {% endset %} + + {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %} + {% endif %} +{% endblock %} + +{% block menu %} + + + Forms + {% if collector.data.forms|length %} + {{ collector.data.forms|length }} + {% endif %} + +{% endblock %} + +{% block panel %} + + + {% if collector.data.forms|length %} +
+
+

Forms

+ +
    + {% for formName, formData in collector.data.forms %} + {{ form_tree_entry(formName, formData, true) }} + {% endfor %} +
+
+ + {% for formName, formData in collector.data.forms %} + {{ form_tree_details(formName, formData, collector.data.forms_by_hash) }} + {% endfor %} +
+ {% else %} +

No forms were submitted for this request.

+ {% endif %} + + +{% endblock %} + +{% macro form_tree_entry(name, data, expanded) %} +
  • +
    + {% if data.children is not empty %} + + {% else %} +
    + {% endif %} + {{ name }} + {% if data.errors is defined and data.errors|length > 0 %} +
    {{ data.errors|length }}
    + {% endif %} +
    + + {% if data.children is not empty %} + + {% endif %} +
  • +{% endmacro %} + +{% macro form_tree_details(name, data, forms_by_hash) %} +
    +

    + {{ name }} + {% if data.type_class is defined %} + [{{ data.type }}] + {% endif %} +

    + + {% if data.errors is defined and data.errors|length > 0 %} +
    +

    + + Errors + + +

    + + + + + + + + {% for error in data.errors %} + + + + + + {% endfor %} +
    MessageOriginCause
    {{ error.message }} + {% if error.origin is empty %} + This form. + {% elseif forms_by_hash[error.origin] is not defined %} + Unknown. + {% else %} + {{ forms_by_hash[error.origin].name }} + {% endif %} + + {% if error.cause is empty %} + Unknown. + {% elseif error.cause.root is defined %} + Constraint Violation
    +
    {{ error.cause.root }}{% if error.cause.path is not empty %}{% if error.cause.path|first != '[' %}.{% endif %}{{ error.cause.path }}{% endif %} = {{ error.cause.value }}
    + {% else %} +
    {{ error.cause }}
    + {% endif %} +
    +
    + {% endif %} + + {% if data.default_data is defined %} +

    + + Default Data + + +

    + +
    + + + + + + + + + + + + + +
    Model Format + {% if data.default_data.model is defined %} +
    {{ data.default_data.model }}
    + {% else %} + same as normalized format + {% endif %} +
    Normalized Format
    {{ data.default_data.norm }}
    View Format + {% if data.default_data.view is defined %} +
    {{ data.default_data.view }}
    + {% else %} + same as normalized format + {% endif %} +
    +
    + {% endif %} + + {% if data.submitted_data is defined %} +

    + + Submitted Data + + +

    + +
    + {% if data.submitted_data.norm is defined %} + + + + + + + + + + + + + +
    View Format + {% if data.submitted_data.view is defined %} +
    {{ data.submitted_data.view }}
    + {% else %} + same as normalized format + {% endif %} +
    Normalized Format
    {{ data.submitted_data.norm }}
    Model Format + {% if data.submitted_data.model is defined %} +
    {{ data.submitted_data.model }}
    + {% else %} + same as normalized format + {% endif %} +
    + {% else %} +

    This form was not submitted.

    + {% endif %} +
    + {% endif %} + + {% if data.passed_options is defined %} +

    + + Passed Options + + +

    + +
    + {% if data.passed_options|length %} + + + + + + + {% for option, value in data.passed_options %} + + + + + + {% endfor %} +
    OptionPassed ValueResolved Value
    {{ option }}
    {{ value }}
    + {% if data.resolved_options[option] is sameas(value) %} + same as passed value + {% else %} +
    {{ data.resolved_options[option] }}
    + {% endif %} +
    + {% else %} +

    No options where passed when constructing this form.

    + {% endif %} +
    + {% endif %} + + {% if data.resolved_options is defined %} +

    + + Resolved Options + + +

    + + + {% endif %} + + {% if data.view_vars is defined %} +

    + + View Variables + + +

    + + + {% endif %} +
    + + {% for childName, childData in data.children %} + {{ _self.form_tree_details(childName, childData, forms_by_hash) }} + {% endfor %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index c4e3df610e913..68fb3e5c74573 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -22,15 +22,19 @@ }, hasClass = function(el, klass) { - return el.className.match(new RegExp('\\b' + klass + '\\b')); + return el.className && el.className.match(new RegExp('\\b' + klass + '\\b')); }, removeClass = function(el, klass) { - el.className = el.className.replace(new RegExp('\\b' + klass + '\\b'), ' '); + if (el.className) { + el.className = el.className.replace(new RegExp('\\b' + klass + '\\b'), ' '); + } }, addClass = function(el, klass) { - if (!hasClass(el, klass)) { el.className += " " + klass; } + if (!hasClass(el, klass)) { + el.className += " " + klass; + } }, getPreference = function(name) { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 7f10150c72bfe..3b45ca11163ec 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -63,6 +63,9 @@ table th, table td { font-size: 12px; padding: 8px 10px; } +table td em { + color: #aaa; +} fieldset { border: none; } @@ -70,6 +73,9 @@ abbr { border-bottom: 1px dotted #000; cursor: help; } +pre, code { + font-size: 0.9em; +} .clear { clear: both; height: 0; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php index f9dd1eacd91c8..f114e74671f33 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php @@ -86,7 +86,7 @@ public function testDefaultConfig($debug) $extension = new WebProfilerExtension(); $extension->load(array(array()), $this->container); - $this->assertFalse($this->container->get('web_profiler.debug_toolbar')->isEnabled()); + $this->assertFalse($this->container->has('web_profiler.debug_toolbar')); $this->assertSaneContainer($this->getDumpedContainer()); } @@ -94,12 +94,16 @@ public function testDefaultConfig($debug) /** * @dataProvider getDebugModes */ - public function testToolbarConfig($enabled) + public function testToolbarConfig($toolbarEnabled, $interceptRedirects, $listenerInjected, $listenerEnabled) { $extension = new WebProfilerExtension(); - $extension->load(array(array('toolbar' => $enabled)), $this->container); + $extension->load(array(array('toolbar' => $toolbarEnabled, 'intercept_redirects' => $interceptRedirects)), $this->container); - $this->assertSame($enabled, $this->container->get('web_profiler.debug_toolbar')->isEnabled()); + $this->assertSame($listenerInjected, $this->container->has('web_profiler.debug_toolbar')); + + if ($listenerInjected) { + $this->assertSame($listenerEnabled, $this->container->get('web_profiler.debug_toolbar')->isEnabled()); + } $this->assertSaneContainer($this->getDumpedContainer()); } @@ -107,8 +111,10 @@ public function testToolbarConfig($enabled) public function getDebugModes() { return array( - array(true), - array(false), + array(false, false, false, false), + array(true, false, true, true), + array(false, true, true, false), + array(true, true, true, true), ); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php index beb7b2c387a64..92f0078b2d753 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php @@ -185,6 +185,27 @@ public function testToolbarIsNotInjectedOnNonHtmlRequests() $this->assertEquals('', $response->getContent()); } + public function testXDebugUrlHeader() + { + $response = new Response(); + $response->headers->set('X-Debug-Token', 'xxxxxxxx'); + + $urlGenerator = $this->getUrlGeneratorMock(); + $urlGenerator + ->expects($this->once()) + ->method('generate') + ->with('_profiler', array('token' => 'xxxxxxxx')) + ->will($this->returnValue('/_profiler/xxxxxxxx')) + ; + + $event = new FilterResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + + $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, 'bottom', $urlGenerator); + $listener->onKernelResponse($event); + + $this->assertEquals('/_profiler/xxxxxxxx', $response->headers->get('X-Debug-Token-Link')); + } + protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true) { $request = $this->getMock( @@ -219,6 +240,11 @@ protected function getTwigMock($render = 'WDT') return $templating; } + protected function getUrlGeneratorMock() + { + return $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); + } + protected function getKernelMock() { return $this->getMock('Symfony\Component\HttpKernel\Kernel', array(), array(), '', false); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/TestCase.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/TestCase.php index 586da133a3843..0c0efeb0e0416 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/TestCase.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/TestCase.php @@ -13,10 +13,4 @@ class TestCase extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Twig_Environment')) { - $this->markTestSkipped('Twig is not available.'); - } - } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index c3f20e87e7943..2911bd7f7fb07 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/BrowserKit/Client.php b/src/Symfony/Component/BrowserKit/Client.php index cf1445c35b71b..1cdab2e417a4a 100644 --- a/src/Symfony/Component/BrowserKit/Client.php +++ b/src/Symfony/Component/BrowserKit/Client.php @@ -32,19 +32,19 @@ abstract class Client { protected $history; protected $cookieJar; - protected $server; + protected $server = array(); protected $internalRequest; protected $request; protected $internalResponse; protected $response; protected $crawler; - protected $insulated; + protected $insulated = false; protected $redirect; - protected $followRedirects; + protected $followRedirects = true; - private $maxRedirects; - private $redirectCount; - private $isMainRequest; + private $maxRedirects = -1; + private $redirectCount = 0; + private $isMainRequest = true; /** * Constructor. @@ -58,13 +58,8 @@ abstract class Client public function __construct(array $server = array(), History $history = null, CookieJar $cookieJar = null) { $this->setServerParameters($server); - $this->history = null === $history ? new History() : $history; - $this->cookieJar = null === $cookieJar ? new CookieJar() : $cookieJar; - $this->insulated = false; - $this->followRedirects = true; - $this->maxRedirects = -1; - $this->redirectCount = 0; - $this->isMainRequest = true; + $this->history = $history ?: new History(); + $this->cookieJar = $cookieJar ?: new CookieJar(); } /** @@ -102,9 +97,7 @@ public function setMaxRedirects($maxRedirects) public function insulate($insulated = true) { if ($insulated && !class_exists('Symfony\\Component\\Process\\Process')) { - // @codeCoverageIgnoreStart throw new \RuntimeException('Unable to isolate requests as the Symfony Process Component is not installed.'); - // @codeCoverageIgnoreEnd } $this->insulated = (Boolean) $insulated; @@ -389,9 +382,7 @@ abstract protected function doRequest($request); */ protected function getScript($request) { - // @codeCoverageIgnoreStart throw new \LogicException('To insulate requests, you need to override the getScript() method.'); - // @codeCoverageIgnoreEnd } /** diff --git a/src/Symfony/Component/BrowserKit/History.php b/src/Symfony/Component/BrowserKit/History.php index a22847ef1395f..0c79d5b525125 100644 --- a/src/Symfony/Component/BrowserKit/History.php +++ b/src/Symfony/Component/BrowserKit/History.php @@ -21,14 +21,6 @@ class History protected $stack = array(); protected $position = -1; - /** - * Constructor. - */ - public function __construct() - { - $this->clear(); - } - /** * Clears the history. */ diff --git a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php index 8eb2549d41f79..45ac52cfca2b4 100644 --- a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php @@ -243,14 +243,6 @@ public function testRequestSecureCookies() public function testClick() { - if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { - $this->markTestSkipped('The "DomCrawler" component is not available'); - } - - if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { - $this->markTestSkipped('The "CssSelector" component is not available'); - } - $client = new TestClient(); $client->setNextResponse(new Response('foo')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); @@ -262,14 +254,6 @@ public function testClick() public function testClickForm() { - if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { - $this->markTestSkipped('The "DomCrawler" component is not available'); - } - - if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { - $this->markTestSkipped('The "CssSelector" component is not available'); - } - $client = new TestClient(); $client->setNextResponse(new Response('')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); @@ -281,14 +265,6 @@ public function testClickForm() public function testSubmit() { - if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { - $this->markTestSkipped('The "DomCrawler" component is not available'); - } - - if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { - $this->markTestSkipped('The "CssSelector" component is not available'); - } - $client = new TestClient(); $client->setNextResponse(new Response('
    ')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); @@ -300,14 +276,6 @@ public function testSubmit() public function testSubmitPreserveAuth() { - if (!class_exists('Symfony\Component\DomCrawler\Crawler')) { - $this->markTestSkipped('The "DomCrawler" component is not available'); - } - - if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { - $this->markTestSkipped('The "CssSelector" component is not available'); - } - $client = new TestClient(array('PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'bar')); $client->setNextResponse(new Response('
    ')); $crawler = $client->request('GET', 'http://www.example.com/foo/foobar'); @@ -543,10 +511,6 @@ public function testRestart() public function testInsulatedRequests() { - if (!class_exists('Symfony\Component\Process\Process')) { - $this->markTestSkipped('The "Process" component is not available'); - } - $client = new TestClient(); $client->insulate(); $client->setNextScript("new Symfony\Component\BrowserKit\Response('foobar')"); diff --git a/src/Symfony/Component/BrowserKit/composer.json b/src/Symfony/Component/BrowserKit/composer.json index 38a51e9401d4a..ed914dde46684 100644 --- a/src/Symfony/Component/BrowserKit/composer.json +++ b/src/Symfony/Component/BrowserKit/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/ClassLoader/CHANGELOG.md b/src/Symfony/Component/ClassLoader/CHANGELOG.md index 54ffb642e69dd..b770166735bf2 100644 --- a/src/Symfony/Component/ClassLoader/CHANGELOG.md +++ b/src/Symfony/Component/ClassLoader/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.4.0 +----- + + * deprecated the DebugClassLoader as it has been moved to the Debug component instead + 2.3.0 ----- diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index be1c7e2b5563a..b2be33ad88ec1 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -53,7 +53,7 @@ public static function load($classes, $cacheDir, $name, $autoReload, $adaptive = $classes = array_diff($classes, $declared); // the cache is different depending on which classes are already declared - $name = $name.'-'.substr(md5(implode('|', $classes)), 0, 5); + $name = $name.'-'.substr(hash('sha256', implode('|', $classes)), 0, 5); } $classes = array_unique($classes); diff --git a/src/Symfony/Component/ClassLoader/DebugClassLoader.php b/src/Symfony/Component/ClassLoader/DebugClassLoader.php index 842f4744c0f66..8edd4c1999fa7 100644 --- a/src/Symfony/Component/ClassLoader/DebugClassLoader.php +++ b/src/Symfony/Component/ClassLoader/DebugClassLoader.php @@ -22,6 +22,8 @@ * @author Christophe Coevoet * * @api + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. Use the DebugClassLoader provided by the Debug component instead. */ class DebugClassLoader { @@ -39,6 +41,16 @@ public function __construct($classFinder) $this->classFinder = $classFinder; } + /** + * Gets the wrapped class loader. + * + * @return object a class loader instance + */ + public function getClassLoader() + { + return $this->classFinder; + } + /** * Replaces all autoloaders implementing a findFile method by a DebugClassLoader wrapper. */ diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassLoaderTest.php index 9dae537442f54..a870935797cda 100644 --- a/src/Symfony/Component/ClassLoader/Tests/ClassLoaderTest.php +++ b/src/Symfony/Component/ClassLoader/Tests/ClassLoaderTest.php @@ -72,7 +72,7 @@ public function testLoadNonexistentClass($className, $testClassName, $message) public function getLoadNonexistentClassTests() { return array( - array('\\Pearlike3_Bar', '\\Pearlike3_Bar', '->loadClass() loads non exising Pearlike3_Bar class with a leading slash'), + array('\\Pearlike3_Bar', '\\Pearlike3_Bar', '->loadClass() loads non existing Pearlike3_Bar class with a leading slash'), ); } diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassMapGeneratorTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassMapGeneratorTest.php index 18f64f75887ef..b8600fc54ed7b 100644 --- a/src/Symfony/Component/ClassLoader/Tests/ClassMapGeneratorTest.php +++ b/src/Symfony/Component/ClassLoader/Tests/ClassMapGeneratorTest.php @@ -120,10 +120,6 @@ public function getTestCreateMapTests() public function testCreateMapFinderSupport() { - if (!class_exists('Symfony\\Component\\Finder\\Finder')) { - $this->markTestSkipped('Finder component is not available'); - } - $finder = new \Symfony\Component\Finder\Finder(); $finder->files()->in(__DIR__.'/Fixtures/beta/NamespaceCollision'); diff --git a/src/Symfony/Component/ClassLoader/composer.json b/src/Symfony/Component/ClassLoader/composer.json index 06a1c624b24b3..84ce6a0a633a7 100644 --- a/src/Symfony/Component/ClassLoader/composer.json +++ b/src/Symfony/Component/ClassLoader/composer.json @@ -28,7 +28,7 @@ "target-dir": "Symfony/Component/ClassLoader", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Config/Definition/ArrayNode.php b/src/Symfony/Component/Config/Definition/ArrayNode.php index d906fa31713ff..6017194421d69 100644 --- a/src/Symfony/Component/Config/Definition/ArrayNode.php +++ b/src/Symfony/Component/Config/Definition/ArrayNode.php @@ -22,34 +22,14 @@ */ class ArrayNode extends BaseNode implements PrototypeNodeInterface { - protected $xmlRemappings; - protected $children; - protected $allowFalse; - protected $allowNewKeys; - protected $addIfNotSet; - protected $performDeepMerging; - protected $ignoreExtraKeys; - protected $normalizeKeys; - - /** - * Constructor. - * - * @param string $name The Node's name - * @param NodeInterface $parent The node parent - */ - public function __construct($name, NodeInterface $parent = null) - { - parent::__construct($name, $parent); - - $this->children = array(); - $this->xmlRemappings = array(); - $this->removeKeyAttribute = true; - $this->allowFalse = false; - $this->addIfNotSet = false; - $this->allowNewKeys = true; - $this->performDeepMerging = true; - $this->normalizeKeys = true; - } + protected $xmlRemappings = array(); + protected $children = array(); + protected $allowFalse = false; + protected $allowNewKeys = true; + protected $addIfNotSet = false; + protected $performDeepMerging = true; + protected $ignoreExtraKeys = false; + protected $normalizeKeys = true; public function setNormalizeKeys($normalizeKeys) { @@ -105,6 +85,16 @@ public function setXmlRemappings(array $remappings) $this->xmlRemappings = $remappings; } + /** + * Gets the xml remappings that should be performed. + * + * @return array $remappings an array of the form array(array(string, string)) + */ + public function getXmlRemappings() + { + return $this->xmlRemappings; + } + /** * Sets whether to add default values for this array if it has not been * defined in any of the configuration files. diff --git a/src/Symfony/Component/Config/Definition/BaseNode.php b/src/Symfony/Component/Config/Definition/BaseNode.php index cbb8df9ca6d7c..3e4412713fc57 100644 --- a/src/Symfony/Component/Config/Definition/BaseNode.php +++ b/src/Symfony/Component/Config/Definition/BaseNode.php @@ -24,11 +24,11 @@ abstract class BaseNode implements NodeInterface { protected $name; protected $parent; - protected $normalizationClosures; - protected $finalValidationClosures; - protected $allowOverwrite; - protected $required; - protected $equivalentValues; + protected $normalizationClosures = array(); + protected $finalValidationClosures = array(); + protected $allowOverwrite = true; + protected $required = false; + protected $equivalentValues = array(); protected $attributes = array(); /** @@ -47,11 +47,6 @@ public function __construct($name, NodeInterface $parent = null) $this->name = $name; $this->parent = $parent; - $this->normalizationClosures = array(); - $this->finalValidationClosures = array(); - $this->allowOverwrite = true; - $this->required = false; - $this->equivalentValues = array(); } public function setAttribute($key, $value) @@ -280,6 +275,16 @@ protected function preNormalize($value) return $value; } + /** + * Returns parent node for this node. + * + * @return NodeInterface|null + */ + public function getParent() + { + return $this->parent; + } + /** * Finalizes a value, applying all finalization closures. * diff --git a/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php index 6027fa9caf9f9..a97fa27239fa2 100644 --- a/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php @@ -22,18 +22,18 @@ */ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinitionInterface { - protected $performDeepMerging; - protected $ignoreExtraKeys; - protected $children; + protected $performDeepMerging = true; + protected $ignoreExtraKeys = false; + protected $children = array(); protected $prototype; - protected $atLeastOne; - protected $allowNewKeys; + protected $atLeastOne = false; + protected $allowNewKeys = true; protected $key; protected $removeKeyItem; - protected $addDefaults; - protected $addDefaultChildren; + protected $addDefaults = false; + protected $addDefaultChildren = false; protected $nodeBuilder; - protected $normalizeKeys; + protected $normalizeKeys = true; /** * {@inheritDoc} @@ -42,16 +42,8 @@ public function __construct($name, NodeParentInterface $parent = null) { parent::__construct($name, $parent); - $this->children = array(); - $this->addDefaults = false; - $this->addDefaultChildren = false; - $this->allowNewKeys = true; - $this->atLeastOne = false; - $this->allowEmptyValue = true; - $this->performDeepMerging = true; $this->nullEquivalent = array(); $this->trueEquivalent = array(); - $this->normalizeKeys = true; } /** diff --git a/src/Symfony/Component/Config/Definition/Builder/MergeBuilder.php b/src/Symfony/Component/Config/Definition/Builder/MergeBuilder.php index 5e09ff5ca0905..88311433399eb 100644 --- a/src/Symfony/Component/Config/Definition/Builder/MergeBuilder.php +++ b/src/Symfony/Component/Config/Definition/Builder/MergeBuilder.php @@ -19,8 +19,8 @@ class MergeBuilder { protected $node; - public $allowFalse; - public $allowOverwrite; + public $allowFalse = false; + public $allowOverwrite = true; /** * Constructor @@ -30,8 +30,6 @@ class MergeBuilder public function __construct(NodeDefinition $node) { $this->node = $node; - $this->allowFalse = false; - $this->allowOverwrite = true; } /** diff --git a/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php index 289e1c4f4d777..a9dcb09fecd8b 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php @@ -25,15 +25,16 @@ abstract class NodeDefinition implements NodeParentInterface protected $normalization; protected $validation; protected $defaultValue; - protected $default; - protected $required; + protected $default = false; + protected $required = false; protected $merge; - protected $allowEmptyValue; + protected $allowEmptyValue = true; protected $nullEquivalent; - protected $trueEquivalent; - protected $falseEquivalent; + protected $trueEquivalent = true; + protected $falseEquivalent = false; + /** - * @var NodeParentInterface|NodeInterface + * @var NodeParentInterface|null */ protected $parent; protected $attributes = array(); @@ -41,17 +42,13 @@ abstract class NodeDefinition implements NodeParentInterface /** * Constructor * - * @param string $name The name of the node - * @param NodeParentInterface $parent The parent + * @param string $name The name of the node + * @param NodeParentInterface|null $parent The parent */ public function __construct($name, NodeParentInterface $parent = null) { $this->parent = $parent; $this->name = $name; - $this->default = false; - $this->required = false; - $this->trueEquivalent = true; - $this->falseEquivalent = false; } /** @@ -110,7 +107,7 @@ public function attribute($key, $value) /** * Returns the parent node. * - * @return NodeParentInterface The builder of the parent node + * @return NodeParentInterface|null The builder of the parent node */ public function end() { diff --git a/src/Symfony/Component/Config/Definition/Builder/NormalizationBuilder.php b/src/Symfony/Component/Config/Definition/Builder/NormalizationBuilder.php index 87f25b723a28e..4020f605d1df9 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NormalizationBuilder.php +++ b/src/Symfony/Component/Config/Definition/Builder/NormalizationBuilder.php @@ -19,8 +19,8 @@ class NormalizationBuilder { protected $node; - public $before; - public $remappings; + public $before = array(); + public $remappings = array(); /** * Constructor @@ -30,9 +30,6 @@ class NormalizationBuilder public function __construct(NodeDefinition $node) { $this->node = $node; - $this->keys = false; - $this->remappings = array(); - $this->before = array(); } /** diff --git a/src/Symfony/Component/Config/Definition/Builder/ValidationBuilder.php b/src/Symfony/Component/Config/Definition/Builder/ValidationBuilder.php index 22f54a1091b90..56d6ddc34b221 100644 --- a/src/Symfony/Component/Config/Definition/Builder/ValidationBuilder.php +++ b/src/Symfony/Component/Config/Definition/Builder/ValidationBuilder.php @@ -19,7 +19,7 @@ class ValidationBuilder { protected $node; - public $rules; + public $rules = array(); /** * Constructor @@ -29,8 +29,6 @@ class ValidationBuilder public function __construct(NodeDefinition $node) { $this->node = $node; - - $this->rules = array(); } /** diff --git a/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php index 75da3ab87bfca..9c6e249c95288 100644 --- a/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php @@ -49,10 +49,7 @@ protected function createNode() $node->setDefaultValue($this->defaultValue); } - if (false === $this->allowEmptyValue) { - $node->setAllowEmptyValue($this->allowEmptyValue); - } - + $node->setAllowEmptyValue($this->allowEmptyValue); $node->addEquivalentValue(null, $this->nullEquivalent); $node->addEquivalentValue(true, $this->trueEquivalent); $node->addEquivalentValue(false, $this->falseEquivalent); diff --git a/src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php b/src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php new file mode 100644 index 0000000000000..d9c842323521a --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php @@ -0,0 +1,300 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Dumper; + +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\PrototypedArrayNode; + +/** + * Dumps a XML reference configuration for the given configuration/node instance. + * + * @author Wouter J + */ +class XmlReferenceDumper +{ + private $reference; + + public function dump(ConfigurationInterface $configuration, $namespace = null) + { + return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree(), $namespace); + } + + public function dumpNode(NodeInterface $node, $namespace = null) + { + $this->reference = ''; + $this->writeNode($node, 0, true, $namespace); + $ref = $this->reference; + $this->reference = null; + + return $ref; + } + + /** + * @param NodeInterface $node + * @param integer $depth + * @param Boolean $root If the node is the root node + * @param string $namespace The namespace of the node + */ + private function writeNode(NodeInterface $node, $depth = 0, $root = false, $namespace = null) + { + $rootName = ($root ? 'config' : $node->getName()); + $rootNamespace = ($namespace ?: ($root ? 'http://example.org/schema/dic/'.$node->getName() : null)); + + // xml remapping + if ($node->getParent()) { + $remapping = array_filter($node->getParent()->getXmlRemappings(), function ($mapping) use ($rootName) { + return $rootName === $mapping[1]; + }); + + if (count($remapping)) { + list($singular, $plural) = current($remapping); + $rootName = $singular; + } + } + $rootName = str_replace('_', '-', $rootName); + + $rootAttributes = array(); + $rootAttributeComments = array(); + $rootChildren = array(); + $rootComments = array(); + + if ($node instanceof ArrayNode) { + $children = $node->getChildren(); + + // comments about the root node + if ($rootInfo = $node->getInfo()) { + $rootComments[] = $rootInfo; + } + + if ($rootNamespace) { + $rootComments[] = 'Namespace: '.$rootNamespace; + } + + // render prototyped nodes + if ($node instanceof PrototypedArrayNode) { + array_unshift($rootComments, 'prototype'); + + if ($key = $node->getKeyAttribute()) { + $rootAttributes[$key] = str_replace('-', ' ', $rootName).' '.$key; + } + + $prototype = $node->getPrototype(); + + if ($prototype instanceof ArrayNode) { + $children = $prototype->getChildren(); + } else { + if ($prototype->hasDefaultValue()) { + $prototypeValue = $prototype->getDefaultValue(); + } else { + switch (get_class($prototype)) { + case 'Symfony\Component\Config\Definition\ScalarNode': + $prototypeValue = 'scalar value'; + break; + + case 'Symfony\Component\Config\Definition\FloatNode': + case 'Symfony\Component\Config\Definition\IntegerNode': + $prototypeValue = 'numeric value'; + break; + + case 'Symfony\Component\Config\Definition\BooleanNode': + $prototypeValue = 'true|false'; + break; + + case 'Symfony\Component\Config\Definition\EnumNode': + $prototypeValue = implode('|', array_map('json_encode', $prototype->getValues())); + break; + + default: + $prototypeValue = 'value'; + } + } + } + } + + // get attributes and elements + foreach ($children as $child) { + if (!$child instanceof ArrayNode) { + // get attributes + + // metadata + $name = str_replace('_', '-', $child->getName()); + $value = '%%%%not_defined%%%%'; // use a string which isn't used in the normal world + + // comments + $comments = array(); + if ($info = $child->getInfo()) { + $comments[] = $info; + } + + if ($example = $child->getExample()) { + $comments[] = 'Example: '.$example; + } + + if ($child->isRequired()) { + $comments[] = 'Required'; + } + + if ($child instanceof EnumNode) { + $comments[] = 'One of '.implode('; ', array_map('json_encode', $child->getValues())); + } + + if (count($comments)) { + $rootAttributeComments[$name] = implode(";\n", $comments); + } + + // default values + if ($child->hasDefaultValue()) { + $value = $child->getDefaultValue(); + } + + // append attribute + $rootAttributes[$name] = $value; + } else { + // get elements + $rootChildren[] = $child; + } + } + } + + // render comments + + // root node comment + if (count($rootComments)) { + foreach ($rootComments as $comment) { + $this->writeLine('', $depth); + } + } + + // attribute comments + if (count($rootAttributeComments)) { + foreach ($rootAttributeComments as $attrName => $comment) { + $commentDepth = $depth + 4 + strlen($attrName) + 2; + $commentLines = explode("\n", $comment); + $multiline = (count($commentLines) > 1); + $comment = implode(PHP_EOL.str_repeat(' ', $commentDepth), $commentLines); + + if ($multiline) { + $this->writeLine('', $depth); + } else { + $this->writeLine('', $depth); + } + } + } + + // render start tag + attributes + $rootIsVariablePrototype = isset($prototypeValue); + $rootIsEmptyTag = (0 === count($rootChildren) && !$rootIsVariablePrototype); + $rootOpenTag = '<'.$rootName; + if (1 >= ($attributesCount = count($rootAttributes))) { + if (1 === $attributesCount) { + $rootOpenTag .= sprintf(' %s="%s"', current(array_keys($rootAttributes)), $this->writeValue(current($rootAttributes))); + } + + $rootOpenTag .= $rootIsEmptyTag ? ' />' : '>'; + + if ($rootIsVariablePrototype) { + $rootOpenTag .= $prototypeValue.''; + } + + $this->writeLine($rootOpenTag, $depth); + } else { + $this->writeLine($rootOpenTag, $depth); + + $i = 1; + + foreach ($rootAttributes as $attrName => $attrValue) { + $attr = sprintf('%s="%s"', $attrName, $this->writeValue($attrValue)); + + $this->writeLine($attr, $depth + 4); + + if ($attributesCount === $i++) { + $this->writeLine($rootIsEmptyTag ? '/>' : '>', $depth); + + if ($rootIsVariablePrototype) { + $rootOpenTag .= $prototypeValue.''; + } + } + } + } + + // render children tags + foreach ($rootChildren as $child) { + $this->writeLine(''); + $this->writeNode($child, $depth + 4); + } + + // render end tag + if (!$rootIsEmptyTag && !$rootIsVariablePrototype) { + $this->writeLine(''); + + $rootEndTag = ''; + $this->writeLine($rootEndTag, $depth); + } + } + + /** + * Outputs a single config reference line + * + * @param string $text + * @param int $indent + */ + private function writeLine($text, $indent = 0) + { + $indent = strlen($text) + $indent; + $format = '%'.$indent.'s'; + + $this->reference .= sprintf($format, $text)."\n"; + } + + /** + * Renders the string conversion of the value. + * + * @param mixed $value + * + * @return string + */ + private function writeValue($value) + { + if ('%%%%not_defined%%%%' === $value) { + return ''; + } + + if (is_string($value) || is_numeric($value)) { + return $value; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + if (null === $value) { + return 'null'; + } + + if (empty($value)) { + return ''; + } + + if (is_array($value)) { + return implode(',', $value); + } + } +} diff --git a/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php b/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php new file mode 100644 index 0000000000000..7b0d161c44474 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php @@ -0,0 +1,199 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Dumper; + +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\ScalarNode; +use Symfony\Component\Config\Definition\EnumNode; +use Symfony\Component\Config\Definition\PrototypedArrayNode; +use Symfony\Component\Yaml\Inline; + +/** + * Dumps a Yaml reference configuration for the given configuration/node instance. + * + * @author Kevin Bond + */ +class YamlReferenceDumper +{ + private $reference; + + public function dump(ConfigurationInterface $configuration) + { + return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree()); + } + + public function dumpNode(NodeInterface $node) + { + $this->reference = ''; + $this->writeNode($node); + $ref = $this->reference; + $this->reference = null; + + return $ref; + } + + /** + * @param NodeInterface $node + * @param integer $depth + */ + private function writeNode(NodeInterface $node, $depth = 0) + { + $comments = array(); + $default = ''; + $defaultArray = null; + $children = null; + $example = $node->getExample(); + + // defaults + if ($node instanceof ArrayNode) { + $children = $node->getChildren(); + + if ($node instanceof PrototypedArrayNode) { + $prototype = $node->getPrototype(); + + if ($prototype instanceof ArrayNode) { + $children = $prototype->getChildren(); + } + + // check for attribute as key + if ($key = $node->getKeyAttribute()) { + $keyNodeClass = 'Symfony\Component\Config\Definition\\'.($prototype instanceof ArrayNode ? 'ArrayNode' : 'ScalarNode'); + $keyNode = new $keyNodeClass($key, $node); + $keyNode->setInfo('Prototype'); + + // add children + foreach ($children as $childNode) { + $keyNode->addChild($childNode); + } + $children = array($key => $keyNode); + } + } + + if (!$children) { + if ($node->hasDefaultValue() && count($defaultArray = $node->getDefaultValue())) { + $default = ''; + } elseif (!is_array($example)) { + $default = '[]'; + } + } + } elseif ($node instanceof EnumNode) { + $comments[] = 'One of '.implode('; ', array_map('json_encode', $node->getValues())); + $default = '~'; + } else { + $default = '~'; + + if ($node->hasDefaultValue()) { + $default = $node->getDefaultValue(); + + if (is_array($default)) { + if (count($defaultArray = $node->getDefaultValue())) { + $default = ''; + } elseif (!is_array($example)) { + $default = '[]'; + } + } else { + $default = Inline::dump($default); + } + } + } + + // required? + if ($node->isRequired()) { + $comments[] = 'Required'; + } + + // example + if ($example && !is_array($example)) { + $comments[] = 'Example: '.$example; + } + + $default = (string) $default != '' ? ' '.$default : ''; + $comments = count($comments) ? '# '.implode(', ', $comments) : ''; + + $text = rtrim(sprintf('%-20s %s %s', $node->getName().':', $default, $comments), ' '); + + if ($info = $node->getInfo()) { + $this->writeLine(''); + // indenting multi-line info + $info = str_replace("\n", sprintf("\n%".$depth * 4 . "s# ", ' '), $info); + $this->writeLine('# '.$info, $depth * 4); + } + + $this->writeLine($text, $depth * 4); + + // output defaults + if ($defaultArray) { + $this->writeLine(''); + + $message = count($defaultArray) > 1 ? 'Defaults' : 'Default'; + + $this->writeLine('# '.$message.':', $depth * 4 + 4); + + $this->writeArray($defaultArray, $depth + 1); + } + + if (is_array($example)) { + $this->writeLine(''); + + $message = count($example) > 1 ? 'Examples' : 'Example'; + + $this->writeLine('# '.$message.':', $depth * 4 + 4); + + $this->writeArray($example, $depth + 1); + } + + if ($children) { + foreach ($children as $childNode) { + $this->writeNode($childNode, $depth + 1); + } + } + } + + /** + * Outputs a single config reference line + * + * @param string $text + * @param int $indent + */ + private function writeLine($text, $indent = 0) + { + $indent = strlen($text) + $indent; + $format = '%'.$indent.'s'; + + $this->reference .= sprintf($format, $text)."\n"; + } + + private function writeArray(array $array, $depth) + { + $isIndexed = array_values($array) === $array; + + foreach ($array as $key => $value) { + if (is_array($value)) { + $val = ''; + } else { + $val = $value; + } + + if ($isIndexed) { + $this->writeLine('- '.$val, $depth * 4); + } else { + $this->writeLine(sprintf('%-20s %s', $key.':', $val), $depth * 4); + } + + if (is_array($value)) { + $this->writeArray($value, $depth + 1); + } + } + } +} diff --git a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php index 2e76156574efb..7074cb362e87e 100644 --- a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php +++ b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php @@ -25,25 +25,11 @@ class PrototypedArrayNode extends ArrayNode { protected $prototype; protected $keyAttribute; - protected $removeKeyAttribute; - protected $minNumberOfElements; - protected $defaultValue; + protected $removeKeyAttribute = false; + protected $minNumberOfElements = 0; + protected $defaultValue = array(); protected $defaultChildren; - /** - * Constructor. - * - * @param string $name The Node's name - * @param NodeInterface $parent The node parent - */ - public function __construct($name, NodeInterface $parent = null) - { - parent::__construct($name, $parent); - - $this->minNumberOfElements = 0; - $this->defaultValue = array(); - } - /** * Sets the minimum number of elements that a prototype based node must * contain. By default this is zero, meaning no elements. diff --git a/src/Symfony/Component/Config/Definition/ReferenceDumper.php b/src/Symfony/Component/Config/Definition/ReferenceDumper.php index 54309021b7f11..7fe336d8fbf11 100644 --- a/src/Symfony/Component/Config/Definition/ReferenceDumper.php +++ b/src/Symfony/Component/Config/Definition/ReferenceDumper.php @@ -11,183 +11,11 @@ namespace Symfony\Component\Config\Definition; +use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; + /** - * Dumps a reference configuration for the given configuration/node instance. - * - * Currently, only YML format is supported. - * - * @author Kevin Bond + * @deprecated Deprecated since version 2.4, to be removed in 3.0. Use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper instead. */ -class ReferenceDumper +class ReferenceDumper extends YamlReferenceDumper { - private $reference; - - public function dump(ConfigurationInterface $configuration) - { - return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree()); - } - - public function dumpNode(NodeInterface $node) - { - $this->reference = ''; - $this->writeNode($node); - $ref = $this->reference; - $this->reference = null; - - return $ref; - } - - /** - * @param NodeInterface $node - * @param integer $depth - */ - private function writeNode(NodeInterface $node, $depth = 0) - { - $comments = array(); - $default = ''; - $defaultArray = null; - $children = null; - $example = $node->getExample(); - - // defaults - if ($node instanceof ArrayNode) { - $children = $node->getChildren(); - - if ($node instanceof PrototypedArrayNode) { - $prototype = $node->getPrototype(); - - if ($prototype instanceof ArrayNode) { - $children = $prototype->getChildren(); - } - - // check for attribute as key - if ($key = $node->getKeyAttribute()) { - $keyNode = new ArrayNode($key, $node); - $keyNode->setInfo('Prototype'); - - // add children - foreach ($children as $childNode) { - $keyNode->addChild($childNode); - } - $children = array($key => $keyNode); - } - } - - if (!$children) { - if ($node->hasDefaultValue() && count($defaultArray = $node->getDefaultValue())) { - $default = ''; - } elseif (!is_array($example)) { - $default = '[]'; - } - } - } else { - $default = '~'; - - if ($node->hasDefaultValue()) { - $default = $node->getDefaultValue(); - - if (true === $default) { - $default = 'true'; - } elseif (false === $default) { - $default = 'false'; - } elseif (null === $default) { - $default = '~'; - } elseif (is_array($default)) { - if ($node->hasDefaultValue() && count($defaultArray = $node->getDefaultValue())) { - $default = ''; - } elseif (!is_array($example)) { - $default = '[]'; - } - } - } - } - - // required? - if ($node->isRequired()) { - $comments[] = 'Required'; - } - - // example - if ($example && !is_array($example)) { - $comments[] = 'Example: '.$example; - } - - $default = (string) $default != '' ? ' '.$default : ''; - $comments = count($comments) ? '# '.implode(', ', $comments) : ''; - - $text = rtrim(sprintf('%-20s %s %s', $node->getName().':', $default, $comments), ' '); - - if ($info = $node->getInfo()) { - $this->writeLine(''); - // indenting multi-line info - $info = str_replace("\n", sprintf("\n%".$depth * 4 . "s# ", ' '), $info); - $this->writeLine('# '.$info, $depth * 4); - } - - $this->writeLine($text, $depth * 4); - - // output defaults - if ($defaultArray) { - $this->writeLine(''); - - $message = count($defaultArray) > 1 ? 'Defaults' : 'Default'; - - $this->writeLine('# '.$message.':', $depth * 4 + 4); - - $this->writeArray($defaultArray, $depth + 1); - } - - if (is_array($example)) { - $this->writeLine(''); - - $message = count($example) > 1 ? 'Examples' : 'Example'; - - $this->writeLine('# '.$message.':', $depth * 4 + 4); - - $this->writeArray($example, $depth + 1); - } - - if ($children) { - foreach ($children as $childNode) { - $this->writeNode($childNode, $depth + 1); - } - } - } - - /** - * Outputs a single config reference line - * - * @param string $text - * @param int $indent - */ - private function writeLine($text, $indent = 0) - { - $indent = strlen($text) + $indent; - $format = '%'.$indent.'s'; - - $this->reference .= sprintf($format, $text)."\n"; - } - - private function writeArray(array $array, $depth) - { - $isIndexed = array_values($array) === $array; - - foreach ($array as $key => $value) { - if (is_array($value)) { - $val = ''; - } else { - $val = $value; - } - - if ($isIndexed) { - $this->writeLine('- '.$val, $depth * 4); - } else { - $this->writeLine(sprintf('%-20s %s', $key.':', $val), $depth * 4); - } - - if (is_array($value)) { - $this->writeArray($value, $depth + 1); - } - } - } } diff --git a/src/Symfony/Component/Config/Loader/LoaderResolver.php b/src/Symfony/Component/Config/Loader/LoaderResolver.php index 2340fe07f8869..9e1ebd4f2a908 100644 --- a/src/Symfony/Component/Config/Loader/LoaderResolver.php +++ b/src/Symfony/Component/Config/Loader/LoaderResolver.php @@ -24,7 +24,7 @@ class LoaderResolver implements LoaderResolverInterface /** * @var LoaderInterface[] An array of LoaderInterface objects */ - private $loaders; + private $loaders = array(); /** * Constructor. @@ -33,7 +33,6 @@ class LoaderResolver implements LoaderResolverInterface */ public function __construct(array $loaders = array()) { - $this->loaders = array(); foreach ($loaders as $loader) { $this->addLoader($loader); } diff --git a/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php new file mode 100644 index 0000000000000..0f650279e8730 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Definition\Dumper; + +use Symfony\Component\Config\Definition\Dumper\XmlReferenceDumper; +use Symfony\Component\Config\Tests\Fixtures\Configuration\ExampleConfiguration; + +class XmlReferenceDumperTest extends \PHPUnit_Framework_TestCase +{ + public function testDumper() + { + $configuration = new ExampleConfiguration(); + + $dumper = new XmlReferenceDumper(); + $this->assertEquals($this->getConfigurationAsString(), $dumper->dump($configuration)); + } + + public function testNamespaceDumper() + { + $configuration = new ExampleConfiguration(); + + $dumper = new XmlReferenceDumper(); + $this->assertEquals(str_replace('http://example.org/schema/dic/acme_root', 'http://symfony.com/schema/dic/symfony', $this->getConfigurationAsString()), $dumper->dump($configuration, 'http://symfony.com/schema/dic/symfony')); + } + + private function getConfigurationAsString() + { + return << + + + + + + + + + + scalar value + + + + + + +EOL; + } +} diff --git a/src/Symfony/Component/Config/Tests/Definition/ReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php similarity index 66% rename from src/Symfony/Component/Config/Tests/Definition/ReferenceDumperTest.php rename to src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php index 137caf8cbc37b..6ac7535fa4800 100644 --- a/src/Symfony/Component/Config/Tests/Definition/ReferenceDumperTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php @@ -9,25 +9,27 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Config\Tests\Definition; +namespace Symfony\Component\Config\Tests\Definition\Dumper; -use Symfony\Component\Config\Definition\ReferenceDumper; +use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; use Symfony\Component\Config\Tests\Fixtures\Configuration\ExampleConfiguration; -class ReferenceDumperTest extends \PHPUnit_Framework_TestCase +class YamlReferenceDumperTest extends \PHPUnit_Framework_TestCase { public function testDumper() { $configuration = new ExampleConfiguration(); - $dumper = new ReferenceDumper(); + $dumper = new YamlReferenceDumper(); + + $this->markTestIncomplete('The Yaml Dumper currently does not support prototyped arrays'); $this->assertEquals($this->getConfigurationAsString(), $dumper->dump($configuration)); } private function getConfigurationAsString() { return <<root('root'); + $rootNode = $treeBuilder->root('acme_root'); $rootNode + ->fixXmlConfig('parameter') + ->fixXmlConfig('connection') ->children() ->booleanNode('boolean')->defaultTrue()->end() ->scalarNode('scalar_empty')->end() @@ -31,6 +33,8 @@ public function getConfigTreeBuilder() ->scalarNode('scalar_default')->defaultValue('default')->end() ->scalarNode('scalar_array_empty')->defaultValue(array())->end() ->scalarNode('scalar_array_defaults')->defaultValue(array('elem1', 'elem2'))->end() + ->scalarNode('scalar_required')->isRequired()->end() + ->enumNode('enum')->values(array('this', 'that'))->end() ->arrayNode('array') ->info('some info') ->canBeUnset() @@ -47,15 +51,15 @@ public function getConfigTreeBuilder() ->end() ->end() ->end() - ->arrayNode('array_prototype') - ->children() - ->arrayNode('parameters') - ->useAttributeAsKey('name') - ->prototype('array') - ->children() - ->scalarNode('value')->isRequired()->end() - ->end() - ->end() + ->arrayNode('parameters') + ->useAttributeAsKey('name') + ->prototype('scalar')->end() + ->end() + ->arrayNode('connections') + ->prototype('array') + ->children() + ->scalarNode('user')->end() + ->scalarNode('pass')->end() ->end() ->end() ->end() diff --git a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php index feb796b68f379..a17119260a3e8 100644 --- a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php +++ b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php @@ -104,6 +104,7 @@ public function testPhpize($expected, $value) public function getDataForPhpize() { return array( + array('', ''), array(null, 'null'), array(true, 'true'), array(false, 'false'), @@ -112,6 +113,7 @@ public function getDataForPhpize() array(false, 'False'), array(0, '0'), array(1, '1'), + array(-1, '-1'), array(0777, '0777'), array(255, '0xFF'), array(100.0, '1e2'), @@ -127,6 +129,7 @@ public function getDataForPhpize() array('111,222,333,444', '111,222,333,444'), array('1111,2222,3333,4444,5555', '1111,2222,3333,4444,5555'), array('foo', 'foo'), + array(6, '0b0110'), ); } } diff --git a/src/Symfony/Component/Config/Util/XmlUtils.php b/src/Symfony/Component/Config/Util/XmlUtils.php index f2ea98beed5e3..de3e230c4e23c 100644 --- a/src/Symfony/Component/Config/Util/XmlUtils.php +++ b/src/Symfony/Component/Config/Util/XmlUtils.php @@ -31,8 +31,8 @@ private function __construct() /** * Loads an XML file. * - * @param string $file An XML file path - * @param string|callable $schemaOrCallable An XSD schema file path or callable + * @param string $file An XML file path + * @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation * * @return \DOMDocument * @@ -187,10 +187,17 @@ public static function phpize($value) $cast = intval($value); return '0' == $value[0] ? octdec($value) : (((string) $raw == (string) $cast) ? $cast : $raw); + case isset($value[1]) && '-' === $value[0] && ctype_digit(substr($value, 1)): + $raw = $value; + $cast = intval($value); + + return '0' == $value[1] ? octdec($value) : (((string) $raw == (string) $cast) ? $cast : $raw); case 'true' === $lowercaseValue: return true; case 'false' === $lowercaseValue: return false; + case isset($value[1]) && '0b' == $value[0].$value[1]: + return bindec($value); case is_numeric($value): return '0x' == $value[0].$value[1] ? hexdec($value) : floatval($value); case preg_match('/^(-|\+)?[0-9]+(\.[0-9]+)?$/', $value): diff --git a/src/Symfony/Component/Config/composer.json b/src/Symfony/Component/Config/composer.json index 6e8967cf65b7a..dce426b77c0e3 100644 --- a/src/Symfony/Component/Config/composer.json +++ b/src/Symfony/Component/Config/composer.json @@ -26,7 +26,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 20ed3197cd2a4..44b8a9393b7be 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -19,6 +19,8 @@ use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputAwareInterface; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -54,16 +56,18 @@ */ class Application { - private $commands; + private $commands = array(); private $wantHelps = false; private $runningCommand; private $name; private $version; - private $catchExceptions; - private $autoExit; + private $catchExceptions = true; + private $autoExit = true; private $definition; private $helperSet; private $dispatcher; + private $terminalDimensions; + private $defaultCommand; /** * Constructor. @@ -77,9 +81,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { $this->name = $name; $this->version = $version; - $this->catchExceptions = true; - $this->autoExit = true; - $this->commands = array(); + $this->defaultCommand = 'list'; $this->helperSet = $this->getDefaultHelperSet(); $this->definition = $this->getDefaultInputDefinition(); @@ -145,9 +147,8 @@ public function run(InputInterface $input = null, OutputInterface $output = null if ($exitCode > 255) { $exitCode = 255; } - // @codeCoverageIgnoreStart + exit($exitCode); - // @codeCoverageIgnoreEnd } return $exitCode; @@ -180,8 +181,8 @@ public function doRun(InputInterface $input, OutputInterface $output) } if (!$name) { - $name = 'list'; - $input = new ArrayInput(array('command' => 'list')); + $name = $this->defaultCommand; + $input = new ArrayInput(array('command' => $this->defaultCommand)); } // the command name MUST be the first element of the input @@ -404,6 +405,10 @@ public function add(Command $command) return; } + if (null === $command->getDefinition()) { + throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); + } + $this->commands[$command->getName()] = $command; foreach ($command->getAliases() as $alias) { @@ -491,51 +496,31 @@ public function getNamespaces() public function findNamespace($namespace) { $allNamespaces = $this->getNamespaces(); - $found = ''; - foreach (explode(':', $namespace) as $i => $part) { - // select sub-namespaces matching the current namespace we found - $namespaces = array(); - foreach ($allNamespaces as $n) { - if ('' === $found || 0 === strpos($n, $found)) { - $namespaces[$n] = explode(':', $n); - } - } - - $abbrevs = static::getAbbreviations(array_unique(array_values(array_filter(array_map(function ($p) use ($i) { return isset($p[$i]) ? $p[$i] : ''; }, $namespaces))))); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace); + $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); - if (!isset($abbrevs[$part])) { - $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); - if (1 <= $i) { - $part = $found.':'.$part; - } - - if ($alternatives = $this->findAlternativeNamespace($part, $abbrevs)) { - if (1 == count($alternatives)) { - $message .= "\n\nDid you mean this?\n "; - } else { - $message .= "\n\nDid you mean one of these?\n "; - } - - $message .= implode("\n ", $alternatives); + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces, array())) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; } - throw new \InvalidArgumentException($message); - } - - // there are multiple matches, but $part is an exact match of one of them so we select it - if (in_array($part, $abbrevs[$part])) { - $abbrevs[$part] = array($part); + $message .= implode("\n ", $alternatives); } - if (count($abbrevs[$part]) > 1) { - throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($abbrevs[$part]))); - } + throw new \InvalidArgumentException($message); + } - $found .= $found ? ':' . $abbrevs[$part][0] : $abbrevs[$part][0]; + $exact = in_array($namespace, $namespaces, true); + if (count($namespaces) > 1 && !$exact) { + throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces)))); } - return $found; + return $exact ? $namespace : reset($namespaces); } /** @@ -554,58 +539,19 @@ public function findNamespace($namespace) */ public function find($name) { - // namespace - $namespace = ''; - $searchName = $name; - if (false !== $pos = strrpos($name, ':')) { - $namespace = $this->findNamespace(substr($name, 0, $pos)); - $searchName = $namespace.substr($name, $pos); - } - - // name - $commands = array(); - foreach ($this->commands as $command) { - $extractedNamespace = $this->extractNamespace($command->getName()); - if ($extractedNamespace === $namespace - || !empty($namespace) && 0 === strpos($extractedNamespace, $namespace) - ) { - $commands[] = $command->getName(); + $allCommands = array_keys($this->commands); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name); + $commands = preg_grep('{^'.$expr.'}', $allCommands); + + if (empty($commands) || count(preg_grep('{^'.$expr.'$}', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + // check if a namespace exists and contains commands + $this->findNamespace(substr($name, 0, $pos)); } - } - $abbrevs = static::getAbbreviations(array_unique($commands)); - if (isset($abbrevs[$searchName]) && 1 == count($abbrevs[$searchName])) { - return $this->get($abbrevs[$searchName][0]); - } - - if (isset($abbrevs[$searchName]) && in_array($searchName, $abbrevs[$searchName])) { - return $this->get($searchName); - } - - if (isset($abbrevs[$searchName]) && count($abbrevs[$searchName]) > 1) { - $suggestions = $this->getAbbreviationSuggestions($abbrevs[$searchName]); - - throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); - } - - // aliases - $aliases = array(); - foreach ($this->commands as $command) { - foreach ($command->getAliases() as $alias) { - $extractedNamespace = $this->extractNamespace($alias); - if ($extractedNamespace === $namespace - || !empty($namespace) && 0 === strpos($extractedNamespace, $namespace) - ) { - $aliases[] = $alias; - } - } - } - - $aliases = static::getAbbreviations(array_unique($aliases)); - if (!isset($aliases[$searchName])) { $message = sprintf('Command "%s" is not defined.', $name); - if ($alternatives = $this->findAlternativeCommands($searchName, $abbrevs)) { + if ($alternatives = $this->findAlternatives($name, $allCommands, array())) { if (1 == count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { @@ -617,11 +563,24 @@ public function find($name) throw new \InvalidArgumentException($message); } - if (count($aliases[$searchName]) > 1) { - throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $this->getAbbreviationSuggestions($aliases[$searchName]))); + // filter out aliases for commands which are already on the list + if (count($commands) > 1) { + $commandList = $this->commands; + $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { + $commandName = $commandList[$nameOrAlias]->getName(); + + return $commandName === $nameOrAlias || !in_array($commandName, $commands); + }); } - return $this->get($aliases[$searchName][0]); + $exact = in_array($name, $commands, true); + if (count($commands) > 1 && !$exact) { + $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); + + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + } + + return $this->get($exact ? $name : reset($commands)); } /** @@ -684,8 +643,10 @@ public static function getAbbreviations($names) public function asText($namespace = null, $raw = false) { $descriptor = new TextDescriptor(); + $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, !$raw); + $descriptor->describe($output, $this, array('namespace' => $namespace, 'raw_output' => true)); - return $descriptor->describe($this, array('namespace' => $namespace, 'raw_text' => $raw)); + return $output->fetch(); } /** @@ -702,7 +663,14 @@ public function asXml($namespace = null, $asDom = false) { $descriptor = new XmlDescriptor(); - return $descriptor->describe($this, array('namespace' => $namespace, 'as_dom' => $asDom)); + if ($asDom) { + return $descriptor->getApplicationDocument($this, $namespace); + } + + $output = new BufferedOutput(); + $descriptor->describe($output, $this, array('namespace' => $namespace)); + + return $output->fetch(); } /** @@ -819,6 +787,10 @@ protected function getTerminalHeight() */ public function getTerminalDimensions() { + if ($this->terminalDimensions) { + return $this->terminalDimensions; + } + if (defined('PHP_WINDOWS_VERSION_BUILD')) { // extract [w, H] from "wxh (WxH)" if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { @@ -844,6 +816,23 @@ public function getTerminalDimensions() return array(null, null); } + /** + * Sets terminal dimensions. + * + * Can be useful to force terminal dimensions for functional tests. + * + * @param integer $width The width + * @param integer $height The height + * + * @return Application The current application + */ + public function setTerminalDimensions($width, $height) + { + $this->terminalDimensions = array($width, $height); + + return $this; + } + /** * Configures the input and output instances based on the user arguments and options. * @@ -894,6 +883,12 @@ protected function configureIO(InputInterface $input, OutputInterface $output) */ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) { + foreach ($command->getHelperSet() as $helper) { + if ($helper instanceof InputAwareInterface) { + $helper->setInput($input); + } + } + if (null === $this->dispatcher) { return $command->run($input, $output); } @@ -1054,75 +1049,64 @@ public function extractNamespace($name, $limit = null) return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); } - /** - * Finds alternative commands of $name - * - * @param string $name The full name of the command - * @param array $abbrevs The abbreviations - * - * @return array A sorted array of similar commands - */ - private function findAlternativeCommands($name, $abbrevs) - { - $callback = function ($item) { - return $item->getName(); - }; - - return $this->findAlternatives($name, $this->commands, $abbrevs, $callback); - } - - /** - * Finds alternative namespace of $name - * - * @param string $name The full name of the namespace - * @param array $abbrevs The abbreviations - * - * @return array A sorted array of similar namespace - */ - private function findAlternativeNamespace($name, $abbrevs) - { - return $this->findAlternatives($name, $this->getNamespaces(), $abbrevs); - } - /** * Finds alternative of $name among $collection, * if nothing is found in $collection, try in $abbrevs * * @param string $name The string * @param array|\Traversable $collection The collection - * @param array $abbrevs The abbreviations - * @param Closure|string|array $callback The callable to transform collection item before comparison * * @return array A sorted array of similar string */ - private function findAlternatives($name, $collection, $abbrevs, $callback = null) + private function findAlternatives($name, $collection) { + $threshold = 1e3; $alternatives = array(); + $collectionParts = array(); foreach ($collection as $item) { - if (null !== $callback) { - $item = call_user_func($callback, $item); - } + $collectionParts[$item] = explode(':', $item); + } - $lev = levenshtein($name, $item); - if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { - $alternatives[$item] = $lev; + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= strlen($subname) / 3 || false !== strpos($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } } } - if (!$alternatives) { - foreach ($abbrevs as $key => $values) { - $lev = levenshtein($name, $key); - if ($lev <= strlen($name) / 3 || false !== strpos($key, $name)) { - foreach ($values as $value) { - $alternatives[$value] = $lev; - } - } + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; } } + $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2*$threshold; }); asort($alternatives); return array_keys($alternatives); } + + /** + * Sets the default Command name. + * + * @param string $commandName The Command name + */ + public function setDefaultCommand($commandName) + { + $this->defaultCommand = $commandName; + } } diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index e4213af338cdf..19d03ca3039ea 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +2.5.0 +----- + +* added a way to set a default command instead of `ListCommand` +* added a way to set the process name of a command + +2.4.0 +----- + + * added a way to force terminal dimensions + * added a convenient method to detect verbosity level + * [BC BREAK] made descriptors use output instead of returning a string + 2.3.0 ----- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index af1b91f2b1ab2..679a1c01b99db 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\HelperSet; @@ -32,13 +33,14 @@ class Command { private $application; private $name; - private $aliases; + private $processName; + private $aliases = array(); private $definition; private $help; private $description; - private $ignoreValidationErrors; - private $applicationDefinitionMerged; - private $applicationDefinitionMergedWithArgs; + private $ignoreValidationErrors = false; + private $applicationDefinitionMerged = false; + private $applicationDefinitionMergedWithArgs = false; private $code; private $synopsis; private $helperSet; @@ -46,7 +48,7 @@ class Command /** * Constructor. * - * @param string $name The name of the command + * @param string|null $name The name of the command; passing null means it must be set in configure() * * @throws \LogicException When the command name is empty * @@ -55,10 +57,6 @@ class Command public function __construct($name = null) { $this->definition = new InputDefinition(); - $this->ignoreValidationErrors = false; - $this->applicationDefinitionMerged = false; - $this->applicationDefinitionMergedWithArgs = false; - $this->aliases = array(); if (null !== $name) { $this->setName($name); @@ -215,6 +213,16 @@ protected function initialize(InputInterface $input, OutputInterface $output) */ public function run(InputInterface $input, OutputInterface $output) { + if (null !== $this->processName) { + if (function_exists('cli_set_process_title')) { + cli_set_process_title($this->processName); + } elseif (function_exists('setproctitle')) { + setproctitle($this->processName); + } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) { + $output->writeln('Install the proctitle PECL to be able to change the process title.'); + } + } + // force the creation of the synopsis before the merge with the app definition $this->getSynopsis(); @@ -401,7 +409,7 @@ public function addOption($name, $shortcut = null, $mode = null, $description = * * @return Command The current instance * - * @throws \InvalidArgumentException When command name given is empty + * @throws \InvalidArgumentException When the name is invalid * * @api */ @@ -414,6 +422,25 @@ public function setName($name) return $this; } + /** + * Sets the process name of the command. + * + * This feature should be used only when creating a long process command, + * like a daemon. + * + * PHP 5.5+ or the proctitle PECL library is required + * + * @param string $name The process name + * + * @return Command The current instance + */ + public function setProcessName($name) + { + $this->processName = $name; + + return $this; + } + /** * Returns the command name. * @@ -511,6 +538,8 @@ public function getProcessedHelp() * * @return Command The current instance * + * @throws \InvalidArgumentException When an alias is invalid + * * @api */ public function setAliases($aliases) @@ -576,8 +605,10 @@ public function getHelper($name) public function asText() { $descriptor = new TextDescriptor(); + $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); + $descriptor->describe($output, $this, array('raw_output' => true)); - return $descriptor->describe($this); + return $output->fetch(); } /** @@ -593,12 +624,28 @@ public function asXml($asDom = false) { $descriptor = new XmlDescriptor(); - return $descriptor->describe($this, array('as_dom' => $asDom)); + if ($asDom) { + return $descriptor->getCommandDocument($this); + } + + $output = new BufferedOutput(); + $descriptor->describe($output, $this); + + return $output->fetch(); } + /** + * Validates a command name. + * + * It must be non-empty and parts can optionally be separated by ":". + * + * @param string $name + * + * @throws \InvalidArgumentException When the name is invalid + */ private function validateName($name) { - if (!preg_match('/^[^\:]+(\:[^\:]+)*$/', $name)) { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); } } diff --git a/src/Symfony/Component/Console/Command/HelpCommand.php b/src/Symfony/Component/Console/Command/HelpCommand.php index 2aa933ea0f7a7..e76044a3a7e3c 100644 --- a/src/Symfony/Component/Console/Command/HelpCommand.php +++ b/src/Symfony/Component/Console/Command/HelpCommand.php @@ -38,7 +38,7 @@ protected function configure() ->setDefinition(array( new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output help in other formats'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output help in other formats', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), )) ->setDescription('Displays help for a command') @@ -81,7 +81,11 @@ protected function execute(InputInterface $input, OutputInterface $output) } $helper = new DescriptorHelper(); - $helper->describe($output, $this->command, $input->getOption('format'), $input->getOption('raw')); + $helper->describe($output, $this->command, array( + 'format' => $input->getOption('format'), + 'raw' => $input->getOption('raw'), + )); + $this->command = null; } } diff --git a/src/Symfony/Component/Console/Command/ListCommand.php b/src/Symfony/Component/Console/Command/ListCommand.php index f3d7438d09817..25c21205e4c7e 100644 --- a/src/Symfony/Component/Console/Command/ListCommand.php +++ b/src/Symfony/Component/Console/Command/ListCommand.php @@ -73,7 +73,11 @@ protected function execute(InputInterface $input, OutputInterface $output) } $helper = new DescriptorHelper(); - $helper->describe($output, $this->getApplication(), $input->getOption('format'), $input->getOption('raw'), $input->getArgument('namespace')); + $helper->describe($output, $this->getApplication(), array( + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + )); } /** @@ -85,7 +89,7 @@ private function createDefinition() new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), new InputOption('xml', null, InputOption::VALUE_NONE, 'To output list as XML'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output list in other formats'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output list in other formats', 'txt'), )); } } diff --git a/src/Symfony/Component/Console/Descriptor/Descriptor.php b/src/Symfony/Component/Console/Descriptor/Descriptor.php index 659b8f4cc76ad..23a7dca86f393 100644 --- a/src/Symfony/Component/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Component/Console/Descriptor/Descriptor.php @@ -16,28 +16,55 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; /** * @author Jean-François Simon */ abstract class Descriptor implements DescriptorInterface { - public function describe($object, array $options = array()) + /** + * @var OutputInterface + */ + private $output; + + /** + * {@inheritdoc} + */ + public function describe(OutputInterface $output, $object, array $options = array()) { + $this->output = $output; + switch (true) { case $object instanceof InputArgument: - return $this->describeInputArgument($object, $options); + $this->describeInputArgument($object, $options); + break; case $object instanceof InputOption: - return $this->describeInputOption($object, $options); + $this->describeInputOption($object, $options); + break; case $object instanceof InputDefinition: - return $this->describeInputDefinition($object, $options); + $this->describeInputDefinition($object, $options); + break; case $object instanceof Command: - return $this->describeCommand($object, $options); + $this->describeCommand($object, $options); + break; case $object instanceof Application: - return $this->describeApplication($object, $options); + $this->describeApplication($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); } + } - throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + /** + * Writes content to output. + * + * @param string $content + * @param boolean $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); } /** diff --git a/src/Symfony/Component/Console/Descriptor/DescriptorInterface.php b/src/Symfony/Component/Console/Descriptor/DescriptorInterface.php index a4e8633e8b50c..3929b6d9ed776 100644 --- a/src/Symfony/Component/Console/Descriptor/DescriptorInterface.php +++ b/src/Symfony/Component/Console/Descriptor/DescriptorInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Console\Descriptor; +use Symfony\Component\Console\Output\OutputInterface; + /** * Descriptor interface. * @@ -21,10 +23,9 @@ interface DescriptorInterface /** * Describes an InputArgument instance. * - * @param object $object - * @param array $options - * - * @return string|mixed + * @param OutputInterface $output + * @param object $object + * @param array $options */ - public function describe($object, array $options = array()); + public function describe(OutputInterface $output, $object, array $options = array()); } diff --git a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php index c2b2a533876d5..05adf0fbd0eab 100644 --- a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php @@ -29,13 +29,7 @@ class JsonDescriptor extends Descriptor */ protected function describeInputArgument(InputArgument $argument, array $options = array()) { - return $this->output(array( - 'name' => $argument->getName(), - 'is_required' => $argument->isRequired(), - 'is_array' => $argument->isArray(), - 'description' => $argument->getDescription(), - 'default' => $argument->getDefault(), - ), $options); + $this->writeData($this->getInputArgumentData($argument), $options); } /** @@ -43,15 +37,7 @@ protected function describeInputArgument(InputArgument $argument, array $options */ protected function describeInputOption(InputOption $option, array $options = array()) { - return $this->output(array( - 'name' => '--'.$option->getName(), - 'shortcut' => $option->getShortcut() ? '-'.implode('|-', explode('|', $option->getShortcut())) : '', - 'accept_value' => $option->acceptValue(), - 'is_value_required' => $option->isValueRequired(), - 'is_multiple' => $option->isArray(), - 'description' => $option->getDescription(), - 'default' => $option->getDefault(), - ), $options); + $this->writeData($this->getInputOptionData($option), $options); } /** @@ -59,17 +45,7 @@ protected function describeInputOption(InputOption $option, array $options = arr */ protected function describeInputDefinition(InputDefinition $definition, array $options = array()) { - $inputArguments = array(); - foreach ($definition->getArguments() as $name => $argument) { - $inputArguments[$name] = $this->describeInputArgument($argument, array('as_array' => true)); - } - - $inputOptions = array(); - foreach ($definition->getOptions() as $name => $option) { - $inputOptions[$name] = $this->describeInputOption($option, array('as_array' => true)); - } - - return $this->output(array('arguments' => $inputArguments, 'options' => $inputOptions), $options); + $this->writeData($this->getInputDefinitionData($definition), $options); } /** @@ -77,17 +53,7 @@ protected function describeInputDefinition(InputDefinition $definition, array $o */ protected function describeCommand(Command $command, array $options = array()) { - $command->getSynopsis(); - $command->mergeApplicationDefinition(false); - - return $this->output(array( - 'name' => $command->getName(), - 'usage' => $command->getSynopsis(), - 'description' => $command->getDescription(), - 'help' => $command->getProcessedHelp(), - 'aliases' => $command->getAliases(), - 'definition' => $this->describeInputDefinition($command->getNativeDefinition(), array('as_array' => true)), - ), $options); + $this->writeData($this->getCommandData($command), $options); } /** @@ -100,30 +66,100 @@ protected function describeApplication(Application $application, array $options $commands = array(); foreach ($description->getCommands() as $command) { - $commands[] = $this->describeCommand($command, array('as_array' => true)); + $commands[] = $this->getCommandData($command); } $data = $describedNamespace ? array('commands' => $commands, 'namespace' => $describedNamespace) : array('commands' => $commands, 'namespaces' => array_values($description->getNamespaces())); - return $this->output($data, $options); + $this->writeData($data, $options); } /** - * Outputs data as array or string according to options. + * Writes data as json. * * @param array $data * @param array $options * * @return array|string */ - private function output(array $data, array $options) + private function writeData(array $data, array $options) + { + $this->write(json_encode($data, isset($options['json_encoding']) ? $options['json_encoding'] : 0)); + } + + /** + * @param InputArgument $argument + * + * @return array + */ + private function getInputArgumentData(InputArgument $argument) + { + return array( + 'name' => $argument->getName(), + 'is_required' => $argument->isRequired(), + 'is_array' => $argument->isArray(), + 'description' => $argument->getDescription(), + 'default' => $argument->getDefault(), + ); + } + + /** + * @param InputOption $option + * + * @return array + */ + private function getInputOptionData(InputOption $option) + { + return array( + 'name' => '--'.$option->getName(), + 'shortcut' => $option->getShortcut() ? '-'.implode('|-', explode('|', $option->getShortcut())) : '', + 'accept_value' => $option->acceptValue(), + 'is_value_required' => $option->isValueRequired(), + 'is_multiple' => $option->isArray(), + 'description' => $option->getDescription(), + 'default' => $option->getDefault(), + ); + } + + /** + * @param InputDefinition $definition + * + * @return array + */ + private function getInputDefinitionData(InputDefinition $definition) { - if (isset($options['as_array']) && $options['as_array']) { - return $data; + $inputArguments = array(); + foreach ($definition->getArguments() as $name => $argument) { + $inputArguments[$name] = $this->getInputArgumentData($argument); + } + + $inputOptions = array(); + foreach ($definition->getOptions() as $name => $option) { + $inputOptions[$name] = $this->getInputOptionData($option); } - return json_encode($data, isset($options['json_encoding']) ? $options['json_encoding'] : 0); + return array('arguments' => $inputArguments, 'options' => $inputOptions); + } + + /** + * @param Command $command + * + * @return array + */ + private function getCommandData(Command $command) + { + $command->getSynopsis(); + $command->mergeApplicationDefinition(false); + + return array( + 'name' => $command->getName(), + 'usage' => $command->getSynopsis(), + 'description' => $command->getDescription(), + 'help' => $command->getProcessedHelp(), + 'aliases' => $command->getAliases(), + 'definition' => $this->getInputDefinitionData($command->getNativeDefinition()), + ); } } diff --git a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php index 770af38c934c5..7bc5808523f79 100644 --- a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php @@ -29,12 +29,14 @@ class MarkdownDescriptor extends Descriptor */ protected function describeInputArgument(InputArgument $argument, array $options = array()) { - return '**'.$argument->getName().':**'."\n\n" + $this->write( + '**'.$argument->getName().':**'."\n\n" .'* Name: '.($argument->getName() ?: '')."\n" .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n" .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n" .'* Description: '.($argument->getDescription() ?: '')."\n" - .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`'; + .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`' + ); } /** @@ -42,14 +44,16 @@ protected function describeInputArgument(InputArgument $argument, array $options */ protected function describeInputOption(InputOption $option, array $options = array()) { - return '**'.$option->getName().':**'."\n\n" + $this->write( + '**'.$option->getName().':**'."\n\n" .'* Name: `--'.$option->getName().'`'."\n" .'* Shortcut: '.($option->getShortcut() ? '`-'.implode('|-', explode('|', $option->getShortcut())).'`' : '')."\n" .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n" .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n" .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n" .'* Description: '.($option->getDescription() ?: '')."\n" - .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'; + .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`' + ); } /** @@ -57,23 +61,25 @@ protected function describeInputOption(InputOption $option, array $options = arr */ protected function describeInputDefinition(InputDefinition $definition, array $options = array()) { - $blocks = array(); - - if (count($definition->getArguments()) > 0) { - $blocks[] = '### Arguments:'; + if ($showArguments = count($definition->getArguments()) > 0) { + $this->write('### Arguments:'); foreach ($definition->getArguments() as $argument) { - $blocks[] = $this->describeInputArgument($argument); + $this->write("\n\n"); + $this->write($this->describeInputArgument($argument)); } } if (count($definition->getOptions()) > 0) { - $blocks[] = '### Options:'; + if ($showArguments) { + $this->write("\n\n"); + } + + $this->write('### Options:'); foreach ($definition->getOptions() as $option) { - $blocks[] = $this->describeInputOption($option); + $this->write("\n\n"); + $this->write($this->describeInputOption($option)); } } - - return implode("\n\n", $blocks); } /** @@ -84,21 +90,23 @@ protected function describeCommand(Command $command, array $options = array()) $command->getSynopsis(); $command->mergeApplicationDefinition(false); - $markdown = $command->getName()."\n" + $this->write( + $command->getName()."\n" .str_repeat('-', strlen($command->getName()))."\n\n" .'* Description: '.($command->getDescription() ?: '')."\n" .'* Usage: `'.$command->getSynopsis().'`'."\n" - .'* Aliases: '.(count($command->getAliases()) ? '`'.implode('`, `', $command->getAliases()).'`' : ''); + .'* Aliases: '.(count($command->getAliases()) ? '`'.implode('`, `', $command->getAliases()).'`' : '') + ); if ($help = $command->getProcessedHelp()) { - $markdown .= "\n\n".$help; + $this->write("\n\n"); + $this->write($help); } - if ($definitionMarkdown = $this->describeInputDefinition($command->getNativeDefinition())) { - $markdown .= "\n\n".$definitionMarkdown; + if ($definition = $command->getNativeDefinition()) { + $this->write("\n\n"); + $this->describeInputDefinition($command->getNativeDefinition()); } - - return $markdown; } /** @@ -108,22 +116,24 @@ protected function describeApplication(Application $application, array $options { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); - $blocks = array($application->getName()."\n".str_repeat('=', strlen($application->getName()))); + + $this->write($application->getName()."\n".str_repeat('=', strlen($application->getName()))); foreach ($description->getNamespaces() as $namespace) { if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { - $blocks[] = '**'.$namespace['id'].':**'; + $this->write("\n\n"); + $this->write('**'.$namespace['id'].':**'); } - $blocks[] = implode("\n", array_map(function ($commandName) { + $this->write("\n\n"); + $this->write(implode("\n", array_map(function ($commandName) { return '* '.$commandName; - } , $namespace['commands'])); + } , $namespace['commands']))); } foreach ($description->getCommands() as $command) { - $blocks[] = $this->describeCommand($command); + $this->write("\n\n"); + $this->write($this->describeCommand($command)); } - - return implode("\n\n", $blocks); } } diff --git a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php index 2003237441cef..f979fa7db27db 100644 --- a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php @@ -36,10 +36,12 @@ protected function describeInputArgument(InputArgument $argument, array $options } $nameWidth = isset($options['name_width']) ? $options['name_width'] : strlen($argument->getName()); - $output = str_replace("\n", "\n".str_repeat(' ', $nameWidth + 2), $argument->getDescription()); - $output = sprintf(" %-${nameWidth}s %s%s", $argument->getName(), $output, $default); - return isset($options['raw_text']) && $options['raw_text'] ? strip_tags($output) : $output; + $this->writeText(sprintf(" %-${nameWidth}s %s%s", + $argument->getName(), + str_replace("\n", "\n".str_repeat(' ', $nameWidth + 2), $argument->getDescription()), + $default + ), $options); } /** @@ -56,15 +58,13 @@ protected function describeInputOption(InputOption $option, array $options = arr $nameWidth = isset($options['name_width']) ? $options['name_width'] : strlen($option->getName()); $nameWithShortcutWidth = $nameWidth - strlen($option->getName()) - 2; - $output = sprintf(" %s %-${nameWithShortcutWidth}s%s%s%s", + $this->writeText(sprintf(" %s %-${nameWithShortcutWidth}s%s%s%s", '--'.$option->getName(), $option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '', str_replace("\n", "\n".str_repeat(' ', $nameWidth + 2), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : '' - ); - - return isset($options['raw_text']) && $options['raw_text'] ? strip_tags($output) : $output; + ), $options); } /** @@ -85,27 +85,27 @@ protected function describeInputDefinition(InputDefinition $definition, array $o } ++$nameWidth; - $messages = array(); - if ($definition->getArguments()) { - $messages[] = 'Arguments:'; + $this->writeText('Arguments:', $options); + $this->writeText("\n"); foreach ($definition->getArguments() as $argument) { - $messages[] = $this->describeInputArgument($argument, array('name_width' => $nameWidth)); + $this->describeInputArgument($argument, array_merge($options, array('name_width' => $nameWidth))); + $this->writeText("\n"); } - $messages[] = ''; + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); } if ($definition->getOptions()) { - $messages[] = 'Options:'; + $this->writeText('Options:', $options); + $this->writeText("\n"); foreach ($definition->getOptions() as $option) { - $messages[] = $this->describeInputOption($option, array('name_width' => $nameWidth)); + $this->describeInputOption($option, array_merge($options, array('name_width' => $nameWidth))); + $this->writeText("\n"); } - $messages[] = ''; } - - $output = implode("\n", $messages); - - return isset($options['raw_text']) && $options['raw_text'] ? strip_tags($output) : $output; } /** @@ -115,22 +115,30 @@ protected function describeCommand(Command $command, array $options = array()) { $command->getSynopsis(); $command->mergeApplicationDefinition(false); - $messages = array('Usage:', ' '.$command->getSynopsis(), ''); - if ($command->getAliases()) { - $messages[] = 'Aliases: '.implode(', ', $command->getAliases()).''; - } + $this->writeText('Usage:', $options); + $this->writeText("\n"); + $this->writeText(' '.$command->getSynopsis(), $options); + $this->writeText("\n"); - $messages[] = $this->describeInputDefinition($command->getNativeDefinition()); + if (count($command->getAliases()) > 0) { + $this->writeText("\n"); + $this->writeText('Aliases: '.implode(', ', $command->getAliases()).'', $options); + } - if ($help = $command->getProcessedHelp()) { - $messages[] = 'Help:'; - $messages[] = ' '.str_replace("\n", "\n ", $help)."\n"; + if ($definition = $command->getNativeDefinition()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); } - $output = implode("\n", $messages); + $this->writeText("\n"); - return isset($options['raw_text']) && $options['raw_text'] ? strip_tags($output) : $output; + if ($help = $command->getProcessedHelp()) { + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' '.str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } } /** @@ -140,41 +148,52 @@ protected function describeApplication(Application $application, array $options { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); - $messages = array(); if (isset($options['raw_text']) && $options['raw_text']) { $width = $this->getColumnWidth($description->getCommands()); foreach ($description->getCommands() as $command) { - $messages[] = sprintf("%-${width}s %s", $command->getName(), $command->getDescription()); + $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); } } else { $width = $this->getColumnWidth($description->getCommands()); - $messages[] = $application->getHelp(); - $messages[] = ''; + $this->writeText($application->getHelp(), $options); + $this->writeText("\n\n"); if ($describedNamespace) { - $messages[] = sprintf("Available commands for the \"%s\" namespace:", $describedNamespace); + $this->writeText(sprintf("Available commands for the \"%s\" namespace:", $describedNamespace), $options); } else { - $messages[] = 'Available commands:'; + $this->writeText('Available commands:', $options); } // add commands by namespace foreach ($description->getNamespaces() as $namespace) { if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { - $messages[] = ''.$namespace['id'].''; + $this->writeText("\n"); + $this->writeText(''.$namespace['id'].'', $options); } foreach ($namespace['commands'] as $name) { - $messages[] = sprintf(" %-${width}s %s", $name, $description->getCommand($name)->getDescription()); + $this->writeText("\n"); + $this->writeText(sprintf(" %-${width}s %s", $name, $description->getCommand($name)->getDescription()), $options); } } - } - $output = implode("\n", $messages); + $this->writeText("\n"); + } + } - return isset($options['raw_text']) && $options['raw_text'] ? strip_tags($output) : $output; + /** + * {@inheritdoc} + */ + private function writeText($content, array $options = array()) + { + $this->write( + isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, + isset($options['raw_output']) ? !$options['raw_output'] : true + ); } /** diff --git a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php index c828a9ceb8278..ac1e25e3afec6 100644 --- a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php @@ -25,91 +25,34 @@ class XmlDescriptor extends Descriptor { /** - * {@inheritdoc} - */ - protected function describeInputArgument(InputArgument $argument, array $options = array()) - { - $dom = new \DOMDocument('1.0', 'UTF-8'); - - $dom->appendChild($objectXML = $dom->createElement('argument')); - $objectXML->setAttribute('name', $argument->getName()); - $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); - $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); - $objectXML->appendChild($descriptionXML = $dom->createElement('description')); - $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); - - $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); - $defaults = is_array($argument->getDefault()) ? $argument->getDefault() : (is_bool($argument->getDefault()) ? array(var_export($argument->getDefault(), true)) : ($argument->getDefault() ? array($argument->getDefault()) : array())); - foreach ($defaults as $default) { - $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); - $defaultXML->appendChild($dom->createTextNode($default)); - } - - return $this->output($dom, $options); - } - - /** - * {@inheritdoc} - */ - protected function describeInputOption(InputOption $option, array $options = array()) - { - $dom = new \DOMDocument('1.0', 'UTF-8'); - - $dom->appendChild($objectXML = $dom->createElement('option')); - $objectXML->setAttribute('name', '--'.$option->getName()); - $pos = strpos($option->getShortcut(), '|'); - if (false !== $pos) { - $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); - $objectXML->setAttribute('shortcuts', '-'.implode('|-', explode('|', $option->getShortcut()))); - } else { - $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); - } - $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); - $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); - $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); - $objectXML->appendChild($descriptionXML = $dom->createElement('description')); - $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); - - if ($option->acceptValue()) { - $defaults = is_array($option->getDefault()) ? $option->getDefault() : (is_bool($option->getDefault()) ? array(var_export($option->getDefault(), true)) : ($option->getDefault() ? array($option->getDefault()) : array())); - $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); - - if (!empty($defaults)) { - foreach ($defaults as $default) { - $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); - $defaultXML->appendChild($dom->createTextNode($default)); - } - } - } - - return $this->output($dom, $options); - } - - /** - * {@inheritdoc} + * @param InputDefinition $definition + * + * @return \DOMDocument */ - protected function describeInputDefinition(InputDefinition $definition, array $options = array()) + public function getInputDefinitionDocument(InputDefinition $definition) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($definitionXML = $dom->createElement('definition')); $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments')); foreach ($definition->getArguments() as $argument) { - $this->appendDocument($argumentsXML, $this->describeInputArgument($argument, array('as_dom' => true))); + $this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument)); } $definitionXML->appendChild($optionsXML = $dom->createElement('options')); foreach ($definition->getOptions() as $option) { - $this->appendDocument($optionsXML, $this->describeInputOption($option, array('as_dom' => true))); + $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); } - return $this->output($dom, $options); + return $dom; } /** - * {@inheritdoc} + * @param Command $command + * + * @return \DOMDocument */ - protected function describeCommand(Command $command, array $options = array()) + public function getCommandDocument(Command $command) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($commandXML = $dom->createElement('command')); @@ -135,47 +78,97 @@ protected function describeCommand(Command $command, array $options = array()) $aliasXML->appendChild($dom->createTextNode($alias)); } - $definitionXML = $this->describeInputDefinition($command->getNativeDefinition(), array('as_dom' => true)); + $definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition()); $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); - return $this->output($dom, $options); + return $dom; } /** - * {@inheritdoc} + * @param Application $application + * @param string|null $namespace + * + * @return \DOMDocument */ - protected function describeApplication(Application $application, array $options = array()) + public function getApplicationDocument(Application $application, $namespace = null) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($rootXml = $dom->createElement('symfony')); + + if ($application->getName() !== 'UNKNOWN') { + $rootXml->setAttribute('name', $application->getName()); + if ($application->getVersion() !== 'UNKNOWN') { + $rootXml->setAttribute('version', $application->getVersion()); + } + } + $rootXml->appendChild($commandsXML = $dom->createElement('commands')); - $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; - $description = new ApplicationDescription($application, $describedNamespace); + $description = new ApplicationDescription($application, $namespace); - if ($describedNamespace) { - $commandsXML->setAttribute('namespace', $describedNamespace); + if ($namespace) { + $commandsXML->setAttribute('namespace', $namespace); } foreach ($description->getCommands() as $command) { - $this->appendDocument($commandsXML, $this->describeCommand($command, array('as_dom' => true))); + $this->appendDocument($commandsXML, $this->getCommandDocument($command)); } - if (!$describedNamespace) { + if (!$namespace) { $rootXml->appendChild($namespacesXML = $dom->createElement('namespaces')); - foreach ($description->getNamespaces() as $namespace) { + foreach ($description->getNamespaces() as $namespaceDescription) { $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace')); - $namespaceArrayXML->setAttribute('id', $namespace['id']); + $namespaceArrayXML->setAttribute('id', $namespaceDescription['id']); - foreach ($namespace['commands'] as $name) { + foreach ($namespaceDescription['commands'] as $name) { $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command')); $commandXML->appendChild($dom->createTextNode($name)); } } } - return $this->output($dom, $options); + return $dom; + } + + /** + * {@inheritdoc} + */ + protected function describeInputArgument(InputArgument $argument, array $options = array()) + { + $this->writeDocument($this->getInputArgumentDocument($argument)); + } + + /** + * {@inheritdoc} + */ + protected function describeInputOption(InputOption $option, array $options = array()) + { + $this->writeDocument($this->getInputOptionDocument($option)); + } + + /** + * {@inheritdoc} + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = array()) + { + $this->writeDocument($this->getInputDefinitionDocument($definition)); + } + + /** + * {@inheritdoc} + */ + protected function describeCommand(Command $command, array $options = array()) + { + $this->writeDocument($this->getCommandDocument($command)); + } + + /** + * {@inheritdoc} + */ + protected function describeApplication(Application $application, array $options = array()) + { + $this->writeDocument($this->getApplicationDocument($application, isset($options['namespace']) ? $options['namespace'] : null)); } /** @@ -192,21 +185,80 @@ private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent) } /** - * Outputs document as DOMDocument or string according to options. + * Writes DOM document. * * @param \DOMDocument $dom - * @param array $options * * @return \DOMDocument|string */ - private function output(\DOMDocument $dom, array $options) + private function writeDocument(\DOMDocument $dom) + { + $dom->formatOutput = true; + $this->write($dom->saveXML()); + } + + /** + * @param InputArgument $argument + * + * @return \DOMDocument + */ + private function getInputArgumentDocument(InputArgument $argument) { - if (isset($options['as_dom']) && $options['as_dom']) { - return $dom; + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $dom->appendChild($objectXML = $dom->createElement('argument')); + $objectXML->setAttribute('name', $argument->getName()); + $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); + $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); + + $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); + $defaults = is_array($argument->getDefault()) ? $argument->getDefault() : (is_bool($argument->getDefault()) ? array(var_export($argument->getDefault(), true)) : ($argument->getDefault() ? array($argument->getDefault()) : array())); + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); } - $dom->formatOutput = true; + return $dom; + } + + /** + * @param InputOption $option + * + * @return \DOMDocument + */ + private function getInputOptionDocument(InputOption $option) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $dom->appendChild($objectXML = $dom->createElement('option')); + $objectXML->setAttribute('name', '--'.$option->getName()); + $pos = strpos($option->getShortcut(), '|'); + if (false !== $pos) { + $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); + $objectXML->setAttribute('shortcuts', '-'.implode('|-', explode('|', $option->getShortcut()))); + } else { + $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); + } + $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); + $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); + $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); + + if ($option->acceptValue()) { + $defaults = is_array($option->getDefault()) ? $option->getDefault() : (is_bool($option->getDefault()) ? array(var_export($option->getDefault(), true)) : ($option->getDefault() ? array($option->getDefault()) : array())); + $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); + + if (!empty($defaults)) { + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); + } + } + } - return $dom->saveXML(); + return $dom; } } diff --git a/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php b/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php index 222838ce6a131..47ac68b688eda 100644 --- a/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Console\Event; -use Symfony\Component\Console\Command\Command; - /** * Allows to do things before the command is executed. * diff --git a/src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php b/src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php index fa054ec223278..a8619b713f28e 100644 --- a/src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php @@ -30,7 +30,7 @@ public function __construct(Command $command, InputInterface $input, OutputInter parent::__construct($command, $input, $output); $this->setException($exception); - $this->exitCode = $exitCode; + $this->exitCode = (int) $exitCode; } /** diff --git a/src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php b/src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php index 9f80cb70a0dbf..7eeea60da4cc9 100644 --- a/src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php @@ -43,7 +43,7 @@ public function __construct(Command $command, InputInterface $input, OutputInter */ public function setExitCode($exitCode) { - $this->exitCode = $exitCode; + $this->exitCode = (int) $exitCode; } /** diff --git a/src/Symfony/Component/Console/Helper/DescriptorHelper.php b/src/Symfony/Component/Console/Helper/DescriptorHelper.php index 0317d5d08dc53..932dd0f299ef4 100644 --- a/src/Symfony/Component/Console/Helper/DescriptorHelper.php +++ b/src/Symfony/Component/Console/Helper/DescriptorHelper.php @@ -46,23 +46,29 @@ public function __construct() /** * Describes an object if supported. * + * Available options are: + * * format: string, the output format name + * * raw_text: boolean, sets output type as raw + * * @param OutputInterface $output * @param object $object - * @param string $format - * @param boolean $raw + * @param array $options + * + * @throws \InvalidArgumentException */ - public function describe(OutputInterface $output, $object, $format = null, $raw = false, $namespace = null) + public function describe(OutputInterface $output, $object, array $options = array()) { - $options = array('raw_text' => $raw, 'format' => $format ?: 'txt', 'namespace' => $namespace); - $type = !$raw && 'txt' === $options['format'] ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW; + $options = array_merge(array( + 'raw_text' => false, + 'format' => 'txt', + ), $options); if (!isset($this->descriptors[$options['format']])) { throw new \InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format'])); } $descriptor = $this->descriptors[$options['format']]; - - $output->writeln($descriptor->describe($object, $options), $type); + $descriptor->describe($output, $object, $options); } /** diff --git a/src/Symfony/Component/Console/Helper/DialogHelper.php b/src/Symfony/Component/Console/Helper/DialogHelper.php index 4b74f5a8a3586..570b601234719 100644 --- a/src/Symfony/Component/Console/Helper/DialogHelper.php +++ b/src/Symfony/Component/Console/Helper/DialogHelper.php @@ -19,7 +19,7 @@ * * @author Fabien Potencier */ -class DialogHelper extends Helper +class DialogHelper extends InputAwareHelper { private $inputStream; private static $shell; @@ -98,6 +98,10 @@ public function select(OutputInterface $output, $question, $choices, $default = */ public function ask(OutputInterface $output, $question, $default = null, array $autocomplete = null) { + if ($this->input && !$this->input->isInteractive()) { + return $default; + } + $output->write($question); $inputStream = $this->inputStream ?: STDIN; diff --git a/src/Symfony/Component/Console/Helper/HelperSet.php b/src/Symfony/Component/Console/Helper/HelperSet.php index d95c9a3036f49..e080678bce86b 100644 --- a/src/Symfony/Component/Console/Helper/HelperSet.php +++ b/src/Symfony/Component/Console/Helper/HelperSet.php @@ -18,9 +18,9 @@ * * @author Fabien Potencier */ -class HelperSet +class HelperSet implements \IteratorAggregate { - private $helpers; + private $helpers = array(); private $command; /** @@ -30,7 +30,6 @@ class HelperSet */ public function __construct(array $helpers = array()) { - $this->helpers = array(); foreach ($helpers as $alias => $helper) { $this->set($helper, is_int($alias) ? null : $alias); } @@ -101,4 +100,9 @@ public function getCommand() { return $this->command; } + + public function getIterator() + { + return new \ArrayIterator($this->helpers); + } } diff --git a/src/Symfony/Component/Console/Helper/InputAwareHelper.php b/src/Symfony/Component/Console/Helper/InputAwareHelper.php new file mode 100644 index 0000000000000..0d91202029e5c --- /dev/null +++ b/src/Symfony/Component/Console/Helper/InputAwareHelper.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputAwareInterface; + +/** + * An implementation of InputAwareInterface for Helpers. + * + * @author Wouter J + */ +abstract class InputAwareHelper extends Helper implements InputAwareInterface +{ + protected $input; + + /** + * {@inheritDoc} + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + } +} diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php index 9563d289d8680..d9502eb814e4a 100644 --- a/src/Symfony/Component/Console/Helper/ProgressHelper.php +++ b/src/Symfony/Component/Console/Helper/ProgressHelper.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Helper; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; /** @@ -185,7 +186,9 @@ public function start(OutputInterface $output, $max = null) $this->startTime = time(); $this->current = 0; $this->max = (int) $max; - $this->output = $output; + + // Disabling output when it does not support ANSI codes as it would result in a broken display anyway. + $this->output = $output->isDecorated() ? $output : new NullOutput(); $this->lastMessagesLength = 0; $this->barCharOriginal = ''; @@ -227,22 +230,7 @@ public function start(OutputInterface $output, $max = null) */ public function advance($step = 1, $redraw = false) { - if (null === $this->startTime) { - throw new \LogicException('You must start the progress bar before calling advance().'); - } - - if (0 === $this->current) { - $redraw = true; - } - - $prevPeriod = intval($this->current / $this->redrawFreq); - - $this->current += $step; - - $currPeriod = intval($this->current / $this->redrawFreq); - if ($redraw || $prevPeriod !== $currPeriod || $this->max === $this->current) { - $this->display(); - } + $this->setCurrent($this->current + $step, $redraw); } /** @@ -299,6 +287,18 @@ public function display($finish = false) $this->overwrite($this->output, $message); } + /** + * Removes the progress bar from the current line. + * + * This is useful if you wish to write some output + * while a progress bar is running. + * Call display() to show the progress bar again. + */ + public function clear() + { + $this->overwrite($this->output, ''); + } + /** * Finishes the progress output. */ diff --git a/src/Symfony/Component/Console/Helper/TableHelper.php b/src/Symfony/Component/Console/Helper/TableHelper.php index 0f81da089f0ee..d6ad0e9e01804 100644 --- a/src/Symfony/Component/Console/Helper/TableHelper.php +++ b/src/Symfony/Component/Console/Helper/TableHelper.php @@ -23,6 +23,7 @@ class TableHelper extends Helper { const LAYOUT_DEFAULT = 0; const LAYOUT_BORDERLESS = 1; + const LAYOUT_COMPACT = 2; /** * Table headers. @@ -45,6 +46,7 @@ class TableHelper extends Helper private $crossingChar; private $cellHeaderFormat; private $cellRowFormat; + private $cellRowContentFormat; private $borderFormat; private $padType; @@ -89,7 +91,22 @@ public function setLayout($layout) ->setVerticalBorderChar(' ') ->setCrossingChar(' ') ->setCellHeaderFormat('%s') - ->setCellRowFormat('%s') + ->setCellRowFormat('%s') + ->setCellRowContentFormat(' %s ') + ->setBorderFormat('%s') + ->setPadType(STR_PAD_RIGHT) + ; + break; + + case self::LAYOUT_COMPACT: + $this + ->setPaddingChar(' ') + ->setHorizontalBorderChar('') + ->setVerticalBorderChar(' ') + ->setCrossingChar('') + ->setCellHeaderFormat('%s') + ->setCellRowFormat('%s') + ->setCellRowContentFormat('%s') ->setBorderFormat('%s') ->setPadType(STR_PAD_RIGHT) ; @@ -102,7 +119,8 @@ public function setLayout($layout) ->setVerticalBorderChar('|') ->setCrossingChar('+') ->setCellHeaderFormat('%s') - ->setCellRowFormat('%s') + ->setCellRowFormat('%s') + ->setCellRowContentFormat(' %s ') ->setBorderFormat('%s') ->setPadType(STR_PAD_RIGHT) ; @@ -185,6 +203,10 @@ public function setRow($column, array $row) */ public function setPaddingChar($paddingChar) { + if (!$paddingChar) { + throw new \LogicException('The padding char must not be empty'); + } + $this->paddingChar = $paddingChar; return $this; @@ -260,6 +282,20 @@ public function setCellRowFormat($cellRowFormat) return $this; } + /** + * Sets row cell content format. + * + * @param string $cellRowContentFormat + * + * @return TableHelper + */ + public function setCellRowContentFormat($cellRowContentFormat) + { + $this->cellRowContentFormat = $cellRowContentFormat; + + return $this; + } + /** * Sets table border format. * @@ -332,11 +368,13 @@ private function renderRowSeparator() return; } + if (!$this->horizontalBorderChar && !$this->crossingChar) { + return; + } + $markup = $this->crossingChar; for ($column = 0; $column < $count; $column++) { - $markup .= str_repeat($this->horizontalBorderChar, $this->getColumnWidth($column)) - .$this->crossingChar - ; + $markup .= str_repeat($this->horizontalBorderChar, $this->getColumnWidth($column)).$this->crossingChar; } $this->output->writeln(sprintf($this->borderFormat, $markup)); @@ -391,15 +429,9 @@ private function renderCell(array $row, $column, $cellFormat) $width += $this->strlen($cell) - $this->computeLengthWithoutDecoration($cell); - $this->output->write(sprintf( - $cellFormat, - str_pad( - $this->paddingChar.$cell.$this->paddingChar, - $width, - $this->paddingChar, - $this->padType - ) - )); + $content = sprintf($this->cellRowContentFormat, $cell); + + $this->output->write(sprintf($cellFormat, str_pad($content, $width, $this->paddingChar, $this->padType))); } /** @@ -441,7 +473,7 @@ private function getColumnWidth($column) $lengths[] = $this->getCellWidth($row, $column); } - return $this->columnWidths[$column] = max($lengths) + 2; + return $this->columnWidths[$column] = max($lengths) + strlen($this->cellRowContentFormat) - 2; } /** diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index fb84cf829e3d6..a55029ec19883 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -24,9 +24,12 @@ */ abstract class Input implements InputInterface { + /** + * @var InputDefinition + */ protected $definition; - protected $options; - protected $arguments; + protected $options = array(); + protected $arguments = array(); protected $interactive = true; /** @@ -37,8 +40,6 @@ abstract class Input implements InputInterface public function __construct(InputDefinition $definition = null) { if (null === $definition) { - $this->arguments = array(); - $this->options = array(); $this->definition = new InputDefinition(); } else { $this->bind($definition); diff --git a/src/Symfony/Component/Console/Input/InputAwareInterface.php b/src/Symfony/Component/Console/Input/InputAwareInterface.php new file mode 100644 index 0000000000000..d0f11e986a3b8 --- /dev/null +++ b/src/Symfony/Component/Console/Input/InputAwareInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +/** + * InputAwareInterface should be implemented by classes that depends on the + * Console Input. + * + * @author Wouter J + */ +interface InputAwareInterface +{ + /** + * Sets the Console Input. + * + * @param InputInterface + */ + public function setInput(InputInterface $input); +} diff --git a/src/Symfony/Component/Console/Input/InputDefinition.php b/src/Symfony/Component/Console/Input/InputDefinition.php index 93ca9167dc958..c1cab5be34998 100644 --- a/src/Symfony/Component/Console/Input/InputDefinition.php +++ b/src/Symfony/Component/Console/Input/InputDefinition.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\Output\BufferedOutput; /** * A InputDefinition represents a set of valid command line arguments and options. @@ -426,8 +427,10 @@ public function getSynopsis() public function asText() { $descriptor = new TextDescriptor(); + $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); + $descriptor->describe($output, $this, array('raw_output' => true)); - return $descriptor->describe($this); + return $output->fetch(); } /** @@ -443,6 +446,13 @@ public function asXml($asDom = false) { $descriptor = new XmlDescriptor(); - return $descriptor->describe($this, array('as_dom' => $asDom)); + if ($asDom) { + return $descriptor->getInputDefinitionDocument($this); + } + + $output = new BufferedOutput(); + $descriptor->describe($output, $this); + + return $output->fetch(); } } diff --git a/src/Symfony/Component/Console/Input/StringInput.php b/src/Symfony/Component/Console/Input/StringInput.php index 7f87f714c43eb..6537e27a6bb3d 100644 --- a/src/Symfony/Component/Console/Input/StringInput.php +++ b/src/Symfony/Component/Console/Input/StringInput.php @@ -72,9 +72,7 @@ private function tokenize($input) $tokens[] = stripcslashes($match[1]); } else { // should never happen - // @codeCoverageIgnoreStart throw new \InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10))); - // @codeCoverageIgnoreEnd } $cursor += strlen($match[0]); diff --git a/src/Symfony/Component/Console/Output/BufferedOutput.php b/src/Symfony/Component/Console/Output/BufferedOutput.php new file mode 100644 index 0000000000000..5682fc2404f78 --- /dev/null +++ b/src/Symfony/Component/Console/Output/BufferedOutput.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\Console\Output; + +/** + * @author Jean-François Simon + */ +class BufferedOutput extends Output +{ + /** + * @var string + */ + private $buffer = ''; + + /** + * Empties buffer and returns its content. + * + * @return string + */ + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + + return $content; + } + + /** + * {@inheritdoc} + */ + protected function doWrite($message, $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= "\n"; + } + } +} diff --git a/src/Symfony/Component/Console/Output/Output.php b/src/Symfony/Component/Console/Output/Output.php index 0daedc3eaf91e..94b9c88d76d91 100644 --- a/src/Symfony/Component/Console/Output/Output.php +++ b/src/Symfony/Component/Console/Output/Output.php @@ -46,7 +46,7 @@ abstract class Output implements OutputInterface public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = false, OutputFormatterInterface $formatter = null) { $this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity; - $this->formatter = null === $formatter ? new OutputFormatter() : $formatter; + $this->formatter = $formatter ?: new OutputFormatter(); $this->formatter->setDecorated($decorated); } @@ -98,6 +98,26 @@ public function getVerbosity() return $this->verbosity; } + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Console/Output/StreamOutput.php b/src/Symfony/Component/Console/Output/StreamOutput.php index 4194ed3d1ef5f..7c7b3c480598b 100644 --- a/src/Symfony/Component/Console/Output/StreamOutput.php +++ b/src/Symfony/Component/Console/Output/StreamOutput.php @@ -75,10 +75,8 @@ public function getStream() protected function doWrite($message, $newline) { if (false === @fwrite($this->stream, $message.($newline ? PHP_EOL : ''))) { - // @codeCoverageIgnoreStart // should never happen throw new \RuntimeException('Unable to write output.'); - // @codeCoverageIgnoreEnd } fflush($this->stream); @@ -96,12 +94,10 @@ protected function doWrite($message, $newline) */ protected function hasColorSupport() { - // @codeCoverageIgnoreStart if (DIRECTORY_SEPARATOR == '\\') { return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); } return function_exists('posix_isatty') && @posix_isatty($this->stream); - // @codeCoverageIgnoreEnd } } diff --git a/src/Symfony/Component/Console/Shell.php b/src/Symfony/Component/Console/Shell.php index 907b63e77cac5..3ea11dc371000 100644 --- a/src/Symfony/Component/Console/Shell.php +++ b/src/Symfony/Component/Console/Shell.php @@ -31,7 +31,7 @@ class Shell private $history; private $output; private $hasReadline; - private $processIsolation; + private $processIsolation = false; /** * Constructor. @@ -47,7 +47,6 @@ public function __construct(Application $application) $this->application = $application; $this->history = getenv('HOME').'/.history_'.$application->getName(); $this->output = new ConsoleOutput(); - $this->processIsolation = false; } /** diff --git a/src/Symfony/Component/Console/Tester/ApplicationTester.php b/src/Symfony/Component/Console/Tester/ApplicationTester.php index 2e57e1a35d2c1..dd7c2a5dc0567 100644 --- a/src/Symfony/Component/Console/Tester/ApplicationTester.php +++ b/src/Symfony/Component/Console/Tester/ApplicationTester.php @@ -32,6 +32,7 @@ class ApplicationTester private $application; private $input; private $output; + private $statusCode; /** * Constructor. @@ -72,7 +73,7 @@ public function run(array $input, $options = array()) $this->output->setVerbosity($options['verbosity']); } - return $this->application->run($this->input, $this->output); + return $this->statusCode = $this->application->run($this->input, $this->output); } /** @@ -114,4 +115,14 @@ public function getOutput() { return $this->output; } + + /** + * Gets the status code returned by the last execution of the application. + * + * @return integer The status code + */ + public function getStatusCode() + { + return $this->statusCode; + } } diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index 02715b00e3c80..6866bc6420baa 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -27,6 +27,7 @@ class CommandTester private $command; private $input; private $output; + private $statusCode; /** * Constructor. @@ -54,6 +55,15 @@ public function __construct(Command $command) */ public function execute(array $input, array $options = array()) { + // set the command name automatically if the application requires + // this argument and no command name was passed + if (!isset($input['command']) + && (null !== $application = $this->command->getApplication()) + && $application->getDefinition()->hasArgument('command') + ) { + $input['command'] = $this->command->getName(); + } + $this->input = new ArrayInput($input); if (isset($options['interactive'])) { $this->input->setInteractive($options['interactive']); @@ -67,7 +77,7 @@ public function execute(array $input, array $options = array()) $this->output->setVerbosity($options['verbosity']); } - return $this->command->run($this->input, $this->output); + return $this->statusCode = $this->command->run($this->input, $this->output); } /** @@ -109,4 +119,14 @@ public function getOutput() { return $this->output; } + + /** + * Gets the status code returned by the last execution of the application. + * + * @return integer The status code + */ + public function getStatusCode() + { + return $this->statusCode; + } } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index e45464a177dc6..044ae60a1843c 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; +use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -22,6 +23,7 @@ use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\Output; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleExceptionEvent; @@ -40,6 +42,9 @@ public static function setUpBeforeClass() require_once self::$fixturesPath.'/Foo2Command.php'; require_once self::$fixturesPath.'/Foo3Command.php'; require_once self::$fixturesPath.'/Foo4Command.php'; + require_once self::$fixturesPath.'/Foo5Command.php'; + require_once self::$fixturesPath.'/FoobarCommand.php'; + require_once self::$fixturesPath.'/BarBucCommand.php'; } protected function normalizeLineBreaks($text) @@ -124,6 +129,16 @@ public function testAdd() $this->assertEquals(array($foo, $foo1), array($commands['foo:bar'], $commands['foo:bar1']), '->addCommands() registers an array of commands'); } + /** + * @expectedException \LogicException + * @expectedExceptionMessage Command class "Foo5Command" is not correctly initialized. You probably forgot to call the parent constructor. + */ + public function testAddCommandWithEmptyConstructor() + { + $application = new Application(); + $application->add(new \Foo5Command()); + } + public function testHasGet() { $application = new Application(); @@ -193,6 +208,7 @@ public function testFindNamespace() public function testFindAmbiguousNamespace() { $application = new Application(); + $application->add(new \BarBucCommand()); $application->add(new \FooCommand()); $application->add(new \Foo2Command()); $application->findNamespace('f'); @@ -208,6 +224,20 @@ public function testFindInvalidNamespace() $application->findNamespace('bar'); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Command "foo1" is not defined + */ + public function testFindUniqueNameButNamespaceName() + { + $application = new Application(); + $application->add(new \FooCommand()); + $application->add(new \Foo1Command()); + $application->add(new \Foo2Command()); + + $application->find($commandName = 'foo1'); + } + public function testFind() { $application = new Application(); @@ -240,7 +270,7 @@ public function provideAmbiguousAbbreviations() return array( array('f', 'Command "f" is not defined.'), array('a', 'Command "a" is ambiguous (afoobar, afoobar1 and 1 more).'), - array('foo:b', 'Command "foo:b" is ambiguous (foo:bar, foo:bar1).') + array('foo:b', 'Command "foo:b" is ambiguous (foo:bar, foo:bar1 and 1 more).') ); } @@ -254,6 +284,23 @@ public function testFindCommandEqualNamespace() $this->assertInstanceOf('Foo4Command', $application->find('foo3:bar:toh'), '->find() returns a command even if its namespace equals another command name'); } + public function testFindCommandWithAmbiguousNamespacesButUniqueName() + { + $application = new Application(); + $application->add(new \FooCommand()); + $application->add(new \FoobarCommand()); + + $this->assertInstanceOf('FoobarCommand', $application->find('f:f')); + } + + public function testFindCommandWithMissingNamespace() + { + $application = new Application(); + $application->add(new \Foo4Command()); + + $this->assertInstanceOf('Foo4Command', $application->find('f::t')); + } + /** * @dataProvider provideInvalidCommandNamesSingle * @expectedException \InvalidArgumentException @@ -262,15 +309,15 @@ public function testFindCommandEqualNamespace() public function testFindAlternativeExceptionMessageSingle($name) { $application = new Application(); - $application->add(new \FooCommand()); + $application->add(new \Foo3Command()); $application->find($name); } public function provideInvalidCommandNamesSingle() { return array( - array('foo:baR'), - array('foO:bar') + array('foo3:baR'), + array('foO3:bar') ); } @@ -288,6 +335,8 @@ public function testFindAlternativeExceptionMessageMultiple() } catch (\Exception $e) { $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); $this->assertRegExp('/Did you mean one of these/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); + $this->assertRegExp('/foo1:bar/', $e->getMessage()); + $this->assertRegExp('/foo:bar/', $e->getMessage()); } // Namespace + plural @@ -297,6 +346,7 @@ public function testFindAlternativeExceptionMessageMultiple() } catch (\Exception $e) { $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); $this->assertRegExp('/Did you mean one of these/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); + $this->assertRegExp('/foo1/', $e->getMessage()); } $application->add(new \Foo3Command()); @@ -329,27 +379,31 @@ public function testFindAlternativeCommands() $this->assertEquals(sprintf('Command "%s" is not defined.', $commandName), $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, without alternatives'); } + // Test if "bar1" command throw an "\InvalidArgumentException" and does not contain + // "foo:bar" as alternative because "bar1" is too far from "foo:bar" try { - $application->find($commandName = 'foo'); + $application->find($commandName = 'bar1'); $this->fail('->find() throws an \InvalidArgumentException if command does not exist'); } catch (\Exception $e) { $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if command does not exist'); $this->assertRegExp(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); - $this->assertRegExp('/foo:bar/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternative : "foo:bar"'); - $this->assertRegExp('/foo1:bar/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternative : "foo1:bar"'); + $this->assertRegExp('/afoobar1/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternative : "afoobar1"'); $this->assertRegExp('/foo:bar1/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternative : "foo:bar1"'); + $this->assertNotRegExp('/foo:bar(?>!1)/', $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, without "foo:bar" alternative'); } + } - // Test if "foo1" command throw an "\InvalidArgumentException" and does not contain - // "foo:bar" as alternative because "foo1" is too far from "foo:bar" - try { - $application->find($commandName = 'foo1'); - $this->fail('->find() throws an \InvalidArgumentException if command does not exist'); - } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->find() throws an \InvalidArgumentException if command does not exist'); - $this->assertRegExp(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws an \InvalidArgumentException if command does not exist, with alternatives'); - $this->assertFalse(strpos($e->getMessage(), 'foo:bar'), '->find() throws an \InvalidArgumentException if command does not exist, without "foo:bar" alternative'); - } + public function testFindAlternativeCommandsWithAnAlias() + { + $fooCommand = new \FooCommand(); + $fooCommand->setAliases(array('foo2')); + + $application = new Application(); + $application->add($fooCommand); + + $result = $application->find('foo'); + + $this->assertSame($fooCommand, $result); } public function testFindAlternativeNamespace() @@ -561,6 +615,29 @@ public function testRun() $this->assertSame('called'.PHP_EOL, $tester->getDisplay(), '->run() does not call interact() if -n is passed'); } + /** + * Issue #9285 + * + * If the "verbose" option is just before an argument in ArgvInput, + * an argument value should not be treated as verbosity value. + * This test will fail with "Not enough arguments." if broken + */ + public function testVerboseValueNotBreakArguments() + { + $application = new Application(); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + $application->add(new \FooCommand()); + + $output = new StreamOutput(fopen('php://memory', 'w', false)); + + $input = new ArgvInput(array('cli.php', '-v', 'foo:bar')); + $application->run($input, $output); + + $input = new ArgvInput(array('cli.php', '--verbose', 'foo:bar')); + $application->run($input, $output); + } + public function testRunReturnsIntegerExitCode() { $exception = new \Exception('', 4); @@ -734,10 +811,6 @@ public function testSettingCustomInputDefinitionOverwritesDefaultValues() public function testRunWithDispatcher() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $application = new Application(); $application->setAutoExit(false); $application->setDispatcher($this->getDispatcher()); @@ -757,10 +830,6 @@ public function testRunWithDispatcher() */ public function testRunWithExceptionAndDispatcher() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $application = new Application(); $application->setDispatcher($this->getDispatcher()); $application->setAutoExit(false); @@ -776,10 +845,6 @@ public function testRunWithExceptionAndDispatcher() public function testRunDispatchesAllEventsWithException() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $application = new Application(); $application->setDispatcher($this->getDispatcher()); $application->setAutoExit(false); @@ -795,9 +860,24 @@ public function testRunDispatchesAllEventsWithException() $this->assertContains('before.foo.after.caught.', $tester->getDisplay()); } + public function testTerminalDimensions() + { + $application = new Application(); + $originalDimensions = $application->getTerminalDimensions(); + $this->assertCount(2, $originalDimensions); + + $width = 80; + if ($originalDimensions[0] == $width) { + $width = 100; + } + + $application->setTerminalDimensions($width, 80); + $this->assertSame(array($width, 80), $application->getTerminalDimensions()); + } + protected function getDispatcher() { - $dispatcher = new EventDispatcher; + $dispatcher = new EventDispatcher(); $dispatcher->addListener('console.command', function (ConsoleCommandEvent $event) { $event->getOutput()->write('before.'); }); @@ -814,6 +894,28 @@ protected function getDispatcher() return $dispatcher; } + + public function testSetRunCustomDefaultCommand() + { + $command = new \FooCommand(); + + $application = new Application(); + $application->setAutoExit(false); + $application->add($command); + $application->setDefaultCommand($command->getName()); + + $tester = new ApplicationTester($application); + $tester->run(array()); + $this->assertEquals('interact called'.PHP_EOL.'called'.PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command'); + + $application = new CustomDefaultCommandApplication(); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array()); + + $this->assertEquals('interact called'.PHP_EOL.'called'.PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command'); + } } class CustomApplication extends Application @@ -838,3 +940,18 @@ protected function getDefaultHelperSet() return new HelperSet(array(new FormatterHelper())); } } + +class CustomDefaultCommandApplication extends Application +{ + /** + * Overwrites the constructor in order to set a different default command. + */ + public function __construct() + { + parent::__construct(); + + $command = new \FooCommand(); + $this->add($command); + $this->setDefaultCommand($command->getName()); + } +} diff --git a/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php index aab1cf376171f..406c6599ba1ea 100644 --- a/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Component/Console/Tests/Descriptor/AbstractDescriptorTest.php @@ -16,31 +16,32 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\BufferedOutput; abstract class AbstractDescriptorTest extends \PHPUnit_Framework_TestCase { /** @dataProvider getDescribeInputArgumentTestData */ public function testDescribeInputArgument(InputArgument $argument, $expectedDescription) { - $this->assertEquals(trim($expectedDescription), trim($this->getDescriptor()->describe($argument))); + $this->assertDescription($expectedDescription, $argument); } /** @dataProvider getDescribeInputOptionTestData */ public function testDescribeInputOption(InputOption $option, $expectedDescription) { - $this->assertEquals(trim($expectedDescription), trim($this->getDescriptor()->describe($option))); + $this->assertDescription($expectedDescription, $option); } /** @dataProvider getDescribeInputDefinitionTestData */ public function testDescribeInputDefinition(InputDefinition $definition, $expectedDescription) { - $this->assertEquals(trim($expectedDescription), trim($this->getDescriptor()->describe($definition))); + $this->assertDescription($expectedDescription, $definition); } /** @dataProvider getDescribeCommandTestData */ public function testDescribeCommand(Command $command, $expectedDescription) { - $this->assertEquals(trim($expectedDescription), trim($this->getDescriptor()->describe($command))); + $this->assertDescription($expectedDescription, $command); } /** @dataProvider getDescribeApplicationTestData */ @@ -53,7 +54,7 @@ public function testDescribeApplication(Application $application, $expectedDescr $command->setHelp(str_replace('%command.full_name%', 'app/console %command.name%', $command->getHelp())); } - $this->assertEquals(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $this->getDescriptor()->describe($application)))); + $this->assertDescription($expectedDescription, $application); } public function getDescribeInputArgumentTestData() @@ -94,4 +95,11 @@ private function getDescriptionTestData(array $objects) return $data; } + + private function assertDescription($expectedDescription, $describedObject) + { + $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); + $this->getDescriptor()->describe($output, $describedObject, array('raw_output' => true)); + $this->assertEquals(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $output->fetch()))); + } } diff --git a/src/Symfony/Component/Console/Tests/Fixtures/BarBucCommand.php b/src/Symfony/Component/Console/Tests/Fixtures/BarBucCommand.php new file mode 100644 index 0000000000000..52b619e821ed0 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/BarBucCommand.php @@ -0,0 +1,11 @@ +setName('bar:buc'); + } +} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Foo5Command.php b/src/Symfony/Component/Console/Tests/Fixtures/Foo5Command.php new file mode 100644 index 0000000000000..a1c60827a5153 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Foo5Command.php @@ -0,0 +1,10 @@ +setName('foobar:foo') + ->setDescription('The foobar:foo command') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + } +} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json index 84b587e268c38..09adbd419a1b1 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.json +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.json @@ -1 +1 @@ -{"commands":[{"name":"help","usage":"help [--xml] [--format=\"...\"] [--raw] [command_name]","description":"Displays help for a command","help":"The help<\/info> command displays help for a given command:\n\n php app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n php app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.","aliases":[],"definition":{"arguments":{"command_name":{"name":"command_name","is_required":false,"is_array":false,"description":"The command name","default":"help"}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output help as XML","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output help in other formats","default":null},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command help","default":false},"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}},{"name":"list","usage":"list [--xml] [--raw] [--format=\"...\"] [namespace]","description":"Lists commands","help":"The list<\/info> command lists all commands:\n\n php app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n php app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n php app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n php app\/console list --raw<\/info>","aliases":[],"definition":{"arguments":{"namespace":{"name":"namespace","is_required":false,"is_array":false,"description":"The namespace name","default":null}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output list as XML","default":false},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command list","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output list in other formats","default":null}}}}],"namespaces":[{"id":"_global","commands":["help","list"]}]} +{"commands":[{"name":"help","usage":"help [--xml] [--format=\"...\"] [--raw] [command_name]","description":"Displays help for a command","help":"The help<\/info> command displays help for a given command:\n\n php app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n php app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.","aliases":[],"definition":{"arguments":{"command_name":{"name":"command_name","is_required":false,"is_array":false,"description":"The command name","default":"help"}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output help as XML","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output help in other formats","default":"txt"},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command help","default":false},"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}},{"name":"list","usage":"list [--xml] [--raw] [--format=\"...\"] [namespace]","description":"Lists commands","help":"The list<\/info> command lists all commands:\n\n php app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n php app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n php app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n php app\/console list --raw<\/info>","aliases":[],"definition":{"arguments":{"namespace":{"name":"namespace","is_required":false,"is_array":false,"description":"The namespace name","default":null}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output list as XML","default":false},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command list","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output list in other formats","default":"txt"}}}}],"namespaces":[{"id":"_global","commands":["help","list"]}]} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md b/src/Symfony/Component/Console/Tests/Fixtures/application_1.md index ae815a8d5bf0f..a789251ed2706 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.md +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.md @@ -51,7 +51,7 @@ To display the list of available commands, please use the list comm * Is value required: yes * Is multiple: no * Description: To output help in other formats -* Default: `NULL` +* Default: `'txt'` **raw:** @@ -196,4 +196,4 @@ It's also possible to get raw list of commands (useful for embedding command run * Is value required: yes * Is multiple: no * Description: To output list in other formats -* Default: `NULL` +* Default: `'txt'` diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml index d5c57be40155b..bfe5de0095c4b 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_1.xml @@ -28,7 +28,9 @@ diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.json b/src/Symfony/Component/Console/Tests/Fixtures/application_2.json index e0c4b9ad02438..27745c15d20b7 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.json +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.json @@ -1 +1 @@ -{"commands":[{"name":"help","usage":"help [--xml] [--format=\"...\"] [--raw] [command_name]","description":"Displays help for a command","help":"The help<\/info> command displays help for a given command:\n\n php app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n php app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.","aliases":[],"definition":{"arguments":{"command_name":{"name":"command_name","is_required":false,"is_array":false,"description":"The command name","default":"help"}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output help as XML","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output help in other formats","default":null},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command help","default":false},"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}},{"name":"list","usage":"list [--xml] [--raw] [--format=\"...\"] [namespace]","description":"Lists commands","help":"The list<\/info> command lists all commands:\n\n php app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n php app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n php app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n php app\/console list --raw<\/info>","aliases":[],"definition":{"arguments":{"namespace":{"name":"namespace","is_required":false,"is_array":false,"description":"The namespace name","default":null}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output list as XML","default":false},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command list","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output list in other formats","default":null}}}},{"name":"descriptor:command1","usage":"descriptor:command1","description":"command 1 description","help":"command 1 help","aliases":["alias1","alias2"],"definition":{"arguments":[],"options":{"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}},{"name":"descriptor:command2","usage":"descriptor:command2 [-o|--option_name] argument_name","description":"command 2 description","help":"command 2 help","aliases":[],"definition":{"arguments":{"argument_name":{"name":"argument_name","is_required":true,"is_array":false,"description":"","default":null}},"options":{"option_name":{"name":"--option_name","shortcut":"-o","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"","default":false},"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}}],"namespaces":[{"id":"_global","commands":["alias1","alias2","help","list"]},{"id":"descriptor","commands":["descriptor:command1","descriptor:command2"]}]} +{"commands":[{"name":"help","usage":"help [--xml] [--format=\"...\"] [--raw] [command_name]","description":"Displays help for a command","help":"The help<\/info> command displays help for a given command:\n\n php app\/console help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n php app\/console help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.","aliases":[],"definition":{"arguments":{"command_name":{"name":"command_name","is_required":false,"is_array":false,"description":"The command name","default":"help"}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output help as XML","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output help in other formats","default":"txt"},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command help","default":false},"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}},{"name":"list","usage":"list [--xml] [--raw] [--format=\"...\"] [namespace]","description":"Lists commands","help":"The list<\/info> command lists all commands:\n\n php app\/console list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n php app\/console list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n php app\/console list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n php app\/console list --raw<\/info>","aliases":[],"definition":{"arguments":{"namespace":{"name":"namespace","is_required":false,"is_array":false,"description":"The namespace name","default":null}},"options":{"xml":{"name":"--xml","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output list as XML","default":false},"raw":{"name":"--raw","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"To output raw command list","default":false},"format":{"name":"--format","shortcut":"","accept_value":true,"is_value_required":true,"is_multiple":false,"description":"To output list in other formats","default":"txt"}}}},{"name":"descriptor:command1","usage":"descriptor:command1","description":"command 1 description","help":"command 1 help","aliases":["alias1","alias2"],"definition":{"arguments":[],"options":{"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}},{"name":"descriptor:command2","usage":"descriptor:command2 [-o|--option_name] argument_name","description":"command 2 description","help":"command 2 help","aliases":[],"definition":{"arguments":{"argument_name":{"name":"argument_name","is_required":true,"is_array":false,"description":"","default":null}},"options":{"option_name":{"name":"--option_name","shortcut":"-o","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"","default":false},"help":{"name":"--help","shortcut":"-h","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this help message.","default":false},"quiet":{"name":"--quiet","shortcut":"-q","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not output any message.","default":false},"verbose":{"name":"--verbose","shortcut":"-v|-vv|-vvv","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug","default":false},"version":{"name":"--version","shortcut":"-V","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Display this application version.","default":false},"ansi":{"name":"--ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Force ANSI output.","default":false},"no-ansi":{"name":"--no-ansi","shortcut":"","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Disable ANSI output.","default":false},"no-interaction":{"name":"--no-interaction","shortcut":"-n","accept_value":false,"is_value_required":false,"is_multiple":false,"description":"Do not ask any interactive question.","default":false}}}}],"namespaces":[{"id":"_global","commands":["alias1","alias2","help","list"]},{"id":"descriptor","commands":["descriptor:command1","descriptor:command2"]}]} diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.md b/src/Symfony/Component/Console/Tests/Fixtures/application_2.md index bb242b7362785..e52ecd3fc44c4 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.md +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.md @@ -58,7 +58,7 @@ To display the list of available commands, please use the list comm * Is value required: yes * Is multiple: no * Description: To output help in other formats -* Default: `NULL` +* Default: `'txt'` **raw:** @@ -203,7 +203,7 @@ It's also possible to get raw list of commands (useful for embedding command run * Is value required: yes * Is multiple: no * Description: To output list in other formats -* Default: `NULL` +* Default: `'txt'` descriptor:command1 ------------------- diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml b/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml index 9aa77bbeec098..9fc3a216f0654 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_2.xml @@ -1,5 +1,5 @@ - + help [--xml] [--format="..."] [--raw] [command_name] @@ -28,7 +28,9 @@ diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_astext1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_astext1.txt index 8e5a162504034..dca37e2edaebf 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_astext1.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_astext1.txt @@ -17,4 +17,4 @@ help Displays help for a command list Lists commands foo - foo:bar The foo:bar command \ No newline at end of file + foo:bar The foo:bar command diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_astext2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_astext2.txt index d27af91e4b5af..827c406ba7a58 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_astext2.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_astext2.txt @@ -13,4 +13,4 @@ --no-interaction -n Do not ask any interactive question. Available commands for the "foo" namespace: - foo:bar The foo:bar command \ No newline at end of file + foo:bar The foo:bar command diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt index 2eb3c3088a031..94a68638f56a6 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_asxml1.txt @@ -28,7 +28,9 @@ diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt index b9dab9a7e5b4f..33c24427d8fa4 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_run2.txt @@ -7,7 +7,7 @@ Arguments: Options: --xml To output help as XML - --format To output help in other formats + --format To output help in other formats (default: "txt") --raw To output raw command help --help (-h) Display this help message. --quiet (-q) Do not output any message. @@ -27,4 +27,3 @@ Help: php app/console help --format=xml list To display the list of available commands, please use the list command. - diff --git a/src/Symfony/Component/Console/Tests/Fixtures/application_run3.txt b/src/Symfony/Component/Console/Tests/Fixtures/application_run3.txt index 68bf516ded16a..01397755054e4 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/application_run3.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/application_run3.txt @@ -7,7 +7,7 @@ Arguments: Options: --xml To output list as XML --raw To output raw command list - --format To output list in other formats + --format To output list in other formats (default: "txt") Help: The list command lists all commands: @@ -25,4 +25,3 @@ Help: It's also possible to get raw list of commands (useful for embedding command runner): php app/console list --raw - diff --git a/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php index 118cc44dbb8bc..5364f38cab689 100644 --- a/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; @@ -156,6 +157,18 @@ public function testAskAndValidate() } } + public function testNoInteraction() + { + $dialog = new DialogHelper(); + + $input = new ArrayInput(array()); + $input->setInteractive(false); + + $dialog->setInput($input); + + $this->assertEquals('not yet', $dialog->ask($this->getOutputStream(), 'Do you have a job?', 'not yet')); + } + protected function getInputStream($input) { $stream = fopen('php://memory', 'r+', false); diff --git a/src/Symfony/Component/Console/Tests/Helper/HelperSetTest.php b/src/Symfony/Component/Console/Tests/Helper/HelperSetTest.php index c9832736a2e5e..a6ccde2d236a6 100644 --- a/src/Symfony/Component/Console/Tests/Helper/HelperSetTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/HelperSetTest.php @@ -111,6 +111,23 @@ public function testGetCommand() $this->assertEquals($cmd, $helperset->getCommand(), '->getCommand() retrieves stored command'); } + /** + * @covers \Symfony\Component\Console\Helper\HelperSet::getIterator + */ + public function testIteration() + { + $helperset = new HelperSet(); + $helperset->set($this->getGenericMockHelper('fake_helper_01', $helperset)); + $helperset->set($this->getGenericMockHelper('fake_helper_02', $helperset)); + + $helpers = array('fake_helper_01', 'fake_helper_02'); + $i = 0; + + foreach ($helperset as $helper) { + $this->assertEquals($helpers[$i++], $helper->getName()); + } + } + /** * Create a generic mock for the helper interface. Optionally check for a call to setHelperSet with a specific * helperset instance. diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php index 266cc3d1c380c..88adf69c56c83 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressHelperTest.php @@ -166,6 +166,20 @@ public function testMultiByteSupport() $this->assertEquals($this->generateOutput(' 3 [■■■>------------------------]'), stream_get_contents($output->getStream())); } + public function testClear() + { + $progress = new ProgressHelper(); + $progress->start($output = $this->getOutputStream(), 50); + $progress->setCurrent(25); + $progress->clear(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 25/50 [==============>-------------] 50%') . $this->generateOutput(''), + stream_get_contents($output->getStream()) + ); + } + public function testPercentNotHundredBeforeComplete() { $progress = new ProgressHelper(); @@ -178,9 +192,19 @@ public function testPercentNotHundredBeforeComplete() $this->assertEquals($this->generateOutput(' 0/200 [>---------------------------] 0%').$this->generateOutput(' 199/200 [===========================>] 99%').$this->generateOutput(' 200/200 [============================] 100%'), stream_get_contents($output->getStream())); } - protected function getOutputStream() + public function testNonDecoratedOutput() + { + $progress = new ProgressHelper(); + $progress->start($output = $this->getOutputStream(false)); + $progress->advance(); + + rewind($output->getStream()); + $this->assertEquals('', stream_get_contents($output->getStream())); + } + + protected function getOutputStream($decorated = true) { - return new StreamOutput(fopen('php://memory', 'r+', false)); + return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated); } protected $lastMessagesLength; diff --git a/src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php index 7a9eb40819778..f3cda0dabf9c1 100644 --- a/src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php @@ -81,15 +81,17 @@ public function testRenderAddRowsOneByOne($headers, $rows, $layout, $expected) public function testRenderProvider() { + $books = array( + array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'), + array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'), + array('960-425-059-0', 'The Lord of the Rings', 'J. R. R. Tolkien'), + array('80-902734-1-6', 'And Then There Were None', 'Agatha Christie'), + ); + return array( array( array('ISBN', 'Title', 'Author'), - array( - array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'), - array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'), - array('960-425-059-0', 'The Lord of the Rings', 'J. R. R. Tolkien'), - array('80-902734-1-6', 'And Then There Were None', 'Agatha Christie'), - ), + $books, TableHelper::LAYOUT_DEFAULT, <<setVerbosity(Output::VERBOSITY_QUIET); $this->assertEquals(Output::VERBOSITY_QUIET, $output->getVerbosity(), '->setVerbosity() sets the verbosity'); + + $this->assertTrue($output->isQuiet()); + $this->assertFalse($output->isVerbose()); + $this->assertFalse($output->isVeryVerbose()); + $this->assertFalse($output->isDebug()); + + $output->setVerbosity(Output::VERBOSITY_NORMAL); + $this->assertFalse($output->isQuiet()); + $this->assertFalse($output->isVerbose()); + $this->assertFalse($output->isVeryVerbose()); + $this->assertFalse($output->isDebug()); + + $output->setVerbosity(Output::VERBOSITY_VERBOSE); + $this->assertFalse($output->isQuiet()); + $this->assertTrue($output->isVerbose()); + $this->assertFalse($output->isVeryVerbose()); + $this->assertFalse($output->isDebug()); + + $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE); + $this->assertFalse($output->isQuiet()); + $this->assertTrue($output->isVerbose()); + $this->assertTrue($output->isVeryVerbose()); + $this->assertFalse($output->isDebug()); + + $output->setVerbosity(Output::VERBOSITY_DEBUG); + $this->assertFalse($output->isQuiet()); + $this->assertTrue($output->isVerbose()); + $this->assertTrue($output->isVeryVerbose()); + $this->assertTrue($output->isDebug()); } public function testWriteWithVerbosityQuiet() diff --git a/src/Symfony/Component/Console/Tests/Tester/ApplicationTesterTest.php b/src/Symfony/Component/Console/Tests/Tester/ApplicationTesterTest.php index 6ce30ab0fc4d7..a8389dd1866b6 100644 --- a/src/Symfony/Component/Console/Tests/Tester/ApplicationTesterTest.php +++ b/src/Symfony/Component/Console/Tests/Tester/ApplicationTesterTest.php @@ -61,4 +61,9 @@ public function testGetDisplay() { $this->assertEquals('foo'.PHP_EOL, $this->tester->getDisplay(), '->getDisplay() returns the display of the last execution'); } + + public function testGetStatusCode() + { + $this->assertSame(0, $this->tester->getStatusCode(), '->getStatusCode() returns the status code'); + } } diff --git a/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php b/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php index da5bf84e22c67..b54c00e83dc85 100644 --- a/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php +++ b/src/Symfony/Component/Console/Tests/Tester/CommandTesterTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Tester; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\Output; use Symfony\Component\Console\Tester\CommandTester; @@ -59,4 +60,25 @@ public function testGetDisplay() { $this->assertEquals('foo'.PHP_EOL, $this->tester->getDisplay(), '->getDisplay() returns the display of the last execution'); } + + public function testGetStatusCode() + { + $this->assertSame(0, $this->tester->getStatusCode(), '->getStatusCode() returns the status code'); + } + + public function testCommandFromApplication() + { + $application = new Application(); + $application->setAutoExit(false); + + $command = new Command('foo'); + $command->setCode(function ($input, $output) { $output->writeln('foo'); }); + + $application->add($command); + + $tester = new CommandTester($application->find('foo')); + + // check that there is no need to pass the command name here + $this->assertEquals(0, $tester->execute(array())); + } } diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index 472b4f23c2c7c..82d721c184407 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/CssSelector/Parser/Reader.php b/src/Symfony/Component/CssSelector/Parser/Reader.php index 8bd8ed66988ac..2a6c4bbd43964 100644 --- a/src/Symfony/Component/CssSelector/Parser/Reader.php +++ b/src/Symfony/Component/CssSelector/Parser/Reader.php @@ -34,7 +34,7 @@ class Reader /** * @var int */ - private $position; + private $position = 0; /** * @param string $source @@ -43,7 +43,6 @@ public function __construct($source) { $this->source = $source; $this->length = strlen($source); - $this->position = 0; } /** diff --git a/src/Symfony/Component/CssSelector/composer.json b/src/Symfony/Component/CssSelector/composer.json index 12d8c8ee5facf..5b4231d794757 100644 --- a/src/Symfony/Component/CssSelector/composer.json +++ b/src/Symfony/Component/CssSelector/composer.json @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index 2ad5ce695c365..d20fde043b7ab 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +2.5.0 +----- + +* added UndefinedMethodFatalErrorHandler + +2.4.0 +----- + + * added a DebugClassLoader able to wrap any autoloader providing a findFile method + * improved error messages for not found classes and functions + 2.3.0 ----- diff --git a/src/Symfony/Component/Debug/Debug.php b/src/Symfony/Component/Debug/Debug.php index 2e36805ca3c6a..c3fa48b50d2c2 100644 --- a/src/Symfony/Component/Debug/Debug.php +++ b/src/Symfony/Component/Debug/Debug.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Debug; -use Symfony\Component\ClassLoader\DebugClassLoader; - /** * Registers all the debug tools. * @@ -51,8 +49,6 @@ public static function enable($errorReportingLevel = null, $displayErrors = true ini_set('display_errors', 1); } - if (class_exists('Symfony\Component\ClassLoader\DebugClassLoader')) { - DebugClassLoader::enable(); - } + DebugClassLoader::enable(); } } diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php new file mode 100644 index 0000000000000..a0c6c5a4f965a --- /dev/null +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug; + +/** + * Autoloader checking if the class is really defined in the file found. + * + * The ClassLoader will wrap all registered autoloaders providing a + * findFile method and will throw an exception if a file is found but does + * not declare the class. + * + * @author Fabien Potencier + * @author Christophe Coevoet + * + * @api + */ +class DebugClassLoader +{ + private $classFinder; + + /** + * Constructor. + * + * @param object $classFinder + * + * @api + */ + public function __construct($classFinder) + { + $this->classFinder = $classFinder; + } + + /** + * Gets the wrapped class loader. + * + * @return object a class loader instance + */ + public function getClassLoader() + { + return $this->classFinder; + } + + /** + * Replaces all autoloaders implementing a findFile method by a DebugClassLoader wrapper. + */ + public static function enable() + { + if (!is_array($functions = spl_autoload_functions())) { + return; + } + + foreach ($functions as $function) { + spl_autoload_unregister($function); + } + + foreach ($functions as $function) { + if (is_array($function) && !$function[0] instanceof self && method_exists($function[0], 'findFile')) { + $function = array(new static($function[0]), 'loadClass'); + } + + spl_autoload_register($function); + } + } + + /** + * Disables the wrapping. + */ + public static function disable() + { + if (!is_array($functions = spl_autoload_functions())) { + return; + } + + foreach ($functions as $function) { + spl_autoload_unregister($function); + } + + foreach ($functions as $function) { + if (is_array($function) && $function[0] instanceof self) { + $function[0] = $function[0]->getClassLoader(); + } + + spl_autoload_register($function); + } + } + + /** + * Finds a file by class name + * + * @param string $class A class name to resolve to file + * + * @return string|null + */ + public function findFile($class) + { + return $this->classFinder->findFile($class); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * + * @return Boolean|null True, if loaded + * + * @throws \RuntimeException + */ + public function loadClass($class) + { + if ($file = $this->classFinder->findFile($class)) { + require $file; + + if (!class_exists($class, false) && !interface_exists($class, false) && (!function_exists('trait_exists') || !trait_exists($class, false))) { + if (false !== strpos($class, '/')) { + throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); + } + + throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); + } + + return true; + } + } +} diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 09eba3f28da8a..dd1bc60307a38 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Debug; -use Symfony\Component\Debug\Exception\FatalErrorException; +use Psr\Log\LoggerInterface; use Symfony\Component\Debug\Exception\ContextErrorException; +use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\DummyException; -use Psr\Log\LoggerInterface; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; +use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface; /** * ErrorHandler. @@ -56,8 +60,8 @@ class ErrorHandler /** * Registers the error handler. * - * @param integer $level The level at which the conversion to Exception is done (null to use the error_reporting() value and 0 to disable) - * @param Boolean $displayErrors Display errors (for dev environment) or just log they (production usage) + * @param integer $level The level at which the conversion to Exception is done (null to use the error_reporting() value and 0 to disable) + * @param Boolean $displayErrors Display errors (for dev environment) or just log them (production usage) * * @return ErrorHandler The registered error handler */ @@ -75,16 +79,32 @@ public static function register($level = null, $displayErrors = true) return $handler; } + /** + * Sets the level at which the conversion to Exception is done. + * + * @param integer|null $level The level (null to use the error_reporting() value and 0 to disable) + */ public function setLevel($level) { $this->level = null === $level ? error_reporting() : $level; } + /** + * Sets the display_errors flag value. + * + * @param integer $displayErrors The display_errors flag value + */ public function setDisplayErrors($displayErrors) { $this->displayErrors = $displayErrors; } + /** + * Sets a logger for the given channel. + * + * @param LoggerInterface $logger A logger interface + * @param string $channel The channel associated with the logger (deprecation or emergency) + */ public static function setLogger(LoggerInterface $logger, $channel = 'deprecation') { self::$loggers[$channel] = $logger; @@ -190,10 +210,38 @@ public function handleFatal() restore_exception_handler(); if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) { - $level = isset($this->levels[$type]) ? $this->levels[$type] : $type; - $message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); - $exception = new FatalErrorException($message, 0, $type, $error['file'], $error['line']); - $exceptionHandler[0]->handle($exception); + $this->handleFatalError($exceptionHandler[0], $error); } } + + /** + * Gets the fatal error handlers. + * + * Override this method if you want to define more fatal error handlers. + * + * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface + */ + protected function getFatalErrorHandlers() + { + return array( + new UndefinedFunctionFatalErrorHandler(), + new UndefinedMethodFatalErrorHandler(), + new ClassNotFoundFatalErrorHandler(), + ); + } + + private function handleFatalError(ExceptionHandler $exceptionHandler, array $error) + { + $level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type']; + $message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); + $exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line']); + + foreach ($this->getFatalErrorHandlers() as $handler) { + if ($ex = $handler->handleError($error, $exception)) { + return $exceptionHandler->handle($ex); + } + } + + $exceptionHandler->handle($exception); + } } diff --git a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php new file mode 100644 index 0000000000000..94169b45a2075 --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Exception; + +/** + * Class (or Trait or Interface) Not Found Exception. + * + * @author Konstanton Myakshin + */ +class ClassNotFoundException extends FatalErrorException +{ + public function __construct($message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + $previous->getPrevious() + ); + } +} diff --git a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php new file mode 100644 index 0000000000000..572c8b30a1a7b --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Exception; + +/** + * Undefined Function Exception. + * + * @author Konstanton Myakshin + */ +class UndefinedFunctionException extends FatalErrorException +{ + public function __construct($message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + $previous->getPrevious() + ); + } +} diff --git a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php new file mode 100644 index 0000000000000..d84031b4429e9 --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Exception; + +/** + * Undefined Method Exception. + * + * @author Grégoire Pineau + */ +class UndefinedMethodException extends FatalErrorException +{ + public function __construct($message, \ErrorException $previous) + { + parent::__construct($message, $previous->getCode(), $previous->getSeverity(), $previous->getFile(), $previous->getLine(), $previous->getPrevious()); + } +} diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php new file mode 100644 index 0000000000000..98418b0497888 --- /dev/null +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\ClassNotFoundException; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\DebugClassLoader; +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use Symfony\Component\ClassLoader as SymfonyClassLoader; + +/** + * ErrorHandler for classes that do not exist. + * + * @author Fabien Potencier + */ +class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = strlen($error['message']); + $notFoundSuffix = '\' not found'; + $notFoundSuffixLen = strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + foreach (array('class', 'interface', 'trait') as $typeName) { + $prefix = ucfirst($typeName).' \''; + $prefixLen = strlen($prefix); + if (0 !== strpos($error['message'], $prefix)) { + continue; + } + + $fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { + $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); + $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); + $message = sprintf( + 'Attempted to load %s "%s" from namespace "%s" in %s line %d. Do you need to "use" it from another namespace?', + $typeName, + $className, + $namespacePrefix, + $error['file'], + $error['line'] + ); + } else { + $className = $fullyQualifiedClassName; + $message = sprintf( + 'Attempted to load %s "%s" from the global namespace in %s line %d. Did you forget a use statement for this %s?', + $typeName, + $className, + $error['file'], + $error['line'], + $typeName + ); + } + + if ($classes = $this->getClassCandidates($className)) { + $message .= sprintf(' Perhaps you need to add a use statement for one of the following: %s.', implode(', ', $classes)); + } + + return new ClassNotFoundException($message, $exception); + } + } + + /** + * Tries to guess the full namespace for a given class name. + * + * By default, it looks for PSR-0 classes registered via a Symfony or a Composer + * autoloader (that should cover all common cases). + * + * @param string $class A class name (without its namespace) + * + * @return array An array of possible fully qualified class names + */ + private function getClassCandidates($class) + { + if (!is_array($functions = spl_autoload_functions())) { + return array(); + } + + // find Symfony and Composer autoloaders + $classes = array(); + foreach ($functions as $function) { + if (!is_array($function)) { + continue; + } + + // get class loaders wrapped by DebugClassLoader + if ($function[0] instanceof DebugClassLoader && method_exists($function[0], 'getClassLoader')) { + $function[0] = $function[0]->getClassLoader(); + } + + if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) { + foreach ($function[0]->getPrefixes() as $paths) { + foreach ($paths as $path) { + $classes = array_merge($classes, $this->findClassInPath($path, $class)); + } + } + } + } + + return $classes; + } + + /** + * @param string $path + * @param string $class + * + * @return array + */ + private function findClassInPath($path, $class) + { + if (!$path = realpath($path)) { + return array(); + } + + $classes = array(); + $filename = $class.'.php'; + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName())) { + $classes[] = $class; + } + } + + return $classes; + } + + /** + * @param string $path + * @param string $file + * + * @return string|null + */ + private function convertFileToClass($path, $file) + { + $namespacedClass = str_replace(array($path.'/', '.php', '/'), array('', '', '\\'), $file); + $pearClass = str_replace('\\', '_', $namespacedClass); + + // We cannot use the autoloader here as most of them use require; but if the class + // is not found, the new autoloader call will require the file again leading to a + // "cannot redeclare class" error. + if (!$this->classExists($namespacedClass) && !$this->classExists($pearClass)) { + require_once $file; + } + + if ($this->classExists($namespacedClass)) { + return $namespacedClass; + } + + if ($this->classExists($pearClass)) { + return $pearClass; + } + } + + /** + * @param string $class + * + * @return Boolean + */ + private function classExists($class) + { + return class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false)); + } +} diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php new file mode 100644 index 0000000000000..6b87eb30a126e --- /dev/null +++ b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\FatalErrorException; + +/** + * Attempts to convert fatal errors to exceptions. + * + * @author Fabien Potencier + */ +interface FatalErrorHandlerInterface +{ + /** + * Attempts to convert an error into an exception. + * + * @param array $error An array as returned by error_get_last() + * @param FatalErrorException $exception A FatalErrorException instance + * + * @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise + */ + public function handleError(array $error, FatalErrorException $exception); +} diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php new file mode 100644 index 0000000000000..4b6fc905c9e25 --- /dev/null +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.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\Component\Debug\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\UndefinedFunctionException; +use Symfony\Component\Debug\Exception\FatalErrorException; + +/** + * ErrorHandler for undefined functions. + * + * @author Fabien Potencier + */ +class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = strlen($error['message']); + $notFoundSuffix = '()'; + $notFoundSuffixLen = strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + $prefix = 'Call to undefined function '; + $prefixLen = strlen($prefix); + if (0 !== strpos($error['message'], $prefix)) { + return; + } + + $fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { + $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); + $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); + $message = sprintf( + 'Attempted to call function "%s" from namespace "%s" in %s line %d.', + $functionName, + $namespacePrefix, + $error['file'], + $error['line'] + ); + } else { + $functionName = $fullyQualifiedFunctionName; + $message = sprintf( + 'Attempted to call function "%s" from the global namespace in %s line %d.', + $functionName, + $error['file'], + $error['line'] + ); + } + + $candidates = array(); + foreach (get_defined_functions() as $type => $definedFunctionNames) { + foreach ($definedFunctionNames as $definedFunctionName) { + if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) { + $definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1); + } else { + $definedFunctionNameBasename = $definedFunctionName; + } + + if ($definedFunctionNameBasename === $functionName) { + $candidates[] = '\\'.$definedFunctionName; + } + } + } + + if ($candidates) { + $message .= ' Did you mean to call: '.implode(', ', array_map(function ($val) { + return '"'.$val.'"'; + }, $candidates)).'?'; + } + + return new UndefinedFunctionException($message, $exception); + } +} diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php new file mode 100644 index 0000000000000..ae385509bb08b --- /dev/null +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.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\Debug\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\Exception\UndefinedMethodException; + +/** + * ErrorHandler for undefined methods. + * + * @author Grégoire Pineau + */ +class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $error['message'], $matches); + if (!$matches) { + return; + } + + $className = $matches[1]; + $methodName = $matches[2]; + + $message = sprintf('Attempted to call method "%s" on class "%s" in %s line %d.', $methodName, $className, $error['file'], $error['line']); + + $candidates = array(); + foreach (get_class_methods($className) as $definedMethodName) { + $lev = levenshtein($methodName, $definedMethodName); + if ($lev <= strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) { + $candidates[] = $definedMethodName; + } + } + + if ($candidates) { + $message .= sprintf(' Did you mean to call: "%s"?', implode('", "', $candidates)); + } + + return new UndefinedMethodException($message, $exception); + } +} diff --git a/src/Symfony/Component/ClassLoader/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php similarity index 70% rename from src/Symfony/Component/ClassLoader/Tests/DebugClassLoaderTest.php rename to src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 873515c3369f5..a18e5811ccd44 100644 --- a/src/Symfony/Component/ClassLoader/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -9,10 +9,9 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ClassLoader\Tests; +namespace Symfony\Component\Debug\Tests; -use Symfony\Component\ClassLoader\ClassLoader; -use Symfony\Component\ClassLoader\DebugClassLoader; +use Symfony\Component\Debug\DebugClassLoader; class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase { @@ -41,12 +40,27 @@ public function testIdempotence() $reflProp = $reflClass->getProperty('classFinder'); $reflProp->setAccessible(true); - $this->assertNotInstanceOf('Symfony\Component\ClassLoader\DebugClassLoader', $reflProp->getValue($function[0])); + $this->assertNotInstanceOf('Symfony\Component\Debug\DebugClassLoader', $reflProp->getValue($function[0])); + + DebugClassLoader::disable(); return; } } - throw new \Exception('DebugClassLoader did not register'); + DebugClassLoader::disable(); + + $this->fail('DebugClassLoader did not register'); + } +} + +class ClassLoader +{ + public function loadClass($class) + { + } + + public function findFile($class) + { } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 287eb44335aae..0ffd0ab32ece3 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -92,6 +92,11 @@ class _CompileTimeError extends _BaseCompileTimeError { function foo($invalid) { ); } catch (DummyException $e) { // if an exception is thrown, the test passed + } catch (\Exception $e) { + restore_error_handler(); + restore_exception_handler(); + + throw $e; } restore_error_handler(); @@ -107,7 +112,7 @@ public function testNotice() $exceptionCheck = function ($exception) use ($that) { $that->assertInstanceOf('Symfony\Component\Debug\Exception\ContextErrorException', $exception); $that->assertEquals(E_NOTICE, $exception->getSeverity()); - $that->assertEquals(__LINE__ + 40, $exception->getLine()); + $that->assertEquals(__LINE__ + 44, $exception->getLine()); $that->assertEquals(__FILE__, $exception->getFile()); $that->assertRegexp('/^Notice: Undefined variable: (foo|bar)/', $exception->getMessage()); $that->assertArrayHasKey('foobar', $exception->getContext()); @@ -137,6 +142,10 @@ public function testNotice() self::triggerNotice($this); } catch (DummyException $e) { // if an exception is thrown, the test passed + } catch (\Exception $e) { + restore_error_handler(); + + throw $e; } restore_error_handler(); @@ -152,71 +161,134 @@ private static function triggerNotice($that) public function testConstruct() { - $handler = ErrorHandler::register(3); + try { + $handler = ErrorHandler::register(3); - $level = new \ReflectionProperty($handler, 'level'); - $level->setAccessible(true); + $level = new \ReflectionProperty($handler, 'level'); + $level->setAccessible(true); - $this->assertEquals(3, $level->getValue($handler)); + $this->assertEquals(3, $level->getValue($handler)); - restore_error_handler(); + restore_error_handler(); + } catch (\Exception $e) { + restore_error_handler(); + + throw $e; + } } public function testHandle() { - $handler = ErrorHandler::register(0); - $this->assertFalse($handler->handle(0, 'foo', 'foo.php', 12, 'foo')); + try { + $handler = ErrorHandler::register(0); + $this->assertFalse($handler->handle(0, 'foo', 'foo.php', 12, 'foo')); - restore_error_handler(); + restore_error_handler(); - $handler = ErrorHandler::register(3); - $this->assertFalse($handler->handle(4, 'foo', 'foo.php', 12, 'foo')); + $handler = ErrorHandler::register(3); + $this->assertFalse($handler->handle(4, 'foo', 'foo.php', 12, 'foo')); - restore_error_handler(); + restore_error_handler(); - $handler = ErrorHandler::register(3); - try { - $handler->handle(111, 'foo', 'foo.php', 12, 'foo'); - } catch (\ErrorException $e) { - $this->assertSame('111: foo in foo.php line 12', $e->getMessage()); - $this->assertSame(111, $e->getSeverity()); - $this->assertSame('foo.php', $e->getFile()); - $this->assertSame(12, $e->getLine()); - } + $handler = ErrorHandler::register(3); + try { + $handler->handle(111, 'foo', 'foo.php', 12, 'foo'); + } catch (\ErrorException $e) { + $this->assertSame('111: foo in foo.php line 12', $e->getMessage()); + $this->assertSame(111, $e->getSeverity()); + $this->assertSame('foo.php', $e->getFile()); + $this->assertSame(12, $e->getLine()); + } - restore_error_handler(); + restore_error_handler(); - $handler = ErrorHandler::register(E_USER_DEPRECATED); - $this->assertTrue($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, 'foo')); + $handler = ErrorHandler::register(E_USER_DEPRECATED); + $this->assertTrue($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, 'foo')); - restore_error_handler(); + restore_error_handler(); - $handler = ErrorHandler::register(E_DEPRECATED); - $this->assertTrue($handler->handle(E_DEPRECATED, 'foo', 'foo.php', 12, 'foo')); + $handler = ErrorHandler::register(E_DEPRECATED); + $this->assertTrue($handler->handle(E_DEPRECATED, 'foo', 'foo.php', 12, 'foo')); - restore_error_handler(); + restore_error_handler(); - $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger = $this->getMock('Psr\Log\LoggerInterface'); - $that = $this; - $warnArgCheck = function ($message, $context) use ($that) { - $that->assertEquals('foo', $message); - $that->assertArrayHasKey('type', $context); - $that->assertEquals($context['type'], ErrorHandler::TYPE_DEPRECATION); - $that->assertArrayHasKey('stack', $context); - $that->assertInternalType('array', $context['stack']); - }; + $that = $this; + $warnArgCheck = function ($message, $context) use ($that) { + $that->assertEquals('foo', $message); + $that->assertArrayHasKey('type', $context); + $that->assertEquals($context['type'], ErrorHandler::TYPE_DEPRECATION); + $that->assertArrayHasKey('stack', $context); + $that->assertInternalType('array', $context['stack']); + }; - $logger - ->expects($this->once()) - ->method('warning') - ->will($this->returnCallback($warnArgCheck)) - ; + $logger + ->expects($this->once()) + ->method('warning') + ->will($this->returnCallback($warnArgCheck)) + ; - $handler = ErrorHandler::register(E_USER_DEPRECATED); - $handler->setLogger($logger); - $handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, 'foo'); + $handler = ErrorHandler::register(E_USER_DEPRECATED); + $handler->setLogger($logger); + $handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, 'foo'); - restore_error_handler(); + restore_error_handler(); + } catch (\Exception $e) { + restore_error_handler(); + + throw $e; + } } + + /** + * @dataProvider provideFatalErrorHandlersData + */ + public function testFatalErrorHandlers($error, $class, $translatedMessage) + { + $handler = new ErrorHandler(); + $exceptionHandler = new MockExceptionHandler(); + + $m = new \ReflectionMethod($handler, 'handleFatalError'); + $m->setAccessible(true); + $m->invoke($handler, $exceptionHandler, $error); + + $this->assertInstanceof($class, $exceptionHandler->e); + $this->assertSame($translatedMessage, $exceptionHandler->e->getMessage()); + $this->assertSame($error['type'], $exceptionHandler->e->getSeverity()); + $this->assertSame($error['file'], $exceptionHandler->e->getFile()); + $this->assertSame($error['line'], $exceptionHandler->e->getLine()); + } + + public function provideFatalErrorHandlersData() + { + return array( + // undefined function + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function test_namespaced_function_again()', + ), + 'Symfony\Component\Debug\Exception\UndefinedFunctionException', + 'Attempted to call function "test_namespaced_function_again" from the global namespace in foo.php line 12. Did you mean to call: "\\symfony\\component\\debug\\tests\\test_namespaced_function_again"?', + ), + // class not found + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'WhizBangFactory\' not found', + ), + 'Symfony\Component\Debug\Exception\ClassNotFoundException', + 'Attempted to load class "WhizBangFactory" from the global namespace in foo.php line 12. Did you forget a use statement for this class?', + ), + ); + } +} + +function test_namespaced_function_again() +{ } diff --git a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php index f187e2d09929d..8b73fcf11d2a3 100644 --- a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php @@ -17,13 +17,6 @@ class ExceptionHandlerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testDebug() { $handler = new ExceptionHandler(false); diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php new file mode 100644 index 0000000000000..be6c74af9c1d0 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; + +class ClassNotFoundFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider provideClassNotFoundData + */ + public function testClassNotFound($error, $translatedMessage) + { + $handler = new ClassNotFoundFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceof('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception); + $this->assertSame($translatedMessage, $exception->getMessage()); + $this->assertSame($error['type'], $exception->getSeverity()); + $this->assertSame($error['file'], $exception->getFile()); + $this->assertSame($error['line'], $exception->getLine()); + } + + public function provideClassNotFoundData() + { + return array( + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'WhizBangFactory\' not found', + ), + 'Attempted to load class "WhizBangFactory" from the global namespace in foo.php line 12. Did you forget a use statement for this class?', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\WhizBangFactory\' not found', + ), + 'Attempted to load class "WhizBangFactory" from namespace "Foo\\Bar" in foo.php line 12. Do you need to "use" it from another namespace?', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'UndefinedFunctionException\' not found', + ), + 'Attempted to load class "UndefinedFunctionException" from the global namespace in foo.php line 12. Did you forget a use statement for this class? Perhaps you need to add a use statement for one of the following: Symfony\Component\Debug\Exception\UndefinedFunctionException.', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'PEARClass\' not found', + ), + 'Attempted to load class "PEARClass" from the global namespace in foo.php line 12. Did you forget a use statement for this class? Perhaps you need to add a use statement for one of the following: Symfony_Component_Debug_Tests_Fixtures_PEARClass.', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', + ), + 'Attempted to load class "UndefinedFunctionException" from namespace "Foo\Bar" in foo.php line 12. Do you need to "use" it from another namespace? Perhaps you need to add a use statement for one of the following: Symfony\Component\Debug\Exception\UndefinedFunctionException.', + ), + ); + } + + public function testCannotRedeclareClass() + { + if (!file_exists(__DIR__.'/../FIXTURES/REQUIREDTWICE.PHP')) { + $this->markTestSkipped('Can only be run on case insensitive filesystems'); + } + + require_once __DIR__.'/../FIXTURES/REQUIREDTWICE.PHP'; + + $error = array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\RequiredTwice\' not found', + ); + + $handler = new ClassNotFoundFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceof('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception); + } +} diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php new file mode 100644 index 0000000000000..511f2040543e8 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; + +class UndefinedFunctionFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider provideUndefinedFunctionData + */ + public function testUndefinedFunction($error, $translatedMessage) + { + $handler = new UndefinedFunctionFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceof('Symfony\Component\Debug\Exception\UndefinedFunctionException', $exception); + $this->assertSame($translatedMessage, $exception->getMessage()); + $this->assertSame($error['type'], $exception->getSeverity()); + $this->assertSame($error['file'], $exception->getFile()); + $this->assertSame($error['line'], $exception->getLine()); + } + + public function provideUndefinedFunctionData() + { + return array( + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function test_namespaced_function()', + ), + 'Attempted to call function "test_namespaced_function" from the global namespace in foo.php line 12. Did you mean to call: "\\symfony\\component\\debug\\tests\\fatalerrorhandler\\test_namespaced_function"?', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function Foo\\Bar\\Baz\\test_namespaced_function()', + ), + 'Attempted to call function "test_namespaced_function" from namespace "Foo\\Bar\\Baz" in foo.php line 12. Did you mean to call: "\\symfony\\component\\debug\\tests\\fatalerrorhandler\\test_namespaced_function"?', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function foo()', + ), + 'Attempted to call function "foo" from the global namespace in foo.php line 12.', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined function Foo\\Bar\\Baz\\foo()', + ), + 'Attempted to call function "foo" from namespace "Foo\Bar\Baz" in foo.php line 12.', + ), + ); + } +} + +function test_namespaced_function() +{ +} diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php new file mode 100644 index 0000000000000..3e3ee41fa969e --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; + +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; + +class UndefinedMethodFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider provideUndefinedMethodData + */ + public function testUndefinedMethod($error, $translatedMessage) + { + $handler = new UndefinedMethodFatalErrorHandler(); + $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); + + $this->assertInstanceof('Symfony\Component\Debug\Exception\UndefinedMethodException', $exception); + $this->assertSame($translatedMessage, $exception->getMessage()); + $this->assertSame($error['type'], $exception->getSeverity()); + $this->assertSame($error['file'], $exception->getFile()); + $this->assertSame($error['line'], $exception->getLine()); + } + + public function provideUndefinedMethodData() + { + return array( + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined method SplObjectStorage::what()', + ), + 'Attempted to call method "what" on class "SplObjectStorage" in foo.php line 12.', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined method SplObjectStorage::walid()', + ), + 'Attempted to call method "walid" on class "SplObjectStorage" in foo.php line 12. Did you mean to call: "valid"?', + ), + array( + array( + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Call to undefined method SplObjectStorage::offsetFet()', + ), + 'Attempted to call method "offsetFet" on class "SplObjectStorage" in foo.php line 12. Did you mean to call: "offsetSet", "offsetUnset", "offsetGet"?', + ), + ); + } +} diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php b/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php new file mode 100644 index 0000000000000..39f228182e0fa --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php @@ -0,0 +1,5 @@ +markTestSkipped('Twig is not available.'); - } + $this->e = $e; } } diff --git a/src/Symfony/Component/Debug/composer.json b/src/Symfony/Component/Debug/composer.json index 35b170a3c0e17..b9cd2d340a363 100644 --- a/src/Symfony/Component/Debug/composer.json +++ b/src/Symfony/Component/Debug/composer.json @@ -24,8 +24,7 @@ }, "suggest": { "symfony/http-foundation": "", - "symfony/http-kernel": "", - "symfony/class-loader": "" + "symfony/http-kernel": "" }, "autoload": { "psr-0": { "Symfony\\Component\\Debug\\": "" } @@ -34,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 4839bd8bee7b4..9a41e85705514 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +2.4.0 +----- + + * added support for expressions in service definitions + * added ContainerAwareTrait to add default container aware behavior to a class + 2.2.0 ----- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php index e536331c6e279..c8978f377f44c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php @@ -74,6 +74,17 @@ public function process(ContainerBuilder $container) $id )); } + + // tag attribute values must be scalars + foreach ($definition->getTags() as $name => $tags) { + foreach ($tags as $attributes) { + foreach ($attributes as $attribute => $value) { + if (!is_scalar($value) && null !== $value) { + throw new RuntimeException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s".', $id, $name, $attribute)); + } + } + } + } } } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php index 36d0604cf8873..4dfa9cf31cb38 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php @@ -23,7 +23,7 @@ class Compiler { private $passConfig; - private $log; + private $log = array(); private $loggingFormatter; private $serviceReferenceGraph; @@ -35,7 +35,6 @@ public function __construct() $this->passConfig = new PassConfig(); $this->serviceReferenceGraph = new ServiceReferenceGraph(); $this->loggingFormatter = new LoggingFormatter(); - $this->log = array(); } /** diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index ba1688f5e8675..44363309bc2ca 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -62,6 +62,11 @@ public function process(ContainerBuilder $container) $definition->setProperties( $this->inlineArguments($container, $definition->getProperties()) ); + + $configurator = $this->inlineArguments($container, array($definition->getConfigurator())); + $definition->setConfigurator( + $configurator[0] + ); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index e863f75640fb1..ac395db22e9a2 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -31,9 +31,9 @@ class PassConfig const TYPE_REMOVE = 'removing'; private $mergePass; - private $afterRemovingPasses; - private $beforeOptimizationPasses; - private $beforeRemovingPasses; + private $afterRemovingPasses = array(); + private $beforeOptimizationPasses = array(); + private $beforeRemovingPasses = array(); private $optimizationPasses; private $removingPasses; @@ -44,10 +44,6 @@ public function __construct() { $this->mergePass = new MergeExtensionConfigurationPass(); - $this->afterRemovingPasses = array(); - $this->beforeOptimizationPasses = array(); - $this->beforeRemovingPasses = array(); - $this->optimizationPasses = array( new ResolveDefinitionTemplatesPass(), new ResolveParameterPlaceHoldersPass(), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php index fbd33eeee16f9..1de14fa62f96b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php @@ -26,15 +26,7 @@ class ServiceReferenceGraph /** * @var ServiceReferenceGraphNode[] */ - private $nodes; - - /** - * Constructor. - */ - public function __construct() - { - $this->nodes = array(); - } + private $nodes = array(); /** * Checks if the graph has a specific node. diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php index 3fd50771d54e2..283f6de5ba0ef 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php @@ -24,8 +24,8 @@ class ServiceReferenceGraphNode { private $id; - private $inEdges; - private $outEdges; + private $inEdges = array(); + private $outEdges = array(); private $value; /** @@ -38,8 +38,6 @@ public function __construct($id, $value) { $this->id = $id; $this->value = $value; - $this->inEdges = array(); - $this->outEdges = array(); } /** diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index ecf35ee4a0550..c4d8f16f32ee6 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -67,13 +67,13 @@ class Container implements IntrospectableContainerInterface */ protected $parameterBag; - protected $services; - protected $methodMap; - protected $aliases; - protected $scopes; - protected $scopeChildren; - protected $scopedServices; - protected $scopeStacks; + protected $services = array(); + protected $methodMap = array(); + protected $aliases = array(); + protected $scopes = array(); + protected $scopeChildren = array(); + protected $scopedServices = array(); + protected $scopeStacks = array(); protected $loading = array(); /** @@ -85,14 +85,7 @@ class Container implements IntrospectableContainerInterface */ public function __construct(ParameterBagInterface $parameterBag = null) { - $this->parameterBag = null === $parameterBag ? new ParameterBag() : $parameterBag; - - $this->services = array(); - $this->aliases = array(); - $this->scopes = array(); - $this->scopeChildren = array(); - $this->scopedServices = array(); - $this->scopeStacks = array(); + $this->parameterBag = $parameterBag ?: new ParameterBag(); $this->set('service_container', $this); } diff --git a/src/Symfony/Component/DependencyInjection/ContainerAwareTrait.php b/src/Symfony/Component/DependencyInjection/ContainerAwareTrait.php new file mode 100644 index 0000000000000..57280aad60da2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ContainerAwareTrait.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection; + +/** + * ContainerAware trait. + * + * @author Fabien Potencier + */ +trait ContainerAwareTrait +{ + /** + * @var ContainerInterface + */ + protected $container; + + /** + * Sets the Container associated with this Controller. + * + * @param ContainerInterface $container A ContainerInterface instance + */ + public function setContainer(ContainerInterface $container = null) + { + $this->container = $container; + } +} diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index dc5738a5cf94d..0e07ec6b159b1 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -24,6 +24,7 @@ use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator; +use Symfony\Component\ExpressionLanguage\Expression; /** * ContainerBuilder is a DI container that provides an API to easily describe services. @@ -78,6 +79,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface */ private $proxyInstantiator; + /** + * @var ExpressionLanguage|null + */ + private $expressionLanguage; + /** * Sets the track resources flag. * @@ -305,11 +311,7 @@ public function loadFromExtension($extension, array $values = array()) */ public function addCompilerPass(CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION) { - if (null === $this->compiler) { - $this->compiler = new Compiler(); - } - - $this->compiler->addPass($pass, $type); + $this->getCompiler()->addPass($pass, $type); $this->addObjectResource($pass); @@ -325,11 +327,7 @@ public function addCompilerPass(CompilerPassInterface $pass, $type = PassConfig: */ public function getCompilerPassConfig() { - if (null === $this->compiler) { - $this->compiler = new Compiler(); - } - - return $this->compiler->getPassConfig(); + return $this->getCompiler()->getPassConfig(); } /** @@ -610,17 +608,15 @@ public function prependExtensionConfig($name, array $config) */ public function compile() { - if (null === $this->compiler) { - $this->compiler = new Compiler(); - } + $compiler = $this->getCompiler(); if ($this->trackResources) { - foreach ($this->compiler->getPassConfig()->getPasses() as $pass) { + foreach ($compiler->getPassConfig()->getPasses() as $pass) { $this->addObjectResource($pass); } } - $this->compiler->compile($this); + $compiler->compile($this); if ($this->trackResources) { foreach ($this->definitions as $definition) { @@ -995,11 +991,12 @@ public function createService(Definition $definition, $id, $tryProxy = true) } /** - * Replaces service references by the real service instance. + * Replaces service references by the real service instance and evaluates expressions. * * @param mixed $value A value * - * @return mixed The same value with all service references replaced by the real service instances + * @return mixed The same value with all service references replaced by + * the real service instances and all expressions evaluated */ public function resolveServices($value) { @@ -1011,6 +1008,8 @@ public function resolveServices($value) $value = $this->get((string) $value, $value->getInvalidBehavior()); } elseif ($value instanceof Definition) { $value = $this->createService($value, null); + } elseif ($value instanceof Expression) { + $value = $this->getExpressionLanguage()->evaluate($value, array('container' => $this)); } return $value; @@ -1161,4 +1160,16 @@ private function shareService(Definition $definition, $service, $id) } } } + + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } } diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 1168444389ef1..428fee27c3a21 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -28,24 +28,24 @@ class Definition private $factoryClass; private $factoryMethod; private $factoryService; - private $scope; - private $properties; - private $calls; + private $scope = ContainerInterface::SCOPE_CONTAINER; + private $properties = array(); + private $calls = array(); private $configurator; - private $tags; - private $public; - private $synthetic; - private $abstract; - private $synchronized; - private $lazy; + private $tags = array(); + private $public = true; + private $synthetic = false; + private $abstract = false; + private $synchronized = false; + private $lazy = false; protected $arguments; /** * Constructor. * - * @param string $class The service class - * @param array $arguments An array of arguments to pass to the service constructor + * @param string|null $class The service class + * @param array $arguments An array of arguments to pass to the service constructor * * @api */ @@ -53,15 +53,6 @@ public function __construct($class = null, array $arguments = array()) { $this->class = $class; $this->arguments = $arguments; - $this->calls = array(); - $this->scope = ContainerInterface::SCOPE_CONTAINER; - $this->tags = array(); - $this->public = true; - $this->synthetic = false; - $this->synchronized = false; - $this->lazy = false; - $this->abstract = false; - $this->properties = array(); } /** @@ -84,7 +75,7 @@ public function setFactoryClass($factoryClass) /** * Gets the factory class. * - * @return string The factory class name + * @return string|null The factory class name * * @api */ @@ -112,7 +103,7 @@ public function setFactoryMethod($factoryMethod) /** * Gets the factory method. * - * @return string The factory method name + * @return string|null The factory method name * * @api */ @@ -140,7 +131,7 @@ public function setFactoryService($factoryService) /** * Gets the factory service id. * - * @return string The factory service id + * @return string|null The factory service id * * @api */ @@ -168,7 +159,7 @@ public function setClass($class) /** * Gets the service class. * - * @return string The service class + * @return string|null The service class * * @api */ @@ -508,7 +499,7 @@ public function setFile($file) /** * Gets the file to require before creating the service. * - * @return string The full pathname to include + * @return string|null The full pathname to include * * @api */ @@ -704,7 +695,7 @@ public function setConfigurator($callable) /** * Gets the configurator to call after the service is fully initialized. * - * @return callable The PHP callable to call + * @return callable|null The PHP callable to call * * @api */ diff --git a/src/Symfony/Component/DependencyInjection/DefinitionDecorator.php b/src/Symfony/Component/DependencyInjection/DefinitionDecorator.php index 1f72b8f16b2e8..b7eed8c564d4d 100644 --- a/src/Symfony/Component/DependencyInjection/DefinitionDecorator.php +++ b/src/Symfony/Component/DependencyInjection/DefinitionDecorator.php @@ -24,7 +24,7 @@ class DefinitionDecorator extends Definition { private $parent; - private $changes; + private $changes = array(); /** * Constructor. @@ -38,7 +38,6 @@ public function __construct($parent) parent::__construct(); $this->parent = $parent; - $this->changes = array(); } /** diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 63ff7424b362f..4ca5d4d524474 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -23,6 +23,8 @@ use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper; +use Symfony\Component\DependencyInjection\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\Expression; /** * PhpDumper dumps a service container as a PHP class. @@ -51,6 +53,7 @@ class PhpDumper extends Dumper private $referenceVariables; private $variableCount; private $reservedVariables = array('instance', 'class'); + private $expressionLanguage; /** * @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface @@ -86,6 +89,7 @@ public function setProxyDumper(ProxyDumper $proxyDumper) * * * class: The class name * * base_class: The base class name + * * namespace: The class namespace * * @param array $options An array of options * @@ -98,9 +102,10 @@ public function dump(array $options = array()) $options = array_merge(array( 'class' => 'ProjectServiceContainer', 'base_class' => 'Container', + 'namespace' => '', ), $options); - $code = $this->startClass($options['class'], $options['base_class']); + $code = $this->startClass($options['class'], $options['base_class'], $options['namespace']); if ($this->container->isFrozen()) { $code .= $this->addFrozenConstructor(); @@ -154,6 +159,7 @@ private function addServiceLocalTempVariables($cId, $definition) $this->getServiceCallsFromArguments($iDefinition->getArguments(), $calls, $behavior); $this->getServiceCallsFromArguments($iDefinition->getMethodCalls(), $calls, $behavior); $this->getServiceCallsFromArguments($iDefinition->getProperties(), $calls, $behavior); + $this->getServiceCallsFromArguments(array($iDefinition->getConfigurator()), $calls, $behavior); } $code = ''; @@ -476,8 +482,15 @@ private function addServiceConfigurator($id, $definition, $variableName = 'insta } if (is_array($callable)) { - if ($callable[0] instanceof Reference) { - return sprintf(" %s->%s(\$%s);\n", $this->getServiceCall((string) $callable[0]), $callable[1], $variableName); + if ($callable[0] instanceof Reference + || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))) { + return sprintf(" %s->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName); + } + + $class = $this->dumpValue($callable[0]); + // If the class is a string we can optimize call_user_func away + if (strpos($class, "'") === 0) { + return sprintf(" %s::%s(\$%s);\n", $this->dumpLiteralClass($class), $callable[1], $variableName); } return sprintf(" call_user_func(array(%s, '%s'), \$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName); @@ -684,6 +697,13 @@ private function addNewInstance($id, Definition $definition, $return, $instantia if (null !== $definition->getFactoryMethod()) { if (null !== $definition->getFactoryClass()) { + $class = $this->dumpValue($definition->getFactoryClass()); + + // If the class is a string we can optimize call_user_func away + if (strpos($class, "'") === 0) { + return sprintf(" $return{$instantiation}%s::%s(%s);\n", $this->dumpLiteralClass($class), $definition->getFactoryMethod(), $arguments ? implode(', ', $arguments) : ''); + } + return sprintf(" $return{$instantiation}call_user_func(array(%s, '%s')%s);\n", $this->dumpValue($definition->getFactoryClass()), $definition->getFactoryMethod(), $arguments ? ', '.implode(', ', $arguments) : ''); } @@ -698,7 +718,7 @@ private function addNewInstance($id, Definition $definition, $return, $instantia return sprintf(" \$class = %s;\n\n $return{$instantiation}new \$class(%s);\n", $class, implode(', ', $arguments)); } - return sprintf(" $return{$instantiation}new \\%s(%s);\n", substr(str_replace('\\\\', '\\', $class), 1, -1), implode(', ', $arguments)); + return sprintf(" $return{$instantiation}new %s(%s);\n", $this->dumpLiteralClass($class), implode(', ', $arguments)); } /** @@ -706,16 +726,18 @@ private function addNewInstance($id, Definition $definition, $return, $instantia * * @param string $class Class name * @param string $baseClass The name of the base class + * @param string $namespace The class namespace * * @return string */ - private function startClass($class, $baseClass) + private function startClass($class, $baseClass, $namespace) { $bagClass = $this->container->isFrozen() ? 'use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;' : 'use Symfony\Component\DependencyInjection\ParameterBag\\ParameterBag;'; + $namespaceLine = $namespace ? "namespace $namespace;\n" : ''; return <<getClass(), $path.'/'.$key)); } elseif ($value instanceof Reference) { throw new InvalidArgumentException(sprintf('You cannot dump a container with parameters that contain references to other services (reference to service "%s" found in "%s").', $value, $path.'/'.$key)); + } elseif ($value instanceof Expression) { + throw new InvalidArgumentException(sprintf('You cannot dump a container with parameters that contain expressions. Expression "%s" found in "%s".', $value, $path.'/'.$key)); } else { $value = var_export($value, true); } @@ -1063,7 +1089,8 @@ private function getInlinedDefinitions(Definition $definition) $definitions = array_merge( $this->getDefinitionsFromArguments($definition->getArguments()), $this->getDefinitionsFromArguments($definition->getMethodCalls()), - $this->getDefinitionsFromArguments($definition->getProperties()) + $this->getDefinitionsFromArguments($definition->getProperties()), + $this->getDefinitionsFromArguments(array($definition->getConfigurator())) ); $this->inlinedDefinitions->offsetSet($definition, $definitions); @@ -1109,7 +1136,7 @@ private function getDefinitionsFromArguments(array $arguments) * * @return Boolean */ - private function hasReference($id, array $arguments, $deep = false, $visited = array()) + private function hasReference($id, array $arguments, $deep = false, array $visited = array()) { foreach ($arguments as $argument) { if (is_array($argument)) { @@ -1117,14 +1144,15 @@ private function hasReference($id, array $arguments, $deep = false, $visited = a return true; } } elseif ($argument instanceof Reference) { - if ($id === (string) $argument) { + $argumentId = (string) $argument; + if ($id === $argumentId) { return true; } - if ($deep && !isset($visited[(string) $argument])) { - $visited[(string) $argument] = true; + if ($deep && !isset($visited[$argumentId])) { + $visited[$argumentId] = true; - $service = $this->container->getDefinition((string) $argument); + $service = $this->container->getDefinition($argumentId); $arguments = array_merge($service->getMethodCalls(), $service->getArguments(), $service->getProperties()); if ($this->hasReference($id, $arguments, $deep, $visited)) { @@ -1196,6 +1224,8 @@ private function dumpValue($value, $interpolate = true) } return $this->getServiceCall((string) $value, $value); + } elseif ($value instanceof Expression) { + return $this->getExpressionLanguage()->compile((string) $value, array('container')); } elseif ($value instanceof Parameter) { return $this->dumpParameter($value); } elseif (true === $interpolate && is_string($value)) { @@ -1220,6 +1250,18 @@ private function dumpValue($value, $interpolate = true) } } + /** + * Dumps a string to a literal (aka PHP Code) class value. + * + * @param string $class + * + * @return string + */ + private function dumpLiteralClass($class) + { + return '\\'.substr(str_replace('\\\\', '\\', $class), 1, -1); + } + /** * Dumps a parameter * @@ -1318,4 +1360,16 @@ private function getNextVariableName() return $name; } } + + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index a311af348e348..31bec31b22456 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\ExpressionLanguage\Expression; /** * XmlDumper dumps a service container as an XML string. @@ -259,6 +260,10 @@ private function convertParameters($parameters, $type, \DOMElement $parent, $key } elseif ($value instanceof Definition) { $element->setAttribute('type', 'service'); $this->addService($value, null, $element); + } elseif ($value instanceof Expression) { + $element->setAttribute('type', 'expression'); + $text = $this->document->createTextNode(self::phpToXml((string) $value)); + $element->appendChild($text); } else { if (in_array($value, array('null', 'true', 'false'), true)) { $element->setAttribute('type', 'string'); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index 0a9d35fdaf6ce..b4748545dc2a7 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\ExpressionLanguage\Expression; /** * YamlDumper dumps a service container as a YAML string. @@ -235,6 +236,8 @@ private function dumpValue($value) return $this->getServiceCall((string) $value, $value); } elseif ($value instanceof Parameter) { return $this->getParameterCall((string) $value); + } elseif ($value instanceof Expression) { + return $this->getExpressionCall((string) $value); } elseif (is_object($value) || is_resource($value)) { throw new RuntimeException('Unable to dump a service container if a parameter is an object or a resource.'); } @@ -271,6 +274,11 @@ private function getParameterCall($id) return sprintf('%%%s%%', $id); } + private function getExpressionCall($expression) + { + return sprintf('@=%s', $expression); + } + /** * Prepares parameters. * diff --git a/src/Symfony/Component/DependencyInjection/ExpressionLanguage.php b/src/Symfony/Component/DependencyInjection/ExpressionLanguage.php new file mode 100644 index 0000000000000..2e7edcd32f62f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ExpressionLanguage.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\DependencyInjection; + +use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; + +/** + * Adds some function to the default ExpressionLanguage. + * + * To get a service, use service('request'). + * To get a parameter, use parameter('kernel.debug'). + * + * @author Fabien Potencier + */ +class ExpressionLanguage extends BaseExpressionLanguage +{ + protected function registerFunctions() + { + parent::registerFunctions(); + + $this->register('service', function ($arg) { + return sprintf('$this->get(%s)', $arg); + }, function (array $variables, $value) { + return $variables['container']->get($value); + }); + + $this->register('parameter', function ($arg) { + return sprintf('$this->getParameter(%s)', $arg); + }, function (array $variables, $value) { + return $variables['container']->getParameter($value); + }); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index b633ea101ab3a..9b2d46d28ef27 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -237,10 +237,11 @@ private function processAnonymousServices(SimpleXMLElement $xml, $file) if (false !== $nodes = $xml->xpath('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { foreach ($nodes as $node) { // give it a unique name - $node['id'] = sprintf('%s_%d', md5($file), ++$count); + $id = sprintf('%s_%d', hash('sha256', $file), ++$count); + $node['id'] = $id; - $definitions[(string) $node['id']] = array($node->service, $file, false); - $node->service['id'] = (string) $node['id']; + $definitions[$id] = array($node->service, $file, false); + $node->service['id'] = $id; } } @@ -248,10 +249,11 @@ private function processAnonymousServices(SimpleXMLElement $xml, $file) if (false !== $nodes = $xml->xpath('//container:services/container:service[not(@id)]')) { foreach ($nodes as $node) { // give it a unique name - $node['id'] = sprintf('%s_%d', md5($file), ++$count); + $id = sprintf('%s_%d', hash('sha256', $file), ++$count); + $node['id'] = $id; - $definitions[(string) $node['id']] = array($node, $file, true); - $node->service['id'] = (string) $node['id']; + $definitions[$id] = array($node, $file, true); + $node->service['id'] = $id; } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index ce135fa81f929..e081be5c2abd7 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Yaml\Parser as YamlParser; +use Symfony\Component\ExpressionLanguage\Expression; /** * YamlFileLoader loads YAML files service definitions. @@ -224,8 +225,8 @@ private function parseDefinition($id, $service, $file) unset($tag['name']); foreach ($tag as $attribute => $value) { - if (!is_scalar($value)) { - throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s" in %s.', $id, $name, $file)); + if (!is_scalar($value) && null !== $value) { + throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in %s.', $id, $name, $attribute, $file)); } } @@ -311,6 +312,8 @@ private function resolveServices($value) { if (is_array($value)) { $value = array_map(array($this, 'resolveServices'), $value); + } elseif (is_string($value) && 0 === strpos($value, '@=')) { + return new Expression(substr($value, 2)); } elseif (is_string($value) && 0 === strpos($value, '@')) { if (0 === strpos($value, '@@')) { $value = substr($value, 1); diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index d221eb7ac7a1b..51a7d9acf2481 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -98,6 +98,9 @@ + + + @@ -166,6 +169,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php index 494d231e9f29f..dabd1c6215673 100644 --- a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php @@ -24,8 +24,8 @@ */ class ParameterBag implements ParameterBagInterface { - protected $parameters; - protected $resolved; + protected $parameters = array(); + protected $resolved = false; /** * Constructor. @@ -36,9 +36,7 @@ class ParameterBag implements ParameterBagInterface */ public function __construct(array $parameters = array()) { - $this->parameters = array(); $this->add($parameters); - $this->resolved = false; } /** diff --git a/src/Symfony/Component/DependencyInjection/Reference.php b/src/Symfony/Component/DependencyInjection/Reference.php index 1517da29885a7..725747055c1df 100644 --- a/src/Symfony/Component/DependencyInjection/Reference.php +++ b/src/Symfony/Component/DependencyInjection/Reference.php @@ -47,7 +47,7 @@ public function __construct($id, $invalidBehavior = ContainerInterface::EXCEPTIO */ public function __toString() { - return (string) $this->id; + return $this->id; } /** diff --git a/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php b/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php index c591f3e809b90..db855f66d1e89 100644 --- a/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php +++ b/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection; use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\ExpressionLanguage\Expression; /** * SimpleXMLElement class. @@ -77,6 +78,9 @@ public function getArgumentsAsPhp($name, $lowercase = true) $arguments[$key] = new Reference((string) $arg['id'], $invalidBehavior, $strict); break; + case 'expression': + $arguments[$key] = new Expression((string) $arg); + break; case 'collection': $arguments[$key] = $arg->getArgumentsAsPhp($name, false); break; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php index 06845a2b86d32..b5e49842e7af4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php @@ -18,7 +18,7 @@ class CheckDefinitionValidityPassTest extends \PHPUnit_Framework_TestCase { /** - * @expectedException \RuntimeException + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException */ public function testProcessDetectsSyntheticNonPublicDefinitions() { @@ -29,7 +29,7 @@ public function testProcessDetectsSyntheticNonPublicDefinitions() } /** - * @expectedException \RuntimeException + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException */ public function testProcessDetectsSyntheticPrototypeDefinitions() { @@ -40,7 +40,7 @@ public function testProcessDetectsSyntheticPrototypeDefinitions() } /** - * @expectedException \RuntimeException + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException */ public function testProcessDetectsNonSyntheticNonAbstractDefinitionWithoutClass() { @@ -61,6 +61,28 @@ public function testProcess() $this->process($container); } + public function testValidTags() + { + $container = new ContainerBuilder(); + $container->register('a', 'class')->addTag('foo', array('bar' => 'baz')); + $container->register('b', 'class')->addTag('foo', array('bar' => null)); + $container->register('c', 'class')->addTag('foo', array('bar' => 1)); + $container->register('d', 'class')->addTag('foo', array('bar' => 1.1)); + + $this->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testInvalidTags() + { + $container = new ContainerBuilder(); + $container->register('a', 'class')->addTag('foo', array('bar' => array('baz' => 'baz'))); + + $this->process($container); + } + protected function process(ContainerBuilder $container) { $pass = new CheckDefinitionValidityPass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index a669305019a82..f74eefe3010a7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Scope; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\ExpressionLanguage\Expression; class ContainerBuilderTest extends \PHPUnit_Framework_TestCase { @@ -39,7 +40,7 @@ public function testDefinitions() { $builder = new ContainerBuilder(); $definitions = array( - 'foo' => new Definition('FooClass'), + 'foo' => new Definition('Bar\FooClass'), 'bar' => new Definition('BarClass'), ); $builder->setDefinitions($definitions); @@ -68,7 +69,7 @@ public function testDefinitions() public function testRegister() { $builder = new ContainerBuilder(); - $builder->register('foo', 'FooClass'); + $builder->register('foo', 'Bar\FooClass'); $this->assertTrue($builder->hasDefinition('foo'), '->register() registers a new service definition'); $this->assertInstanceOf('Symfony\Component\DependencyInjection\Definition', $builder->getDefinition('foo'), '->register() returns the newly created Definition instance'); } @@ -80,7 +81,7 @@ public function testHas() { $builder = new ContainerBuilder(); $this->assertFalse($builder->has('foo'), '->has() returns false if the service does not exist'); - $builder->register('foo', 'FooClass'); + $builder->register('foo', 'Bar\FooClass'); $this->assertTrue($builder->has('foo'), '->has() returns true if a service definition exists'); $builder->set('bar', new \stdClass()); $this->assertTrue($builder->has('bar'), '->has() returns true if a service exists'); @@ -258,11 +259,11 @@ public function testAddGetCompilerPass() public function testCreateService() { $builder = new ContainerBuilder(); - $builder->register('foo1', 'FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php'); - $this->assertInstanceOf('\FooClass', $builder->get('foo1'), '->createService() requires the file defined by the service definition'); - $builder->register('foo2', 'FooClass')->setFile(__DIR__.'/Fixtures/includes/%file%.php'); + $builder->register('foo1', 'Bar\FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php'); + $this->assertInstanceOf('\Bar\FooClass', $builder->get('foo1'), '->createService() requires the file defined by the service definition'); + $builder->register('foo2', 'Bar\FooClass')->setFile(__DIR__.'/Fixtures/includes/%file%.php'); $builder->setParameter('file', 'foo'); - $this->assertInstanceOf('\FooClass', $builder->get('foo2'), '->createService() replaces parameters in the file provided by the service definition'); + $this->assertInstanceOf('\Bar\FooClass', $builder->get('foo2'), '->createService() replaces parameters in the file provided by the service definition'); } /** @@ -272,13 +273,13 @@ public function testCreateProxyWithRealServiceInstantiator() { $builder = new ContainerBuilder(); - $builder->register('foo1', 'FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php'); + $builder->register('foo1', 'Bar\FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php'); $builder->getDefinition('foo1')->setLazy(true); $foo1 = $builder->get('foo1'); $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved on multiple subsequent calls'); - $this->assertSame('FooClass', get_class($foo1)); + $this->assertSame('Bar\FooClass', get_class($foo1)); } /** @@ -299,7 +300,7 @@ public function testCreateServiceArguments() { $builder = new ContainerBuilder(); $builder->register('bar', 'stdClass'); - $builder->register('foo1', 'FooClass')->addArgument(array('foo' => '%value%', '%value%' => 'foo', new Reference('bar'), '%%unescape_it%%')); + $builder->register('foo1', 'Bar\FooClass')->addArgument(array('foo' => '%value%', '%value%' => 'foo', new Reference('bar'), '%%unescape_it%%')); $builder->setParameter('value', 'bar'); $this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', $builder->get('bar'), '%unescape_it%'), $builder->get('foo1')->arguments, '->createService() replaces parameters and service references in the arguments provided by the service definition'); } @@ -311,7 +312,7 @@ public function testCreateServiceFactoryMethod() { $builder = new ContainerBuilder(); $builder->register('bar', 'stdClass'); - $builder->register('foo1', 'FooClass')->setFactoryClass('FooClass')->setFactoryMethod('getInstance')->addArgument(array('foo' => '%value%', '%value%' => 'foo', new Reference('bar'))); + $builder->register('foo1', 'Bar\FooClass')->setFactoryClass('Bar\FooClass')->setFactoryMethod('getInstance')->addArgument(array('foo' => '%value%', '%value%' => 'foo', new Reference('bar'))); $builder->setParameter('value', 'bar'); $this->assertTrue($builder->get('foo1')->called, '->createService() calls the factory method to create the service instance'); $this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', $builder->get('bar')), $builder->get('foo1')->arguments, '->createService() passes the arguments to the factory method'); @@ -336,7 +337,7 @@ public function testCreateServiceMethodCalls() { $builder = new ContainerBuilder(); $builder->register('bar', 'stdClass'); - $builder->register('foo1', 'FooClass')->addMethodCall('setBar', array(array('%value%', new Reference('bar')))); + $builder->register('foo1', 'Bar\FooClass')->addMethodCall('setBar', array(array('%value%', new Reference('bar')))); $builder->setParameter('value', 'bar'); $this->assertEquals(array('bar', $builder->get('bar')), $builder->get('foo1')->bar, '->createService() replaces the values in the method calls arguments'); } @@ -347,23 +348,23 @@ public function testCreateServiceMethodCalls() public function testCreateServiceConfigurator() { $builder = new ContainerBuilder(); - $builder->register('foo1', 'FooClass')->setConfigurator('sc_configure'); + $builder->register('foo1', 'Bar\FooClass')->setConfigurator('sc_configure'); $this->assertTrue($builder->get('foo1')->configured, '->createService() calls the configurator'); - $builder->register('foo2', 'FooClass')->setConfigurator(array('%class%', 'configureStatic')); + $builder->register('foo2', 'Bar\FooClass')->setConfigurator(array('%class%', 'configureStatic')); $builder->setParameter('class', 'BazClass'); $this->assertTrue($builder->get('foo2')->configured, '->createService() calls the configurator'); $builder->register('baz', 'BazClass'); - $builder->register('foo3', 'FooClass')->setConfigurator(array(new Reference('baz'), 'configure')); + $builder->register('foo3', 'Bar\FooClass')->setConfigurator(array(new Reference('baz'), 'configure')); $this->assertTrue($builder->get('foo3')->configured, '->createService() calls the configurator'); - $builder->register('foo4', 'FooClass')->setConfigurator('foo'); + $builder->register('foo4', 'Bar\FooClass')->setConfigurator('foo'); try { $builder->get('foo4'); $this->fail('->createService() throws an InvalidArgumentException if the configure callable is not a valid callable'); } catch (\InvalidArgumentException $e) { - $this->assertEquals('The configure callable for class "FooClass" is not a callable.', $e->getMessage(), '->createService() throws an InvalidArgumentException if the configure callable is not a valid callable'); + $this->assertEquals('The configure callable for class "Bar\FooClass" is not a callable.', $e->getMessage(), '->createService() throws an InvalidArgumentException if the configure callable is not a valid callable'); } } @@ -374,19 +375,29 @@ public function testCreateServiceConfigurator() public function testCreateSyntheticService() { $builder = new ContainerBuilder(); - $builder->register('foo', 'FooClass')->setSynthetic(true); + $builder->register('foo', 'Bar\FooClass')->setSynthetic(true); $builder->get('foo'); } + public function testCreateServiceWithExpression() + { + $builder = new ContainerBuilder(); + $builder->setParameter('bar', 'bar'); + $builder->register('bar', 'BarClass'); + $builder->register('foo', 'Bar\FooClass')->addArgument(array('foo' => new Expression('service("bar").foo ~ parameter("bar")'))); + $this->assertEquals('foobar', $builder->get('foo')->arguments['foo']); + } + /** * @covers Symfony\Component\DependencyInjection\ContainerBuilder::resolveServices */ public function testResolveServices() { $builder = new ContainerBuilder(); - $builder->register('foo', 'FooClass'); + $builder->register('foo', 'Bar\FooClass'); $this->assertEquals($builder->get('foo'), $builder->resolveServices(new Reference('foo')), '->resolveServices() resolves service references to service instances'); $this->assertEquals(array('foo' => array('foo', $builder->get('foo'))), $builder->resolveServices(array('foo' => array('foo', new Reference('foo')))), '->resolveServices() resolves service references to service instances in nested arrays'); + $this->assertEquals($builder->get('foo'), $builder->resolveServices(new Expression('service("foo")')), '->resolveServices() resolves expressions'); } /** @@ -404,7 +415,6 @@ public function testMerge() $container->setResourceTracking(false); $config = new ContainerBuilder(new ParameterBag(array('foo' => '%bar%'))); $container->merge($config); -////// FIXME $container->compile(); $this->assertEquals(array('bar' => 'foo', 'foo' => 'foo'), $container->getParameterBag()->all(), '->merge() evaluates the values of the parameters towards already defined ones'); @@ -412,13 +422,12 @@ public function testMerge() $container->setResourceTracking(false); $config = new ContainerBuilder(new ParameterBag(array('foo' => '%bar%', 'baz' => '%foo%'))); $container->merge($config); -////// FIXME $container->compile(); $this->assertEquals(array('bar' => 'foo', 'foo' => 'foo', 'baz' => 'foo'), $container->getParameterBag()->all(), '->merge() evaluates the values of the parameters towards already defined ones'); $container = new ContainerBuilder(); $container->setResourceTracking(false); - $container->register('foo', 'FooClass'); + $container->register('foo', 'Bar\FooClass'); $container->register('bar', 'BarClass'); $config = new ContainerBuilder(); $config->setDefinition('baz', new Definition('BazClass')); @@ -432,7 +441,7 @@ public function testMerge() $container = new ContainerBuilder(); $container->setResourceTracking(false); - $container->register('foo', 'FooClass'); + $container->register('foo', 'Bar\FooClass'); $config->setDefinition('foo', new Definition('BazClass')); $container->merge($config); $this->assertEquals('BazClass', $container->getDefinition('foo')->getClass(), '->merge() overrides already defined services'); @@ -457,7 +466,7 @@ public function testfindTaggedServiceIds() { $builder = new ContainerBuilder(); $builder - ->register('foo', 'FooClass') + ->register('foo', 'Bar\FooClass') ->addTag('foo', array('foo' => 'foo')) ->addTag('bar', array('bar' => 'bar')) ->addTag('foo', array('foofoo' => 'foofoo')) @@ -477,7 +486,7 @@ public function testfindTaggedServiceIds() public function testFindDefinition() { $container = new ContainerBuilder(); - $container->setDefinition('foo', $definition = new Definition('FooClass')); + $container->setDefinition('foo', $definition = new Definition('Bar\FooClass')); $container->setAlias('bar', 'foo'); $container->setAlias('foobar', 'bar'); $this->assertEquals($definition, $container->findDefinition('foobar'), '->findDefinition() returns a Definition'); @@ -488,10 +497,6 @@ public function testFindDefinition() */ public function testAddObjectResource() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $container = new ContainerBuilder(); $container->setResourceTracking(false); @@ -518,10 +523,6 @@ public function testAddObjectResource() */ public function testAddClassResource() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $container = new ContainerBuilder(); $container->setResourceTracking(false); @@ -548,10 +549,6 @@ public function testAddClassResource() */ public function testCompilesClassDefinitionsOfLazyServices() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $container = new ContainerBuilder(); $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); @@ -578,10 +575,6 @@ function (ResourceInterface $resource) use ($classesPath) { */ public function testResources() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $container = new ContainerBuilder(); $container->addResource($a = new FileResource(__DIR__.'/Fixtures/xml/services1.xml')); $container->addResource($b = new FileResource(__DIR__.'/Fixtures/xml/services2.xml')); @@ -672,10 +665,6 @@ public function testThrowsExceptionWhenSetServiceOnAFrozenContainer() */ public function testThrowsExceptionWhenAddServiceOnAFrozenContainer() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $container = new ContainerBuilder(); $container->compile(); $container->set('a', new \stdClass()); @@ -683,10 +672,6 @@ public function testThrowsExceptionWhenAddServiceOnAFrozenContainer() public function testNoExceptionWhenSetSyntheticServiceOnAFrozenContainer() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $container = new ContainerBuilder(); $def = new Definition('stdClass'); $def->setSynthetic(true); diff --git a/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php b/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php index 3464a6af3c66e..692d73dbcdc2e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php @@ -18,13 +18,6 @@ class CrossCheckTest extends \PHPUnit_Framework_TestCase { protected static $fixturesPath; - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public static function setUpBeforeClass() { self::$fixturesPath = __DIR__.'/Fixtures/'; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 420671145fe95..98b05d2501ee0 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -31,7 +31,7 @@ public function testDump() $dumper = new PhpDumper($container = new ContainerBuilder()); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services1.php', $dumper->dump(), '->dump() dumps an empty container as an empty PHP class'); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/services1-1.php', $dumper->dump(array('class' => 'Container', 'base_class' => 'AbstractContainer')), '->dump() takes a class and a base_class options'); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services1-1.php', $dumper->dump(array('class' => 'Container', 'base_class' => 'AbstractContainer', 'namespace' => 'Symfony\Component\DependencyInjection\Dump')), '->dump() takes a class and a base_class options'); $container = new ContainerBuilder(); new PhpDumper($container); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php index ca7aec054e481..f9747a7c2fae9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php @@ -18,13 +18,6 @@ class YamlDumperTest extends \PHPUnit_Framework_TestCase { protected static $fixturesPath; - protected function setUp() - { - if (!class_exists('Symfony\Component\Yaml\Yaml')) { - $this->markTestSkipped('The "Yaml" component is not available'); - } - } - public static function setUpBeforeClass() { self::$fixturesPath = realpath(__DIR__.'/../Fixtures/'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php index 862920da02db7..c1fceaf24f498 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php @@ -6,13 +6,14 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Parameter; +use Symfony\Component\ExpressionLanguage\Expression; $container = new ContainerBuilder(); $container-> - register('foo', 'FooClass')-> + register('foo', 'Bar\FooClass')-> addTag('foo', array('foo' => 'foo'))-> addTag('foo', array('bar' => 'bar'))-> - setFactoryClass('FooClass')-> + setFactoryClass('Bar\\FooClass')-> setFactoryMethod('getInstance')-> setArguments(array('foo', new Reference('foo.baz'), array('%foo%' => 'foo is %foo%', 'foobar' => '%foo%'), true, new Reference('service_container')))-> setProperties(array('foo' => 'bar', 'moo' => new Reference('foo.baz'), 'qux' => array('%foo%' => 'foo is %foo%', 'foobar' => '%foo%')))-> @@ -21,7 +22,7 @@ setConfigurator('sc_configure') ; $container-> - register('bar', 'FooClass')-> + register('bar', 'Bar\FooClass')-> setArguments(array('foo', new Reference('foo.baz'), new Parameter('foo_bar')))-> setScope('container')-> setConfigurator(array(new Reference('foo.baz'), 'configure')) @@ -39,18 +40,19 @@ $container->getParameterBag()->clear(); $container->getParameterBag()->add(array( 'baz_class' => 'BazClass', - 'foo_class' => 'FooClass', + 'foo_class' => 'Bar\FooClass', 'foo' => 'bar', )); $container->setAlias('alias_for_foo', 'foo'); $container->setAlias('alias_for_alias', 'alias_for_foo'); $container-> - register('method_call1', 'FooClass')-> + register('method_call1', 'Bar\FooClass')-> setFile(realpath(__DIR__.'/../includes/foo.php'))-> addMethodCall('setBar', array(new Reference('foo')))-> addMethodCall('setBar', array(new Reference('foo2', ContainerInterface::NULL_ON_INVALID_REFERENCE)))-> addMethodCall('setBar', array(new Reference('foo3', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))-> - addMethodCall('setBar', array(new Reference('foobaz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))) + addMethodCall('setBar', array(new Reference('foobaz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))-> + addMethodCall('setBar', array(new Expression('service("foo").foo() ~ parameter("foo")'))) ; $container-> register('factory_service', 'Bar')-> @@ -81,5 +83,14 @@ ->register('depends_on_request', 'stdClass') ->addMethodCall('setRequest', array(new Reference('request', ContainerInterface::NULL_ON_INVALID_REFERENCE, false))) ; +$container + ->register('configurator_service', 'ConfClass') + ->setPublic(false) + ->addMethodCall('setFoo', array(new Reference('baz'))) +; +$container + ->register('configured_service', 'stdClass') + ->setConfigurator(array(new Reference('configurator_service'), 'configureStdClass')) +; return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot index cb006641d0b13..35a0692caf4a6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot @@ -3,17 +3,19 @@ digraph sc { node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; - node_foo [label="foo (alias_for_foo)\nFooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_bar [label="bar\nFooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_foo [label="foo (alias_for_foo)\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_bar [label="bar\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo_baz [label="foo.baz\nBazClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_foo_bar [label="foo_bar\nFooClass\n", shape=record, fillcolor="#eeeeee", style="dotted"]; - node_method_call1 [label="method_call1\nFooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_foo_bar [label="foo_bar\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="dotted"]; + node_method_call1 [label="method_call1\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_factory_service [label="factory_service\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo_with_inline [label="foo_with_inline\nFoo\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_inlined [label="inlined\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_baz [label="baz\nBaz\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_request [label="request\nRequest\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_depends_on_request [label="depends_on_request\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_configurator_service [label="configurator_service\nConfClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_configured_service [label="configured_service\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"]; node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"]; @@ -31,4 +33,5 @@ digraph sc { node_inlined -> node_baz [label="setBaz()" style="dashed"]; node_baz -> node_foo_with_inline [label="setFoo()" style="dashed"]; node_depends_on_request -> node_request [label="setRequest()" style="dashed"]; + node_configurator_service -> node_baz [label="setFoo()" style="dashed"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php index 48e1c2c84e12d..70c3d275b8898 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php @@ -8,6 +8,7 @@ function sc_configure($instance) class BarClass { protected $baz; + public $foo = 'foo'; public function setBaz(BazClass $baz) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/foo.php index 180bb384a3de6..55968aff923c4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/foo.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/foo.php @@ -1,5 +1,7 @@ methodMap = array( 'bar' => 'getBarService', 'baz' => 'getBazService', + 'configurator_service' => 'getConfiguratorServiceService', + 'configured_service' => 'getConfiguredServiceService', 'depends_on_request' => 'getDependsOnRequestService', 'factory_service' => 'getFactoryServiceService', 'foo' => 'getFooService', @@ -47,13 +49,15 @@ public function __construct() * This service is shared. * This method always returns the same instance of the service. * - * @return FooClass A FooClass instance. + * @return Bar\FooClass A Bar\FooClass instance. */ protected function getBarService() { - $this->services['bar'] = $instance = new \FooClass('foo', $this->get('foo.baz'), $this->getParameter('foo_bar')); + $a = $this->get('foo.baz'); + + $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); - $this->get('foo.baz')->configure($instance); + $a->configure($instance); return $instance; } @@ -75,6 +79,23 @@ protected function getBazService() return $instance; } + /** + * Gets the 'configured_service' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getConfiguredServiceService() + { + $this->services['configured_service'] = $instance = new \stdClass(); + + $this->get('configurator_service')->configureStdClass($instance); + + return $instance; + } + /** * Gets the 'depends_on_request' service. * @@ -111,13 +132,13 @@ protected function getFactoryServiceService() * This service is shared. * This method always returns the same instance of the service. * - * @return FooClass A FooClass instance. + * @return Bar\FooClass A Bar\FooClass instance. */ protected function getFooService() { $a = $this->get('foo.baz'); - $this->services['foo'] = $instance = call_user_func(array('FooClass', 'getInstance'), 'foo', $a, array($this->getParameter('foo') => 'foo is '.$this->getParameter('foo').'', 'foobar' => $this->getParameter('foo')), true, $this); + $this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array($this->getParameter('foo') => 'foo is '.$this->getParameter('foo').'', 'foobar' => $this->getParameter('foo')), true, $this); $instance->setBar($this->get('bar')); $instance->initialize(); @@ -181,13 +202,13 @@ protected function getFooWithInlineService() * This service is shared. * This method always returns the same instance of the service. * - * @return FooClass A FooClass instance. + * @return Bar\FooClass A Bar\FooClass instance. */ protected function getMethodCall1Service() { require_once '%path%foo.php'; - $this->services['method_call1'] = $instance = new \FooClass(); + $this->services['method_call1'] = $instance = new \Bar\FooClass(); $instance->setBar($this->get('foo')); $instance->setBar($this->get('foo2', ContainerInterface::NULL_ON_INVALID_REFERENCE)); @@ -197,6 +218,7 @@ protected function getMethodCall1Service() if ($this->has('foobaz')) { $instance->setBar($this->get('foobaz', ContainerInterface::NULL_ON_INVALID_REFERENCE)); } + $instance->setBar(($this->get("foo")->foo() . $this->getParameter("foo"))); return $instance; } @@ -224,6 +246,27 @@ protected function synchronizeRequestService() } } + /** + * Gets the 'configurator_service' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * This service is private. + * If you want to be able to request this service from the container directly, + * make it public, otherwise you might end up with broken code. + * + * @return ConfClass A ConfClass instance. + */ + protected function getConfiguratorServiceService() + { + $this->services['configurator_service'] = $instance = new \ConfClass(); + + $instance->setFoo($this->get('baz')); + + return $instance; + } + /** * Gets the 'inlined' service. * @@ -255,7 +298,7 @@ protected function getDefaultParameters() { return array( 'baz_class' => 'BazClass', - 'foo_class' => 'FooClass', + 'foo_class' => 'Bar\\FooClass', 'foo' => 'bar', ); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index 4205a695d1a6d..74e518f3e1607 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -16,6 +16,8 @@ */ class ProjectServiceContainer extends Container { + private $parameters; + /** * Constructor. */ @@ -34,6 +36,7 @@ public function __construct() $this->methodMap = array( 'bar' => 'getBarService', 'baz' => 'getBazService', + 'configured_service' => 'getConfiguredServiceService', 'depends_on_request' => 'getDependsOnRequestService', 'factory_service' => 'getFactoryServiceService', 'foo' => 'getFooService', @@ -55,13 +58,15 @@ public function __construct() * This service is shared. * This method always returns the same instance of the service. * - * @return FooClass A FooClass instance. + * @return Bar\FooClass A Bar\FooClass instance. */ protected function getBarService() { - $this->services['bar'] = $instance = new \FooClass('foo', $this->get('foo.baz'), $this->getParameter('foo_bar')); + $a = $this->get('foo.baz'); + + $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); - $this->get('foo.baz')->configure($instance); + $a->configure($instance); return $instance; } @@ -83,6 +88,26 @@ protected function getBazService() return $instance; } + /** + * Gets the 'configured_service' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return stdClass A stdClass instance. + */ + protected function getConfiguredServiceService() + { + $a = new \ConfClass(); + $a->setFoo($this->get('baz')); + + $this->services['configured_service'] = $instance = new \stdClass(); + + $a->configureStdClass($instance); + + return $instance; + } + /** * Gets the 'depends_on_request' service. * @@ -119,13 +144,13 @@ protected function getFactoryServiceService() * This service is shared. * This method always returns the same instance of the service. * - * @return FooClass A FooClass instance. + * @return Bar\FooClass A Bar\FooClass instance. */ protected function getFooService() { $a = $this->get('foo.baz'); - $this->services['foo'] = $instance = call_user_func(array('FooClass', 'getInstance'), 'foo', $a, array('bar' => 'foo is bar', 'foobar' => 'bar'), true, $this); + $this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array('bar' => 'foo is bar', 'foobar' => 'bar'), true, $this); $instance->setBar($this->get('bar')); $instance->initialize(); @@ -147,9 +172,9 @@ protected function getFooService() */ protected function getFoo_BazService() { - $this->services['foo.baz'] = $instance = call_user_func(array('BazClass', 'getInstance')); + $this->services['foo.baz'] = $instance = \BazClass::getInstance(); - call_user_func(array('BazClass', 'configureStatic1'), $instance); + \BazClass::configureStatic1($instance); return $instance; } @@ -157,11 +182,11 @@ protected function getFoo_BazService() /** * Gets the 'foo_bar' service. * - * @return FooClass A FooClass instance. + * @return Bar\FooClass A Bar\FooClass instance. */ protected function getFooBarService() { - return new \FooClass(); + return new \Bar\FooClass(); } /** @@ -192,16 +217,17 @@ protected function getFooWithInlineService() * This service is shared. * This method always returns the same instance of the service. * - * @return FooClass A FooClass instance. + * @return Bar\FooClass A Bar\FooClass instance. */ protected function getMethodCall1Service() { require_once '%path%foo.php'; - $this->services['method_call1'] = $instance = new \FooClass(); + $this->services['method_call1'] = $instance = new \Bar\FooClass(); $instance->setBar($this->get('foo')); $instance->setBar(NULL); + $instance->setBar(($this->get("foo")->foo() . $this->getParameter("foo"))); return $instance; } @@ -281,7 +307,7 @@ protected function getDefaultParameters() { return array( 'baz_class' => 'BazClass', - 'foo_class' => 'FooClass', + 'foo_class' => 'Bar\\FooClass', 'foo' => 'bar', ); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services2.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services2.xml index 6e8a6ce364dce..43cf86c54751a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services2.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services2.xml @@ -27,5 +27,6 @@ value + PHP_EOL diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml index abd9fbc1529b1..ffc038234fae3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml @@ -32,6 +32,9 @@ + + service("foo").foo() ~ parameter("foo") + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml index 7b99fe2667e06..f9d9d4797bebf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml @@ -2,11 +2,11 @@ BazClass - FooClass + Bar\FooClass bar - + foo @@ -29,7 +29,7 @@ - + foo %foo_bar% @@ -39,7 +39,7 @@ - + %path%foo.php @@ -53,6 +53,9 @@ + + service("foo").foo() ~ parameter("foo") + @@ -77,6 +80,14 @@ + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag3.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag3.yml index 8137fab63fc0a..72ec4e8f0bc6f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag3.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag3.yml @@ -3,4 +3,4 @@ services: class: FooClass tags: # tag-attribute is not a scalar - - { name: foo, foo: { foo: foo, bar: bar } } + - { name: foo, bar: { foo: foo, bar: bar } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml index 7ba9453bdd6dd..d3c793f2e31c3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml @@ -15,6 +15,7 @@ services: calls: - [ setBar, [] ] - [ setBar ] + - [ setBar, ['@=service("foo").foo() ~ parameter("foo")'] ] method_call2: class: FooClass calls: diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml index a0e941a50af00..1dd163a259cca 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml @@ -1,15 +1,15 @@ parameters: baz_class: BazClass - foo_class: FooClass + foo_class: Bar\FooClass foo: bar services: foo: - class: FooClass + class: Bar\FooClass tags: - { name: foo, foo: foo } - { name: foo, bar: bar } - factory_class: FooClass + factory_class: Bar\FooClass factory_method: getInstance arguments: [foo, '@foo.baz', { '%foo%': 'foo is %foo%', foobar: '%foo%' }, true, '@service_container'] properties: { foo: bar, moo: '@foo.baz', qux: { '%foo%': 'foo is %foo%', foobar: '%foo%' } } @@ -19,7 +19,7 @@ services: configurator: sc_configure bar: - class: FooClass + class: Bar\FooClass arguments: [foo, '@foo.baz', '%foo_bar%'] configurator: ['@foo.baz', configure] foo.baz: @@ -31,13 +31,14 @@ services: class: %foo_class% scope: prototype method_call1: - class: FooClass + class: Bar\FooClass file: %path%foo.php calls: - [setBar, ['@foo']] - [setBar, ['@?foo2']] - [setBar, ['@?foo3']] - [setBar, ['@?foobaz']] + - [setBar, ['@=service("foo").foo() ~ parameter("foo")']] factory_service: class: Bar @@ -69,5 +70,14 @@ services: calls: - [setRequest, ['@?request']] + configurator_service: + class: ConfClass + public: false + calls: + - [setFoo, ['@baz']] + + configured_service: + class: stdClass + configurator: ['@configurator_service', configureStdClass] alias_for_foo: @foo alias_for_alias: @foo diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/ClosureLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/ClosureLoaderTest.php index 33594d640cf6b..483e30b78b818 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/ClosureLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/ClosureLoaderTest.php @@ -16,13 +16,6 @@ class ClosureLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - /** * @covers Symfony\Component\DependencyInjection\Loader\ClosureLoader::supports */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php index 220ad7fe4d871..b9f402b70189f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php @@ -29,10 +29,6 @@ public static function setUpBeforeClass() protected function setUp() { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $this->container = new ContainerBuilder(); $this->loader = new IniFileLoader($this->container, new FileLocator(self::$fixturesPath.'/ini')); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index 3a97dc27da5eb..505f710cbeb1a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -18,13 +18,6 @@ class PhpFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - /** * @covers Symfony\Component\DependencyInjection\Loader\PhpFileLoader::supports */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index faa8d70bd672d..74db3c8eff5d4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -22,18 +22,12 @@ use Symfony\Component\DependencyInjection\Loader\IniFileLoader; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\FileLocator; +use Symfony\Component\ExpressionLanguage\Expression; class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase { protected static $fixturesPath; - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public static function setUpBeforeClass() { self::$fixturesPath = realpath(__DIR__.'/../Fixtures/'); @@ -99,17 +93,33 @@ public function testLoadParameters() $loader->load('services2.xml'); $actual = $container->getParameterBag()->all(); - $expected = array('a string', 'foo' => 'bar', 'values' => array(0, 'integer' => 4, 100 => null, 'true', true, false, 'on', 'off', 'float' => 1.3, 1000.3, 'a string', array('foo', 'bar')), 'foo_bar' => new Reference('foo_bar'), 'mixedcase' => array('MixedCaseKey' => 'value')); + $expected = array( + 'a string', + 'foo' => 'bar', + 'values' => array( + 0, + 'integer' => 4, + 100 => null, + 'true', + true, + false, + 'on', + 'off', + 'float' => 1.3, + 1000.3, + 'a string', + array('foo', 'bar'), + ), + 'foo_bar' => new Reference('foo_bar'), + 'mixedcase' => array('MixedCaseKey' => 'value'), + 'constant' => PHP_EOL, + ); $this->assertEquals($expected, $actual, '->load() converts XML values to PHP ones'); } public function testLoadImports() { - if (!class_exists('Symfony\Component\Yaml\Yaml')) { - $this->markTestSkipped('The "Yaml" component is not available'); - } - $container = new ContainerBuilder(); $resolver = new LoaderResolver(array( new IniFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')), @@ -120,7 +130,30 @@ public function testLoadImports() $loader->load('services4.xml'); $actual = $container->getParameterBag()->all(); - $expected = array('a string', 'foo' => 'bar', 'values' => array(true, false), 'foo_bar' => new Reference('foo_bar'), 'mixedcase' => array('MixedCaseKey' => 'value'), 'bar' => '%foo%', 'imported_from_ini' => true, 'imported_from_yaml' => true); + $expected = array( + 'a string', + 'foo' => 'bar', + 'values' => array( + 0, + 'integer' => 4, + 100 => null, + 'true', + true, + false, + 'on', + 'off', + 'float' => 1.3, + 1000.3, + 'a string', + array('foo', 'bar'), + ), + 'foo_bar' => new Reference('foo_bar'), + 'mixedcase' => array('MixedCaseKey' => 'value'), + 'constant' => PHP_EOL, + 'bar' => '%foo%', + 'imported_from_ini' => true, + 'imported_from_yaml' => true + ); $this->assertEquals(array_keys($expected), array_keys($actual), '->load() imports and merges imported files'); @@ -179,7 +212,7 @@ public function testLoadServices() $this->assertEquals('sc_configure', $services['configurator1']->getConfigurator(), '->load() parses the configurator tag'); $this->assertEquals(array(new Reference('baz', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag'); $this->assertEquals(array('BazClass', 'configureStatic'), $services['configurator3']->getConfigurator(), '->load() parses the configurator tag'); - $this->assertEquals(array(array('setBar', array())), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag'); + $this->assertEquals(array(array('setBar', array()), array('setBar', array(new Expression('service("foo").foo() ~ parameter("foo")')))), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag'); $this->assertEquals(array(array('setBar', array('foo', new Reference('foo'), array(true, false)))), $services['method_call2']->getMethodCalls(), '->load() parses the method_call tag'); $this->assertNull($services['factory_service']->getClass()); $this->assertEquals('getInstance', $services['factory_service']->getFactoryMethod()); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index e452e5d221d19..c7cb0cde5f01b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -20,22 +20,12 @@ use Symfony\Component\DependencyInjection\Loader\IniFileLoader; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\FileLocator; +use Symfony\Component\ExpressionLanguage\Expression; class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase { protected static $fixturesPath; - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - - if (!class_exists('Symfony\Component\Yaml\Yaml')) { - $this->markTestSkipped('The "Yaml" component is not available'); - } - } - public static function setUpBeforeClass() { self::$fixturesPath = realpath(__DIR__.'/../Fixtures/'); @@ -124,7 +114,7 @@ public function testLoadServices() $this->assertEquals('sc_configure', $services['configurator1']->getConfigurator(), '->load() parses the configurator tag'); $this->assertEquals(array(new Reference('baz'), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag'); $this->assertEquals(array('BazClass', 'configureStatic'), $services['configurator3']->getConfigurator(), '->load() parses the configurator tag'); - $this->assertEquals(array(array('setBar', array()), array('setBar', array())), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag'); + $this->assertEquals(array(array('setBar', array()), array('setBar', array()), array('setBar', array(new Expression('service("foo").foo() ~ parameter("foo")')))), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag'); $this->assertEquals(array(array('setBar', array('foo', new Reference('foo'), array(true, false)))), $services['method_call2']->getMethodCalls(), '->load() parses the method_call tag'); $this->assertEquals('baz_factory', $services['factory_service']->getFactoryService()); @@ -209,7 +199,7 @@ public function testTagWithAttributeArrayThrowsException() $this->fail('->load() should throw an exception when a tag-attribute is not a scalar'); } catch (\Exception $e) { $this->assertInstanceOf('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException', $e, '->load() throws an InvalidArgumentException if a tag-attribute is not a scalar'); - $this->assertStringStartsWith('A "tags" attribute must be of a scalar-type for service ', $e->getMessage(), '->load() throws an InvalidArgumentException if a tag-attribute is not a scalar'); + $this->assertStringStartsWith('A "tags" attribute must be of a scalar-type for service "foo_service", tag "foo", attribute "bar"', $e->getMessage(), '->load() throws an InvalidArgumentException if a tag-attribute is not a scalar'); } } } diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 2631da96df2f8..2190101fa2c9f 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -20,7 +20,8 @@ }, "require-dev": { "symfony/yaml": "~2.0", - "symfony/config": "~2.2" + "symfony/config": "~2.2", + "symfony/expression-language": "~2.4" }, "suggest": { "symfony/yaml": "", @@ -34,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/DomCrawler/CHANGELOG.md b/src/Symfony/Component/DomCrawler/CHANGELOG.md index 2343a51b4f357..bc5180513360d 100644 --- a/src/Symfony/Component/DomCrawler/CHANGELOG.md +++ b/src/Symfony/Component/DomCrawler/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +2.4.0 +----- + + * `Crawler::addXmlContent()` removes the default document namespace again if it's an only namespace. + * added support for automatic discovery and explicit registration of document + namespaces for `Crawler::filterXPath()` and `Crawler::filter()` + * improved content type guessing in `Crawler::addContent()` + * [BC BREAK] `Crawler::addXmlContent()` no longer removes the default document + namespace + 2.3.0 ----- diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index c3954946ad83e..ee50d7077ce14 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -27,6 +27,16 @@ class Crawler extends \SplObjectStorage */ protected $uri; + /** + * @var string The default namespace prefix to be used with XPath and CSS expressions + */ + private $defaultNamespacePrefix = 'default'; + + /** + * @var array A map of manually registered namespaces + */ + private $namespaces = array(); + /** * Constructor. * @@ -92,7 +102,7 @@ public function add($node) public function addContent($content, $type = null) { if (empty($type)) { - $type = 'text/html'; + $type = 0 === strpos($content, 'validateOnParse = true; - - // remove the default namespace to make XPath expressions simpler - @$dom->loadXML(str_replace('xmlns', 'ns', $content), LIBXML_NONET); + @$dom->loadXML($content, LIBXML_NONET); libxml_use_internal_errors($current); libxml_disable_entity_loader($disableEntities); @@ -474,7 +487,7 @@ public function children() * * @param string $attribute The attribute name * - * @return string The attribute value + * @return string|null The attribute value or null if the attribute does not exist * * @throws \InvalidArgumentException When current node is empty * @@ -486,7 +499,9 @@ public function attr($attribute) throw new \InvalidArgumentException('The current node list is empty.'); } - return $this->getNode(0)->getAttribute($attribute); + $node = $this->getNode(0); + + return $node->hasAttribute($attribute) ? $node->getAttribute($attribute) : null; } /** @@ -590,7 +605,8 @@ public function filterXPath($xpath) $root->appendChild($document->importNode($node, true)); } - $domxpath = new \DOMXPath($document); + $prefixes = $this->findNamespacePrefixes($xpath); + $domxpath = $this->createDOMXPath($document, $prefixes); return new static($domxpath->query($xpath), $this->uri); } @@ -611,9 +627,7 @@ public function filterXPath($xpath) public function filter($selector) { if (!class_exists('Symfony\\Component\\CssSelector\\CssSelector')) { - // @codeCoverageIgnoreStart throw new \RuntimeException('Unable to filter with a CSS selector as the Symfony CssSelector is not installed (you can use filterXPath instead).'); - // @codeCoverageIgnoreEnd } return $this->filterXPath(CssSelector::toXPath($selector)); @@ -720,6 +734,25 @@ public function form(array $values = null, $method = null) return $form; } + /** + * Overloads a default namespace prefix to be used with XPath and CSS expressions. + * + * @param string $prefix + */ + public function setDefaultNamespacePrefix($prefix) + { + $this->defaultNamespacePrefix = $prefix; + } + + /** + * @param string $prefix + * @param string $namespace + */ + public function registerNamespace($prefix, $namespace) + { + $this->namespaces[$prefix] = $namespace; + } + /** * Converts string for XPath expressions. * @@ -772,17 +805,15 @@ public static function xpathLiteral($s) * * @return \DOMElement|null */ - protected function getNode($position) + public function getNode($position) { foreach ($this as $i => $node) { if ($i == $position) { return $node; } - // @codeCoverageIgnoreStart } return null; - // @codeCoverageIgnoreEnd } /** @@ -803,4 +834,62 @@ protected function sibling($node, $siblingDir = 'nextSibling') return $nodes; } + + /** + * @param \DOMDocument $document + * @param array $prefixes + * + * @return \DOMXPath + * + * @throws \InvalidArgumentException + */ + private function createDOMXPath(\DOMDocument $document, array $prefixes = array()) + { + $domxpath = new \DOMXPath($document); + + foreach ($prefixes as $prefix) { + $namespace = $this->discoverNamespace($domxpath, $prefix); + if (null !== $namespace) { + $domxpath->registerNamespace($prefix, $namespace); + } + } + + return $domxpath; + } + + /** + * @param \DOMXPath $domxpath + * @param string $prefix + * + * @return string + * + * @throws \InvalidArgumentException + */ + private function discoverNamespace(\DOMXPath $domxpath, $prefix) + { + if (isset($this->namespaces[$prefix])) { + return $this->namespaces[$prefix]; + } + + // ask for one namespace, otherwise we'd get a collection with an item for each node + $namespaces = $domxpath->query(sprintf('(//namespace::*[name()="%s"])[last()]', $this->defaultNamespacePrefix === $prefix ? '' : $prefix)); + + if ($node = $namespaces->item(0)) { + return $node->nodeValue; + } + } + + /** + * @param $xpath + * + * @return array + */ + private function findNamespacePrefixes($xpath) + { + if (preg_match_all('/(?P[a-z_][a-z_0-9\-\.]*):[^"\/]/i', $xpath, $matches)) { + return array_unique($matches['prefix']); + } + + return array(); + } } diff --git a/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php b/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php index 2e192cb1906dd..de49220299ecd 100644 --- a/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php +++ b/src/Symfony/Component/DomCrawler/Field/ChoiceFormField.php @@ -34,6 +34,10 @@ class ChoiceFormField extends FormField * @var array */ private $options; + /** + * @var boolean + */ + private $validationDisabled = false; /** * Returns true if the field should be included in the submitted values. @@ -280,6 +284,10 @@ private function buildOptionValue($node) */ public function containsOption($optionValue, $options) { + if ($this->validationDisabled) { + return true; + } + foreach ($options as $option) { if ($option['value'] == $optionValue) { return true; @@ -304,4 +312,16 @@ public function availableOptionValues() return $values; } + + /** + * Disables the internal validation of the field. + * + * @return self + */ + public function disableValidation() + { + $this->validationDisabled = true; + + return $this; + } } diff --git a/src/Symfony/Component/DomCrawler/Form.php b/src/Symfony/Component/DomCrawler/Form.php index a127227974d89..608c9061fe519 100644 --- a/src/Symfony/Component/DomCrawler/Form.php +++ b/src/Symfony/Component/DomCrawler/Form.php @@ -330,6 +330,22 @@ public function offsetUnset($name) $this->fields->remove($name); } + /** + * Disables validation + * + * @return self + */ + public function disableValidation() + { + foreach ($this->fields->all() as $field) { + if ($field instanceof Field\ChoiceFormField) { + $field->disableValidation(); + } + } + + return $this; + } + /** * Sets the node for the form. * diff --git a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php index 1c223980252fd..8ffa473654e41 100644 --- a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DomCrawler\Tests; +use Symfony\Component\CssSelector\CssSelector; use Symfony\Component\DomCrawler\Crawler; class CrawlerTest extends \PHPUnit_Framework_TestCase @@ -332,6 +333,17 @@ public function testAttr() } } + public function testMissingAttrValueIsNull() + { + $crawler = new Crawler(); + $crawler->addContent('
    ', 'text/html; charset=UTF-8'); + $div = $crawler->filterXPath('//div'); + + $this->assertEquals('sample value', $div->attr('non-empty-attr'), '->attr() reads non-empty attributes correctly'); + $this->assertEquals('', $div->attr('empty-attr'), '->attr() reads empty attributes correctly'); + $this->assertNull($div->attr('missing-attr'), '->attr() reads missing attributes correctly'); + } + public function testText() { $this->assertEquals('One', $this->createTestCrawler()->filterXPath('//li')->text(), '->text() returns the node value of the first element of the node list'); @@ -382,15 +394,60 @@ public function testFilterXPath() $this->assertCount(6, $crawler->filterXPath('//li'), '->filterXPath() filters the node list with the XPath expression'); } + public function testFilterXPathWithDefaultNamespace() + { + $crawler = $this->createTestXmlCrawler()->filterXPath('//default:entry/default:id'); + $this->assertCount(1, $crawler, '->filterXPath() automatically registers a namespace'); + $this->assertSame('tag:youtube.com,2008:video:kgZRZmEc9j4', $crawler->text()); + } + + public function testFilterXPathWithCustomDefaultNamespace() + { + $crawler = $this->createTestXmlCrawler(); + $crawler->setDefaultNamespacePrefix('x'); + $crawler = $crawler->filterXPath('//x:entry/x:id'); + + $this->assertCount(1, $crawler, '->filterXPath() lets to override the default namespace prefix'); + $this->assertSame('tag:youtube.com,2008:video:kgZRZmEc9j4', $crawler->text()); + } + + public function testFilterXPathWithNamespace() + { + $crawler = $this->createTestXmlCrawler()->filterXPath('//yt:accessControl'); + $this->assertCount(2, $crawler, '->filterXPath() automatically registers a namespace'); + } + + public function testFilterXPathWithMultipleNamespaces() + { + $crawler = $this->createTestXmlCrawler()->filterXPath('//media:group/yt:aspectRatio'); + $this->assertCount(1, $crawler, '->filterXPath() automatically registers multiple namespaces'); + $this->assertSame('widescreen', $crawler->text()); + } + + public function testFilterXPathWithManuallyRegisteredNamespace() + { + $crawler = $this->createTestXmlCrawler(); + $crawler->registerNamespace('m', 'http://search.yahoo.com/mrss/'); + + $crawler = $crawler->filterXPath('//m:group/yt:aspectRatio'); + $this->assertCount(1, $crawler, '->filterXPath() uses manually registered namespace'); + $this->assertSame('widescreen', $crawler->text()); + } + + public function testFilterXPathWithAnUrl() + { + $crawler = $this->createTestXmlCrawler(); + + $crawler = $crawler->filterXPath('//media:category[@scheme="http://gdata.youtube.com/schemas/2007/categories.cat"]'); + $this->assertCount(1, $crawler); + $this->assertSame('Music', $crawler->text()); + } + /** * @covers Symfony\Component\DomCrawler\Crawler::filter */ public function testFilter() { - if (!class_exists('Symfony\Component\CssSelector\CssSelector')) { - $this->markTestSkipped('The "CssSelector" component is not available'); - } - $crawler = $this->createTestCrawler(); $this->assertNotSame($crawler, $crawler->filter('li'), '->filter() returns a new instance of a crawler'); $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->filter() returns a new instance of a crawler'); @@ -400,6 +457,52 @@ public function testFilter() $this->assertCount(6, $crawler->filter('li'), '->filter() filters the node list with the CSS selector'); } + public function testFilterWithDefaultNamespace() + { + $crawler = $this->createTestXmlCrawler()->filter('default|entry default|id'); + $this->assertCount(1, $crawler, '->filter() automatically registers namespaces'); + $this->assertSame('tag:youtube.com,2008:video:kgZRZmEc9j4', $crawler->text()); + } + + public function testFilterWithNamespace() + { + CssSelector::disableHtmlExtension(); + + $crawler = $this->createTestXmlCrawler()->filter('yt|accessControl'); + $this->assertCount(2, $crawler, '->filter() automatically registers namespaces'); + } + + public function testFilterWithMultipleNamespaces() + { + CssSelector::disableHtmlExtension(); + + $crawler = $this->createTestXmlCrawler()->filter('media|group yt|aspectRatio'); + $this->assertCount(1, $crawler, '->filter() automatically registers namespaces'); + $this->assertSame('widescreen', $crawler->text()); + } + + public function testFilterWithDefaultNamespaceOnly() + { + $crawler = new Crawler(' + + + http://localhost/foo + weekly + 0.5 + 2012-11-16 + + + http://localhost/bar + weekly + 0.5 + 2012-11-16 + + + '); + + $this->assertEquals(2, $crawler->filter('url')->count()); + } + public function testSelectLink() { $crawler = $this->createTestCrawler(); @@ -672,6 +775,23 @@ public function createTestCrawler($uri = null) return new Crawler($dom, $uri); } + protected function createTestXmlCrawler($uri = null) + { + $xml = ' + + tag:youtube.com,2008:video:kgZRZmEc9j4 + + + + Chordates - CrashCourse Biology #24 + widescreen + + Music + '; + + return new Crawler($xml, $uri); + } + protected function createDomDocument() { $dom = new \DOMDocument(); diff --git a/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php b/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php index cf7cd5a2f2663..d2a95a53e8611 100644 --- a/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/Field/ChoiceFormFieldTest.php @@ -284,6 +284,21 @@ public function testOptionWithNoValue() $this->assertEquals('foo', $field->getValue(), '->select() changes the selected option'); } + public function testDisableValidation() + { + $node = $this->createSelectNode(array('foo' => false, 'bar' => false)); + $field = new ChoiceFormField($node); + $field->disableValidation(); + $field->setValue('foobar'); + $this->assertEquals('foobar', $field->getValue(), '->disableValidation() allows to set a value which is not in the selected options.'); + + $node = $this->createSelectNode(array('foo' => false, 'bar' => false), array('multiple' => 'multiple')); + $field = new ChoiceFormField($node); + $field->disableValidation(); + $field->setValue(array('foobar')); + $this->assertEquals(array('foobar'), $field->getValue(), '->disableValidation() allows to set a value which is not in the selected options.'); + } + protected function createSelectNode($options, $attributes = array()) { $document = new \DOMDocument(); diff --git a/src/Symfony/Component/DomCrawler/Tests/FormTest.php b/src/Symfony/Component/DomCrawler/Tests/FormTest.php index 843313c4a7e7c..0dfe82b62ad9f 100644 --- a/src/Symfony/Component/DomCrawler/Tests/FormTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/FormTest.php @@ -338,6 +338,26 @@ public function testSetValueOnMultiValuedFieldsWithMalformedName() } } + public function testDisableValidation() + { + $form = $this->createForm('
    + + + + '); + + $form->disableValidation(); + + $form['foo[bar]']->select('foo'); + $form['foo[baz]']->select('bar'); + $this->assertEquals('foo', $form['foo[bar]']->getValue(), '->disableValidation() disables validation of all ChoiceFormField.'); + $this->assertEquals('bar', $form['foo[baz]']->getValue(), '->disableValidation() disables validation of all ChoiceFormField.'); + } + public function testOffsetUnset() { $form = $this->createForm('
    '); diff --git a/src/Symfony/Component/DomCrawler/composer.json b/src/Symfony/Component/DomCrawler/composer.json index 4e56c92584407..0246346238c0f 100644 --- a/src/Symfony/Component/DomCrawler/composer.json +++ b/src/Symfony/Component/DomCrawler/composer.json @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/EventDispatcher/CHANGELOG.md b/src/Symfony/Component/EventDispatcher/CHANGELOG.md index 536c5ac729812..bb42ee19c04ca 100644 --- a/src/Symfony/Component/EventDispatcher/CHANGELOG.md +++ b/src/Symfony/Component/EventDispatcher/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +2.5.0 +----- + + * added Debug\TraceableEventDispatcher (originally in HttpKernel) + * changed Debug\TraceableEventDispatcherInterface to extend EventDispatcherInterface + * added RegisterListenersPass (originally in HttpKernel) + 2.1.0 ----- diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php new file mode 100644 index 0000000000000..14cd49dd66034 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php @@ -0,0 +1,368 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\Stopwatch\Stopwatch; +use Psr\Log\LoggerInterface; + +/** + * Collects some data about event listeners. + * + * This event dispatcher delegates the dispatching to another one. + * + * @author Fabien Potencier + */ +class TraceableEventDispatcher implements TraceableEventDispatcherInterface +{ + protected $logger; + protected $stopwatch; + private $called = array(); + private $dispatcher; + private $wrappedListeners = array(); + private $firstCalledEvent = array(); + private $id; + private $lastEventId = 0; + + /** + * Constructor. + * + * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance + * @param Stopwatch $stopwatch A Stopwatch instance + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null) + { + $this->dispatcher = $dispatcher; + $this->stopwatch = $stopwatch; + $this->logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function addListener($eventName, $listener, $priority = 0) + { + $this->dispatcher->addListener($eventName, $listener, $priority); + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + $this->dispatcher->addSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function removeListener($eventName, $listener) + { + return $this->dispatcher->removeListener($eventName, $listener); + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + return $this->dispatcher->removeSubscriber($subscriber); + } + + /** + * {@inheritdoc} + */ + public function getListeners($eventName = null) + { + return $this->dispatcher->getListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function hasListeners($eventName = null) + { + return $this->dispatcher->hasListeners($eventName); + } + + /** + * {@inheritdoc} + */ + public function dispatch($eventName, Event $event = null) + { + if (null === $event) { + $event = new Event(); + } + + $this->id = $eventId = ++$this->lastEventId; + + // Wrap all listeners before they are called + $this->wrappedListeners[$this->id] = new \SplObjectStorage(); + + $listeners = $this->dispatcher->getListeners($eventName); + + foreach ($listeners as $listener) { + $this->dispatcher->removeListener($eventName, $listener); + $wrapped = $this->wrapListener($eventName, $listener); + $this->wrappedListeners[$this->id][$wrapped] = $listener; + $this->dispatcher->addListener($eventName, $wrapped); + } + + $this->preDispatch($eventName, $event); + + $e = $this->stopwatch->start($eventName, 'section'); + + $this->firstCalledEvent[$eventName] = $this->stopwatch->start($eventName.'.loading', 'event_listener_loading'); + + if (!$this->dispatcher->hasListeners($eventName)) { + $this->firstCalledEvent[$eventName]->stop(); + } + + $this->dispatcher->dispatch($eventName, $event); + + // reset the id as another event might have been dispatched during the dispatching of this event + $this->id = $eventId; + + unset($this->firstCalledEvent[$eventName]); + + if ($e->isStarted()) { + $e->stop(); + } + + $this->postDispatch($eventName, $event); + + // Unwrap all listeners after they are called + foreach ($this->wrappedListeners[$this->id] as $wrapped) { + $this->dispatcher->removeListener($eventName, $wrapped); + $this->dispatcher->addListener($eventName, $this->wrappedListeners[$this->id][$wrapped]); + } + + unset($this->wrappedListeners[$this->id]); + + return $event; + } + + /** + * {@inheritDoc} + */ + public function getCalledListeners() + { + return $this->called; + } + + /** + * {@inheritDoc} + */ + public function getNotCalledListeners() + { + $notCalled = array(); + + foreach ($this->getListeners() as $name => $listeners) { + foreach ($listeners as $listener) { + $info = $this->getListenerInfo($listener, $name); + if (!isset($this->called[$name.'.'.$info['pretty']])) { + $notCalled[$name.'.'.$info['pretty']] = $info; + } + } + } + + return $notCalled; + } + + /** + * Proxies all method calls to the original event dispatcher. + * + * @param string $method The method name + * @param array $arguments The method arguments + * + * @return mixed + */ + public function __call($method, $arguments) + { + return call_user_func_array(array($this->dispatcher, $method), $arguments); + } + + /** + * This is a private method and must not be used. + * + * This method is public because it is used in a closure. + * Whenever Symfony will require PHP 5.4, this could be changed + * to a proper private method. + */ + public function logSkippedListeners($eventName, Event $event, $listener) + { + if (null === $this->logger) { + return; + } + + $info = $this->getListenerInfo($listener, $eventName); + + $this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName)); + + $skippedListeners = $this->getListeners($eventName); + $skipped = false; + + foreach ($skippedListeners as $skippedListener) { + $skippedListener = $this->unwrapListener($skippedListener); + + if ($skipped) { + $info = $this->getListenerInfo($skippedListener, $eventName); + $this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName)); + } + + if ($skippedListener === $listener) { + $skipped = true; + } + } + } + + /** + * This is a private method. + * + * This method is public because it is used in a closure. + * Whenever Symfony will require PHP 5.4, this could be changed + * to a proper private method. + */ + public function preListenerCall($eventName, $listener) + { + // is it the first called listener? + if (isset($this->firstCalledEvent[$eventName])) { + $this->firstCalledEvent[$eventName]->stop(); + + unset($this->firstCalledEvent[$eventName]); + } + + $info = $this->getListenerInfo($listener, $eventName); + + if (null !== $this->logger) { + $this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty'])); + } + + $this->called[$eventName.'.'.$info['pretty']] = $info; + + return $this->stopwatch->start(isset($info['class']) ? $info['class'] : $info['type'], 'event_listener'); + } + + /** + * Returns information about the listener + * + * @param object $listener The listener + * @param string $eventName The event name + * + * @return array Information about the listener + */ + private function getListenerInfo($listener, $eventName) + { + $listener = $this->unwrapListener($listener); + + $info = array( + 'event' => $eventName, + ); + if ($listener instanceof \Closure) { + $info += array( + 'type' => 'Closure', + 'pretty' => 'closure' + ); + } elseif (is_string($listener)) { + try { + $r = new \ReflectionFunction($listener); + $file = $r->getFileName(); + $line = $r->getStartLine(); + } catch (\ReflectionException $e) { + $file = null; + $line = null; + } + $info += array( + 'type' => 'Function', + 'function' => $listener, + 'file' => $file, + 'line' => $line, + 'pretty' => $listener, + ); + } elseif (is_array($listener) || (is_object($listener) && is_callable($listener))) { + if (!is_array($listener)) { + $listener = array($listener, '__invoke'); + } + $class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0]; + try { + $r = new \ReflectionMethod($class, $listener[1]); + $file = $r->getFileName(); + $line = $r->getStartLine(); + } catch (\ReflectionException $e) { + $file = null; + $line = null; + } + $info += array( + 'type' => 'Method', + 'class' => $class, + 'method' => $listener[1], + 'file' => $file, + 'line' => $line, + 'pretty' => $class.'::'.$listener[1], + ); + } + + return $info; + } + + /** + * Called before dispatching the event. + * + * @param string $eventName The event name + * @param Event $event The event + */ + protected function preDispatch($eventName, Event $event) + { + } + + /** + * Called after dispatching the event. + * + * @param string $eventName The event name + * @param Event $event The event + */ + protected function postDispatch($eventName, Event $event) + { + } + + private function wrapListener($eventName, $listener) + { + $self = $this; + + return function (Event $event) use ($self, $eventName, $listener) { + $e = $self->preListenerCall($eventName, $listener); + + call_user_func($listener, $event, $eventName, $self); + + if ($e->isStarted()) { + $e->stop(); + } + + if ($event->isPropagationStopped()) { + $self->logSkippedListeners($eventName, $event, $listener); + } + }; + } + + private function unwrapListener($listener) + { + // get the original listener + if (is_object($listener) && isset($this->wrappedListeners[$this->id][$listener])) { + return $this->wrappedListeners[$this->id][$listener]; + } + + return $listener; + } +} diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php index a67a979014f9b..5483e815068c4 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php @@ -11,10 +11,12 @@ namespace Symfony\Component\EventDispatcher\Debug; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + /** * @author Fabien Potencier */ -interface TraceableEventDispatcherInterface +interface TraceableEventDispatcherInterface extends EventDispatcherInterface { /** * Gets the called listeners. diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php new file mode 100644 index 0000000000000..afe3ecd182212 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.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\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Compiler pass to register tagged services for an event dispatcher. + */ +class RegisterListenersPass implements CompilerPassInterface +{ + /** + * @var string + */ + protected $dispatcherService; + + /** + * @var string + */ + protected $listenerTag; + + /** + * @var string + */ + protected $subscriberTag; + + /** + * Constructor. + * + * @param string $dispatcherService Service name of the event dispatcher in processed container + * @param string $listenerTag Tag name used for listener + * @param string $subscriberTag Tag name used for subscribers + */ + public function __construct($dispatcherService = 'event_dispatcher', $listenerTag = 'kernel.event_listener', $subscriberTag = 'kernel.event_subscriber') + { + $this->dispatcherService = $dispatcherService; + $this->listenerTag = $listenerTag; + $this->subscriberTag = $subscriberTag; + } + + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) { + return; + } + + $definition = $container->findDefinition($this->dispatcherService); + + foreach ($container->findTaggedServiceIds($this->listenerTag) as $id => $events) { + $def = $container->getDefinition($id); + if (!$def->isPublic()) { + throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event listeners are lazy-loaded.', $id)); + } + + if ($def->isAbstract()) { + throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as event listeners are lazy-loaded.', $id)); + } + + foreach ($events as $event) { + $priority = isset($event['priority']) ? $event['priority'] : 0; + + if (!isset($event['event'])) { + throw new \InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); + } + + if (!isset($event['method'])) { + $event['method'] = 'on'.preg_replace_callback(array( + '/(?<=\b)[a-z]/i', + '/[^a-z0-9]/i', + ), function ($matches) { return strtoupper($matches[0]); }, $event['event']); + $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); + } + + $definition->addMethodCall('addListenerService', array($event['event'], array($id, $event['method']), $priority)); + } + } + + foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) { + $def = $container->getDefinition($id); + if (!$def->isPublic()) { + throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event subscribers are lazy-loaded.', $id)); + } + + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $def->getClass(); + + $refClass = new \ReflectionClass($class); + $interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface'; + if (!$refClass->implementsInterface($interface)) { + throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface)); + } + + $definition->addMethodCall('addSubscriberService', array($id, $class)); + } + } +} diff --git a/src/Symfony/Component/EventDispatcher/Event.php b/src/Symfony/Component/EventDispatcher/Event.php index 42f09eaa5118e..e25e7f1ebae42 100644 --- a/src/Symfony/Component/EventDispatcher/Event.php +++ b/src/Symfony/Component/EventDispatcher/Event.php @@ -76,6 +76,8 @@ public function stopPropagation() * * @param EventDispatcherInterface $dispatcher * + * @deprecated Deprecated in 2.4, to be removed in 3.0. The event dispatcher is passed to the listener call. + * * @api */ public function setDispatcher(EventDispatcherInterface $dispatcher) @@ -88,6 +90,8 @@ public function setDispatcher(EventDispatcherInterface $dispatcher) * * @return EventDispatcherInterface * + * @deprecated Deprecated in 2.4, to be removed in 3.0. The event dispatcher is passed to the listener call. + * * @api */ public function getDispatcher() @@ -100,6 +104,8 @@ public function getDispatcher() * * @return string * + * @deprecated Deprecated in 2.4, to be removed in 3.0. The event name is passed to the listener call. + * * @api */ public function getName() @@ -112,6 +118,8 @@ public function getName() * * @param string $name The event name. * + * @deprecated Deprecated in 2.4, to be removed in 3.0. The event name is passed to the listener call. + * * @api */ public function setName($name) diff --git a/src/Symfony/Component/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php index eb1fb5949efbb..ad48d4311d8e6 100644 --- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php @@ -154,14 +154,14 @@ public function removeSubscriber(EventSubscriberInterface $subscriber) * This method can be overridden to add functionality that is executed * for each listener. * - * @param array[callback] $listeners The event listeners. - * @param string $eventName The name of the event to dispatch. - * @param Event $event The event object to pass to the event handlers/listeners. + * @param callable[] $listeners The event listeners. + * @param string $eventName The name of the event to dispatch. + * @param Event $event The event object to pass to the event handlers/listeners. */ protected function doDispatch($listeners, $eventName, Event $event) { foreach ($listeners as $listener) { - call_user_func($listener, $event); + call_user_func($listener, $event, $eventName, $this); if ($event->isPropagationStopped()) { break; } diff --git a/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php index 71f3ad05215ec..965a0c6bcfb5c 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php @@ -19,13 +19,6 @@ class ContainerAwareEventDispatcherTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - } - public function testAddAListenerService() { $event = new Event(); diff --git a/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php new file mode 100644 index 0000000000000..8ccfabb1ca407 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Tests/Debug/TraceableEventDispatcherTest.php @@ -0,0 +1,171 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Tests\Debug; + +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\Stopwatch\Stopwatch; + +class TraceableEventDispatcherTest extends \PHPUnit_Framework_TestCase +{ + public function testAddRemoveListener() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $tdispatcher->addListener('foo', $listener = function () { ; }); + $listeners = $dispatcher->getListeners('foo'); + $this->assertCount(1, $listeners); + $this->assertSame($listener, $listeners[0]); + + $tdispatcher->removeListener('foo', $listener); + $this->assertCount(0, $dispatcher->getListeners('foo')); + } + + public function testGetListeners() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $tdispatcher->addListener('foo', $listener = function () { ; }); + $this->assertSame($dispatcher->getListeners('foo'), $tdispatcher->getListeners('foo')); + } + + public function testHasListeners() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $this->assertFalse($dispatcher->hasListeners('foo')); + $this->assertFalse($tdispatcher->hasListeners('foo')); + + $tdispatcher->addListener('foo', $listener = function () { ; }); + $this->assertTrue($dispatcher->hasListeners('foo')); + $this->assertTrue($tdispatcher->hasListeners('foo')); + } + + public function testAddRemoveSubscriber() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + + $subscriber = new EventSubscriber(); + + $tdispatcher->addSubscriber($subscriber); + $listeners = $dispatcher->getListeners('foo'); + $this->assertCount(1, $listeners); + $this->assertSame(array($subscriber, 'call'), $listeners[0]); + + $tdispatcher->removeSubscriber($subscriber); + $this->assertCount(0, $dispatcher->getListeners('foo')); + } + + public function testGetCalledListeners() + { + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + $tdispatcher->addListener('foo', $listener = function () { ; }); + + $this->assertEquals(array(), $tdispatcher->getCalledListeners()); + $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getNotCalledListeners()); + + $tdispatcher->dispatch('foo'); + + $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getCalledListeners()); + $this->assertEquals(array(), $tdispatcher->getNotCalledListeners()); + } + + public function testLogger() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); + $tdispatcher->addListener('foo', $listener1 = function () { ; }); + $tdispatcher->addListener('foo', $listener2 = function () { ; }); + + $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); + $logger->expects($this->at(1))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); + + $tdispatcher->dispatch('foo'); + } + + public function testLoggerWithStoppedEvent() + { + $logger = $this->getMock('Psr\Log\LoggerInterface'); + + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); + $tdispatcher->addListener('foo', $listener1 = function (Event $event) { $event->stopPropagation(); }); + $tdispatcher->addListener('foo', $listener2 = function () { ; }); + + $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); + $logger->expects($this->at(1))->method('debug')->with("Listener \"closure\" stopped propagation of the event \"foo\"."); + $logger->expects($this->at(2))->method('debug')->with("Listener \"closure\" was not called for event \"foo\"."); + + $tdispatcher->dispatch('foo'); + } + + public function testDispatchCallListeners() + { + $called = array(); + + $dispatcher = new EventDispatcher(); + $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); + $tdispatcher->addListener('foo', $listener1 = function () use (&$called) { $called[] = 'foo1'; }); + $tdispatcher->addListener('foo', $listener2 = function () use (&$called) { $called[] = 'foo2'; }); + + $tdispatcher->dispatch('foo'); + + $this->assertEquals(array('foo1', 'foo2'), $called); + } + + public function testDispatchNested() + { + $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $loop = 1; + $dispatcher->addListener('foo', $listener1 = function () use ($dispatcher, &$loop) { + ++$loop; + if (2 == $loop) { + $dispatcher->dispatch('foo'); + } + }); + + $dispatcher->dispatch('foo'); + } + + public function testDispatchReusedEventNested() + { + $nestedCall = false; + $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); + $dispatcher->addListener('foo', function (Event $e) use ($dispatcher) { + $dispatcher->dispatch('bar', $e); + }); + $dispatcher->addListener('bar', function (Event $e) use (&$nestedCall) { + $nestedCall = true; + }); + + $this->assertFalse($nestedCall); + $dispatcher->dispatch('foo'); + $this->assertTrue($nestedCall); + } +} + +class EventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array('foo' => 'call'); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php similarity index 92% rename from src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterListenersPassTest.php rename to src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index 8c9fa0856d479..5959db0d4227f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -9,10 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; +namespace Symfony\Component\EventDispatcher\Tests\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass; +use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; class RegisterListenersPassTest extends \PHPUnit_Framework_TestCase { @@ -67,7 +67,7 @@ public function testValidEventSubscriber() ->will($this->returnValue(true)); $definition->expects($this->atLeastOnce()) ->method('getClass') - ->will($this->returnValue('Symfony\Component\HttpKernel\Tests\DependencyInjection\SubscriberService')); + ->will($this->returnValue('Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService')); $builder = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); $builder->expects($this->any()) @@ -83,8 +83,11 @@ public function testValidEventSubscriber() ->method('getDefinition') ->will($this->returnValue($definition)); - $registerListenersPass = new RegisterListenersPass(); + $builder->expects($this->atLeastOnce()) + ->method('findDefinition') + ->will($this->returnValue($definition)); + $registerListenersPass = new RegisterListenersPass(); $registerListenersPass->process($builder); } diff --git a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php index 097c3e48005d1..50af44549b0c3 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php @@ -23,6 +23,9 @@ class EventDispatcherTest extends \PHPUnit_Framework_TestCase const preBar = 'pre.bar'; const postBar = 'post.bar'; + /** + * @var EventDispatcher + */ private $dispatcher; private $listener; @@ -237,7 +240,6 @@ public function testRemoveSubscriberWithMultipleListeners() public function testEventReceivesTheDispatcherInstance() { - $test = $this; $this->dispatcher->addListener('test', function ($event) use (&$dispatcher) { $dispatcher = $event->getDispatcher(); }); @@ -245,6 +247,17 @@ public function testEventReceivesTheDispatcherInstance() $this->assertSame($this->dispatcher, $dispatcher); } + public function testEventReceivesTheDispatcherInstanceAsArgument() + { + $listener = new TestWithDispatcher(); + $this->dispatcher->addListener('test', array($listener, 'foo')); + $this->assertNull($listener->name); + $this->assertNull($listener->dispatcher); + $this->dispatcher->dispatch('test'); + $this->assertEquals('test', $listener->name); + $this->assertSame($this->dispatcher, $listener->dispatcher); + } + /** * @see https://bugs.php.net/bug.php?id=62976 * @@ -289,6 +302,18 @@ public function postFoo(Event $e) } } +class TestWithDispatcher +{ + public $name; + public $dispatcher; + + public function foo(Event $e, $name, $dispatcher) + { + $this->name = $name; + $this->dispatcher = $dispatcher; + } +} + class TestEventSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() diff --git a/src/Symfony/Component/EventDispatcher/Tests/EventTest.php b/src/Symfony/Component/EventDispatcher/Tests/EventTest.php index 0600ac2aea532..7a20fe6bf3a41 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/EventTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/EventTest.php @@ -35,7 +35,7 @@ class EventTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->event = new Event; + $this->event = new Event(); $this->dispatcher = new EventDispatcher(); } diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index 1db2ecfd6c870..6343b5d1d9c74 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -19,7 +19,9 @@ "php": ">=5.3.3" }, "require-dev": { - "symfony/dependency-injection": "~2.0" + "symfony/dependency-injection": "~2.0", + "symfony/stopwatch": "~2.2", + "psr/log": "~1.0" }, "suggest": { "symfony/dependency-injection": "", @@ -32,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/ExpressionLanguage/.gitignore b/src/Symfony/Component/ExpressionLanguage/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md b/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md new file mode 100644 index 0000000000000..eacb5a60fbdca --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +2.4.0 +----- + + * added the component diff --git a/src/Symfony/Component/ExpressionLanguage/Compiler.php b/src/Symfony/Component/ExpressionLanguage/Compiler.php new file mode 100644 index 0000000000000..d9f29696474e1 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Compiler.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\ExpressionLanguage; + +/** + * Compiles a node to PHP code. + * + * @author Fabien Potencier + */ +class Compiler +{ + private $source; + private $functions; + + public function __construct(array $functions) + { + $this->functions = $functions; + } + + public function getFunction($name) + { + return $this->functions[$name]; + } + + /** + * Gets the current PHP code after compilation. + * + * @return string The PHP code + */ + public function getSource() + { + return $this->source; + } + + public function reset() + { + $this->source = ''; + + return $this; + } + + /** + * Compiles a node. + * + * @param Node\Node $node The node to compile + * + * @return Compiler The current compiler instance + */ + public function compile(Node\Node $node) + { + $node->compile($this); + + return $this; + } + + public function subcompile(Node\Node $node) + { + $current = $this->source; + $this->source = ''; + + $node->compile($this); + + $source = $this->source; + $this->source = $current; + + return $source; + } + + /** + * Adds a raw string to the compiled code. + * + * @param string $string The string + * + * @return Compiler The current compiler instance + */ + public function raw($string) + { + $this->source .= $string; + + return $this; + } + + /** + * Adds a quoted string to the compiled code. + * + * @param string $value The string + * + * @return Compiler The current compiler instance + */ + public function string($value) + { + $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); + + return $this; + } + + /** + * Returns a PHP representation of a given value. + * + * @param mixed $value The value to convert + * + * @return Compiler The current compiler instance + */ + public function repr($value) + { + if (is_int($value) || is_float($value)) { + if (false !== $locale = setlocale(LC_NUMERIC, 0)) { + setlocale(LC_NUMERIC, 'C'); + } + + $this->raw($value); + + if (false !== $locale) { + setlocale(LC_NUMERIC, $locale); + } + } elseif (null === $value) { + $this->raw('null'); + } elseif (is_bool($value)) { + $this->raw($value ? 'true' : 'false'); + } elseif (is_array($value)) { + $this->raw('array('); + $first = true; + foreach ($value as $key => $value) { + if (!$first) { + $this->raw(', '); + } + $first = false; + $this->repr($key); + $this->raw(' => '); + $this->repr($value); + } + $this->raw(')'); + } else { + $this->string($value); + } + + return $this; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Expression.php b/src/Symfony/Component/ExpressionLanguage/Expression.php new file mode 100644 index 0000000000000..f5a3820309231 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Expression.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\ExpressionLanguage; + +/** + * Represents an expression. + * + * @author Fabien Potencier + */ +class Expression +{ + protected $expression; + + /** + * Constructor. + * + * @param string $expression An expression + */ + public function __construct($expression) + { + $this->expression = (string) $expression; + } + + /** + * Gets the expression. + * + * @return string The expression + */ + public function __toString() + { + return $this->expression; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php new file mode 100644 index 0000000000000..57dae3a9ca472 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +use Symfony\Component\ExpressionLanguage\ParserCache\ArrayParserCache; +use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface; + +/** + * Allows to compile and evaluate expressions written in your own DSL. + * + * @author Fabien Potencier + */ +class ExpressionLanguage +{ + /** + * @var ParserCacheInterface + */ + private $cache; + private $lexer; + private $parser; + private $compiler; + + protected $functions = array(); + + public function __construct(ParserCacheInterface $cache = null) + { + $this->cache = $cache ?: new ArrayParserCache(); + $this->registerFunctions(); + } + + /** + * Compiles an expression source code. + * + * @param Expression|string $expression The expression to compile + * @param array $names An array of valid names + * + * @return string The compiled PHP source code + */ + public function compile($expression, $names = array()) + { + return $this->getCompiler()->compile($this->parse($expression, $names)->getNodes())->getSource(); + } + + /** + * Evaluate an expression. + * + * @param Expression|string $expression The expression to compile + * @param array $values An array of values + * + * @return string The result of the evaluation of the expression + */ + public function evaluate($expression, $values = array()) + { + return $this->parse($expression, array_keys($values))->getNodes()->evaluate($this->functions, $values); + } + + /** + * Parses an expression. + * + * @param Expression|string $expression The expression to parse + * @param array $names An array of valid names + * + * @return ParsedExpression A ParsedExpression instance + */ + public function parse($expression, $names) + { + if ($expression instanceof ParsedExpression) { + return $expression; + } + + $key = $expression.'//'.implode('-', $names); + + if (null === $parsedExpression = $this->cache->fetch($key)) { + $nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names); + $parsedExpression = new ParsedExpression((string) $expression, $nodes); + + $this->cache->save($key, $parsedExpression); + } + + return $parsedExpression; + } + + /** + * Registers a function. + * + * @param string $name The function name + * @param callable $compiler A callable able to compile the function + * @param callable $evaluator A callable able to evaluate the function + */ + public function register($name, $compiler, $evaluator) + { + $this->functions[$name] = array('compiler' => $compiler, 'evaluator' => $evaluator); + } + + protected function registerFunctions() + { + $this->register('constant', function ($constant) { + return sprintf('constant(%s)', $constant); + }, function (array $values, $constant) { + return constant($constant); + }); + } + + private function getLexer() + { + if (null === $this->lexer) { + $this->lexer = new Lexer(); + } + + return $this->lexer; + } + + private function getParser() + { + if (null === $this->parser) { + $this->parser = new Parser($this->functions); + } + + return $this->parser; + } + + private function getCompiler() + { + if (null === $this->compiler) { + $this->compiler = new Compiler($this->functions); + } + + return $this->compiler->reset(); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/LICENSE b/src/Symfony/Component/ExpressionLanguage/LICENSE new file mode 100644 index 0000000000000..0b3292cf90235 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2014 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/ExpressionLanguage/Lexer.php b/src/Symfony/Component/ExpressionLanguage/Lexer.php new file mode 100644 index 0000000000000..aba51bb3fb5bc --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Lexer.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Lexes an expression. + * + * @author Fabien Potencier + */ +class Lexer +{ + /** + * Tokenizes an expression. + * + * @param string $expression The expression to tokenize + * + * @return TokenStream A token stream instance + * + * @throws SyntaxError + */ + public function tokenize($expression) + { + $expression = str_replace(array("\r", "\n", "\t", "\v", "\f"), ' ', $expression); + $cursor = 0; + $tokens = array(); + $brackets = array(); + $end = strlen($expression); + + while ($cursor < $end) { + if (' ' == $expression[$cursor]) { + ++$cursor; + + continue; + } + + if (preg_match('/[0-9]+(?:\.[0-9]+)?/A', $expression, $match, null, $cursor)) { + // numbers + $number = (float) $match[0]; // floats + if (ctype_digit($match[0]) && $number <= PHP_INT_MAX) { + $number = (int) $match[0]; // integers lower than the maximum + } + $tokens[] = new Token(Token::NUMBER_TYPE, $number, $cursor + 1); + $cursor += strlen($match[0]); + } elseif (false !== strpos('([{', $expression[$cursor])) { + // opening bracket + $brackets[] = array($expression[$cursor], $cursor); + + $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); + ++$cursor; + } elseif (false !== strpos(')]}', $expression[$cursor])) { + // closing bracket + if (empty($brackets)) { + throw new SyntaxError(sprintf('Unexpected "%s"', $expression[$cursor]), $cursor); + } + + list($expect, $cur) = array_pop($brackets); + if ($expression[$cursor] != strtr($expect, '([{', ')]}')) { + throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); + } + + $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); + ++$cursor; + } elseif (preg_match('/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As', $expression, $match, null, $cursor)) { + // strings + $tokens[] = new Token(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)), $cursor + 1); + $cursor += strlen($match[0]); + } elseif (preg_match('/not in(?=[\s(])|\!\=\=|not(?=[\s(])|and(?=[\s(])|\=\=\=|\>\=|or(?=[\s(])|\<\=|\*\*|\.\.|in(?=[\s(])|&&|\|\||matches|\=\=|\!\=|\*|~|%|\/|\>|\||\!|\^|&|\+|\<|\-/A', $expression, $match, null, $cursor)) { + // operators + $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); + $cursor += strlen($match[0]); + } elseif (false !== strpos('.,?:', $expression[$cursor])) { + // punctuation + $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); + ++$cursor; + } elseif (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $expression, $match, null, $cursor)) { + // names + $tokens[] = new Token(Token::NAME_TYPE, $match[0], $cursor + 1); + $cursor += strlen($match[0]); + } else { + // unlexable + throw new SyntaxError(sprintf('Unexpected character "%s"', $expression[$cursor]), $cursor); + } + } + + $tokens[] = new Token(Token::EOF_TYPE, null, $cursor + 1); + + if (!empty($brackets)) { + list($expect, $cur) = array_pop($brackets); + throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur); + } + + return new TokenStream($tokens); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ArgumentsNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ArgumentsNode.php new file mode 100644 index 0000000000000..f440101684396 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ArgumentsNode.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\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ArgumentsNode extends ArrayNode +{ + public function compile(Compiler $compiler) + { + $this->compileArguments($compiler, false); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ArrayNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ArrayNode.php new file mode 100644 index 0000000000000..f110f542ad7d2 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ArrayNode.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ArrayNode extends Node +{ + protected $index; + + public function __construct() + { + $this->index = -1; + } + + public function addElement(Node $value, Node $key = null) + { + if (null === $key) { + $key = new ConstantNode(++$this->index); + } + + array_push($this->nodes, $key, $value); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Compiler instance + */ + public function compile(Compiler $compiler) + { + $compiler->raw('array('); + $this->compileArguments($compiler); + $compiler->raw(')'); + } + + public function evaluate($functions, $values) + { + $result = array(); + foreach ($this->getKeyValuePairs() as $pair) { + $result[$pair['key']->evaluate($functions, $values)] = $pair['value']->evaluate($functions, $values); + } + + return $result; + } + + protected function getKeyValuePairs() + { + $pairs = array(); + foreach (array_chunk($this->nodes, 2) as $pair) { + $pairs[] = array('key' => $pair[0], 'value' => $pair[1]); + } + + return $pairs; + } + + protected function compileArguments(Compiler $compiler, $withKeys = true) + { + $first = true; + foreach ($this->getKeyValuePairs() as $pair) { + if (!$first) { + $compiler->raw(', '); + } + $first = false; + + if ($withKeys) { + $compiler + ->compile($pair['key']) + ->raw(' => ') + ; + } + + $compiler->compile($pair['value']); + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php b/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php new file mode 100644 index 0000000000000..15341e86c536e --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/BinaryNode.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class BinaryNode extends Node +{ + private static $operators = array( + '~' => '.', + 'and' => '&&', + 'or' => '||', + ); + + private static $functions = array( + '**' => 'pow', + '..' => 'range', + 'in' => 'in_array', + 'not in' => '!in_array', + ); + + public function __construct($operator, Node $left, Node $right) + { + parent::__construct( + array('left' => $left, 'right' => $right), + array('operator' => $operator) + ); + } + + public function compile(Compiler $compiler) + { + $operator = $this->attributes['operator']; + + if ('matches' == $operator) { + $compiler + ->raw('preg_match(') + ->compile($this->nodes['right']) + ->raw(', ') + ->compile($this->nodes['left']) + ->raw(')') + ; + + return; + } + + if (isset(self::$functions[$operator])) { + $compiler + ->raw(sprintf('%s(', self::$functions[$operator])) + ->compile($this->nodes['left']) + ->raw(', ') + ->compile($this->nodes['right']) + ->raw(')') + ; + + return; + } + + if (isset(self::$operators[$operator])) { + $operator = self::$operators[$operator]; + } + + $compiler + ->raw('(') + ->compile($this->nodes['left']) + ->raw(' ') + ->raw($operator) + ->raw(' ') + ->compile($this->nodes['right']) + ->raw(')') + ; + } + + public function evaluate($functions, $values) + { + $operator = $this->attributes['operator']; + $left = $this->nodes['left']->evaluate($functions, $values); + $right = $this->nodes['right']->evaluate($functions, $values); + + if (isset(self::$functions[$operator])) { + if ('not in' == $operator) { + return !call_user_func('in_array', $left, $right); + } + + return call_user_func(self::$functions[$operator], $left, $right); + } + + switch ($operator) { + case 'or': + case '||': + return $left || $right; + case 'and': + case '&&': + return $left && $right; + case '|': + return $left | $right; + case '^': + return $left ^ $right; + case '&': + return $left & $right; + case '==': + return $left == $right; + case '===': + return $left === $right; + case '!=': + return $left != $right; + case '!==': + return $left !== $right; + case '<': + return $left < $right; + case '>': + return $left > $right; + case '>=': + return $left >= $right; + case '<=': + return $left <= $right; + case 'not in': + return !in_array($left, $right); + case 'in': + return in_array($left, $right); + case '+': + return $left + $right; + case '-': + return $left - $right; + case '~': + return $left.$right; + case '*': + return $left * $right; + case '/': + return $left / $right; + case '%': + return $left % $right; + case 'matches': + return preg_match($right, $left); + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ConditionalNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ConditionalNode.php new file mode 100644 index 0000000000000..7de1e3de12691 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ConditionalNode.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\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ConditionalNode extends Node +{ + public function __construct(Node $expr1, Node $expr2, Node $expr3) + { + parent::__construct( + array('expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3) + ); + } + + public function compile(Compiler $compiler) + { + $compiler + ->raw('((') + ->compile($this->nodes['expr1']) + ->raw(') ? (') + ->compile($this->nodes['expr2']) + ->raw(') : (') + ->compile($this->nodes['expr3']) + ->raw('))') + ; + } + + public function evaluate($functions, $values) + { + if ($this->nodes['expr1']->evaluate($functions, $values)) { + return $this->nodes['expr2']->evaluate($functions, $values); + } + + return $this->nodes['expr3']->evaluate($functions, $values); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/ConstantNode.php b/src/Symfony/Component/ExpressionLanguage/Node/ConstantNode.php new file mode 100644 index 0000000000000..7842e5787776c --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/ConstantNode.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class ConstantNode extends Node +{ + public function __construct($value) + { + parent::__construct( + array(), + array('value' => $value) + ); + } + + public function compile(Compiler $compiler) + { + $compiler->repr($this->attributes['value']); + } + + public function evaluate($functions, $values) + { + return $this->attributes['value']; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/FunctionNode.php b/src/Symfony/Component/ExpressionLanguage/Node/FunctionNode.php new file mode 100644 index 0000000000000..4a290a488d98c --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/FunctionNode.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class FunctionNode extends Node +{ + public function __construct($name, Node $arguments) + { + parent::__construct( + array('arguments' => $arguments), + array('name' => $name) + ); + } + + public function compile(Compiler $compiler) + { + $arguments = array(); + foreach ($this->nodes['arguments']->nodes as $node) { + $arguments[] = $compiler->subcompile($node); + } + + $function = $compiler->getFunction($this->attributes['name']); + + $compiler->raw(call_user_func_array($function['compiler'], $arguments)); + } + + public function evaluate($functions, $values) + { + $arguments = array($values); + foreach ($this->nodes['arguments']->nodes as $node) { + $arguments[] = $node->evaluate($functions, $values); + } + + return call_user_func_array($functions[$this->attributes['name']]['evaluator'], $arguments); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php b/src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php new file mode 100644 index 0000000000000..c7f9c411135b9 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/GetAttrNode.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class GetAttrNode extends Node +{ + const PROPERTY_CALL = 1; + const METHOD_CALL = 2; + const ARRAY_CALL = 3; + + public function __construct(Node $node, Node $attribute, ArrayNode $arguments, $type) + { + parent::__construct( + array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments), + array('type' => $type) + ); + } + + public function compile(Compiler $compiler) + { + switch ($this->attributes['type']) { + case self::PROPERTY_CALL: + $compiler + ->compile($this->nodes['node']) + ->raw('->') + ->raw($this->nodes['attribute']->attributes['value']) + ; + break; + + case self::METHOD_CALL: + $compiler + ->compile($this->nodes['node']) + ->raw('->') + ->raw($this->nodes['attribute']->attributes['value']) + ->raw('(') + ->compile($this->nodes['arguments']) + ->raw(')') + ; + break; + + case self::ARRAY_CALL: + $compiler + ->compile($this->nodes['node']) + ->raw('[') + ->compile($this->nodes['attribute'])->raw(']') + ; + break; + } + } + + public function evaluate($functions, $values) + { + switch ($this->attributes['type']) { + case self::PROPERTY_CALL: + $obj = $this->nodes['node']->evaluate($functions, $values); + if (!is_object($obj)) { + throw new \RuntimeException('Unable to get a property on a non-object.'); + } + + $property = $this->nodes['attribute']->attributes['value']; + + return $obj->$property; + + case self::METHOD_CALL: + $obj = $this->nodes['node']->evaluate($functions, $values); + if (!is_object($obj)) { + throw new \RuntimeException('Unable to get a property on a non-object.'); + } + + return call_user_func_array(array($obj, $this->nodes['attribute']->evaluate($functions, $values)), $this->nodes['arguments']->evaluate($functions, $values)); + + case self::ARRAY_CALL: + $array = $this->nodes['node']->evaluate($functions, $values); + if (!is_array($array) && !$array instanceof \ArrayAccess) { + throw new \RuntimeException('Unable to get an item on a non-array.'); + } + + return $array[$this->nodes['attribute']->evaluate($functions, $values)]; + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/NameNode.php b/src/Symfony/Component/ExpressionLanguage/Node/NameNode.php new file mode 100644 index 0000000000000..3d39f4077ece4 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/NameNode.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class NameNode extends Node +{ + public function __construct($name) + { + parent::__construct( + array(), + array('name' => $name) + ); + } + + public function compile(Compiler $compiler) + { + $compiler->raw('$'.$this->attributes['name']); + } + + public function evaluate($functions, $values) + { + return $values[$this->attributes['name']]; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/Node.php b/src/Symfony/Component/ExpressionLanguage/Node/Node.php new file mode 100644 index 0000000000000..da49d6b4b29cc --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/Node.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\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +/** + * Represents a node in the AST. + * + * @author Fabien Potencier + */ +class Node +{ + public $nodes = array(); + public $attributes = array(); + + /** + * Constructor. + * + * @param array $nodes An array of nodes + * @param array $attributes An array of attributes + */ + public function __construct(array $nodes = array(), array $attributes = array()) + { + $this->nodes = $nodes; + $this->attributes = $attributes; + } + + public function __toString() + { + $attributes = array(); + foreach ($this->attributes as $name => $value) { + $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + } + + $repr = array(str_replace('Symfony\Component\ExpressionLanguage\Node\\', '', get_class($this)).'('.implode(', ', $attributes)); + + if (count($this->nodes)) { + foreach ($this->nodes as $node) { + foreach (explode("\n", (string) $node) as $line) { + $repr[] = ' '.$line; + } + } + + $repr[] = ')'; + } else { + $repr[0] .= ')'; + } + + return implode("\n", $repr); + } + + public function compile(Compiler $compiler) + { + foreach ($this->nodes as $node) { + $node->compile($compiler); + } + } + + public function evaluate($functions, $values) + { + $results = array(); + foreach ($this->nodes as $node) { + $results[] = $node->evaluate($functions, $values); + } + + return $results; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Node/UnaryNode.php b/src/Symfony/Component/ExpressionLanguage/Node/UnaryNode.php new file mode 100644 index 0000000000000..fd980e5b7a88c --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Node/UnaryNode.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +class UnaryNode extends Node +{ + private static $operators = array( + '!' => '!', + 'not' => '!', + '+' => '+', + '-' => '-', + ); + + public function __construct($operator, Node $node) + { + parent::__construct( + array('node' => $node), + array('operator' => $operator) + ); + } + + public function compile(Compiler $compiler) + { + $compiler + ->raw('(') + ->raw(self::$operators[$this->attributes['operator']]) + ->compile($this->nodes['node']) + ->raw(')') + ; + } + + public function evaluate($functions, $values) + { + $value = $this->nodes['node']->evaluate($functions, $values); + switch ($this->attributes['operator']) { + case 'not': + case '!': + return !$value; + case '-': + return -$value; + } + + return $value; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/ParsedExpression.php b/src/Symfony/Component/ExpressionLanguage/ParsedExpression.php new file mode 100644 index 0000000000000..61bf5807c49e7 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/ParsedExpression.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\ExpressionLanguage; + +use Symfony\Component\ExpressionLanguage\Node\Node; + +/** + * Represents an already parsed expression. + * + * @author Fabien Potencier + */ +class ParsedExpression extends Expression +{ + private $nodes; + + /** + * Constructor. + * + * @param string $expression An expression + * @param Node $nodes A Node representing the expression + */ + public function __construct($expression, Node $nodes) + { + parent::__construct($expression); + + $this->nodes = $nodes; + } + + public function getNodes() + { + return $this->nodes; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Parser.php b/src/Symfony/Component/ExpressionLanguage/Parser.php new file mode 100644 index 0000000000000..f0d23212419e2 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Parser.php @@ -0,0 +1,360 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Parsers a token stream. + * + * This parser implements a "Precedence climbing" algorithm. + * + * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm + * @see http://en.wikipedia.org/wiki/Operator-precedence_parser + * + * @author Fabien Potencier + */ +class Parser +{ + const OPERATOR_LEFT = 1; + const OPERATOR_RIGHT = 2; + + private $stream; + private $unaryOperators; + private $binaryOperators; + private $functions; + private $names; + + public function __construct(array $functions) + { + $this->functions = $functions; + + $this->unaryOperators = array( + 'not' => array('precedence' => 50), + '!' => array('precedence' => 50), + '-' => array('precedence' => 500), + '+' => array('precedence' => 500), + ); + $this->binaryOperators = array( + 'or' => array('precedence' => 10, 'associativity' => Parser::OPERATOR_LEFT), + '||' => array('precedence' => 10, 'associativity' => Parser::OPERATOR_LEFT), + 'and' => array('precedence' => 15, 'associativity' => Parser::OPERATOR_LEFT), + '&&' => array('precedence' => 15, 'associativity' => Parser::OPERATOR_LEFT), + '|' => array('precedence' => 16, 'associativity' => Parser::OPERATOR_LEFT), + '^' => array('precedence' => 17, 'associativity' => Parser::OPERATOR_LEFT), + '&' => array('precedence' => 18, 'associativity' => Parser::OPERATOR_LEFT), + '==' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '===' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '!=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '!==' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '<' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '>' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '>=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '<=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + 'not in' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + 'in' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + 'matches' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT), + '..' => array('precedence' => 25, 'associativity' => Parser::OPERATOR_LEFT), + '+' => array('precedence' => 30, 'associativity' => Parser::OPERATOR_LEFT), + '-' => array('precedence' => 30, 'associativity' => Parser::OPERATOR_LEFT), + '~' => array('precedence' => 40, 'associativity' => Parser::OPERATOR_LEFT), + '*' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT), + '/' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT), + '%' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT), + '**' => array('precedence' => 200, 'associativity' => Parser::OPERATOR_RIGHT), + ); + } + + /** + * Converts a token stream to a node tree. + * + * @param TokenStream $stream A token stream instance + * @param array $names An array of valid names + * + * @return Node A node tree + * + * @throws SyntaxError + */ + public function parse(TokenStream $stream, $names = array()) + { + $this->stream = $stream; + $this->names = $names; + + $node = $this->parseExpression(); + if (!$stream->isEOF()) { + throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $stream->current->type, $stream->current->value), $stream->current->cursor); + } + + return $node; + } + + public function parseExpression($precedence = 0) + { + $expr = $this->getPrimary(); + $token = $this->stream->current; + while ($token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->value]) && $this->binaryOperators[$token->value]['precedence'] >= $precedence) { + $op = $this->binaryOperators[$token->value]; + $this->stream->next(); + + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $expr = new Node\BinaryNode($token->value, $expr, $expr1); + + $token = $this->stream->current; + } + + if (0 === $precedence) { + return $this->parseConditionalExpression($expr); + } + + return $expr; + } + + protected function getPrimary() + { + $token = $this->stream->current; + + if ($token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->value])) { + $operator = $this->unaryOperators[$token->value]; + $this->stream->next(); + $expr = $this->parseExpression($operator['precedence']); + + return $this->parsePostfixExpression(new Node\UnaryNode($token->value, $expr)); + } + + if ($token->test(Token::PUNCTUATION_TYPE, '(')) { + $this->stream->next(); + $expr = $this->parseExpression(); + $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); + + return $this->parsePostfixExpression($expr); + } + + return $this->parsePrimaryExpression(); + } + + protected function parseConditionalExpression($expr) + { + while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '?')) { + $this->stream->next(); + if (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { + $expr2 = $this->parseExpression(); + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { + $this->stream->next(); + $expr3 = $this->parseExpression(); + } else { + $expr3 = new Node\ConstantNode(null); + } + } else { + $this->stream->next(); + $expr2 = $expr; + $expr3 = $this->parseExpression(); + } + + $expr = new Node\ConditionalNode($expr, $expr2, $expr3); + } + + return $expr; + } + + public function parsePrimaryExpression() + { + $token = $this->stream->current; + switch ($token->type) { + case Token::NAME_TYPE: + $this->stream->next(); + switch ($token->value) { + case 'true': + case 'TRUE': + return new Node\ConstantNode(true); + + case 'false': + case 'FALSE': + return new Node\ConstantNode(false); + + case 'null': + case 'NULL': + return new Node\ConstantNode(null); + + default: + if ('(' === $this->stream->current->value) { + if (false === isset($this->functions[$token->value])) { + throw new SyntaxError(sprintf('The function "%s" does not exist', $token->value), $token->cursor); + } + + $node = new Node\FunctionNode($token->value, $this->parseArguments()); + } else { + if (!in_array($token->value, $this->names)) { + throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor); + } + + $node = new Node\NameNode($token->value); + } + } + break; + + case Token::NUMBER_TYPE: + case Token::STRING_TYPE: + $this->stream->next(); + + return new Node\ConstantNode($token->value); + + default: + if ($token->test(Token::PUNCTUATION_TYPE, '[')) { + $node = $this->parseArrayExpression(); + } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { + $node = $this->parseHashExpression(); + } else { + throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s"', $token->type, $token->value), $token->cursor); + } + } + + return $this->parsePostfixExpression($node); + } + + public function parseArrayExpression() + { + $this->stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); + + $node = new Node\ArrayNode(); + $first = true; + while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { + if (!$first) { + $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); + + // trailing ,? + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { + break; + } + } + $first = false; + + $node->addElement($this->parseExpression()); + } + $this->stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); + + return $node; + } + + public function parseHashExpression() + { + $this->stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); + + $node = new Node\ArrayNode(); + $first = true; + while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); + + // trailing ,? + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + // a hash key can be: + // + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) + if ($this->stream->current->test(Token::STRING_TYPE) || $this->stream->current->test(Token::NAME_TYPE) || $this->stream->current->test(Token::NUMBER_TYPE)) { + $key = new Node\ConstantNode($this->stream->current->value); + $this->stream->next(); + } elseif ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { + $key = $this->parseExpression(); + } else { + $current = $this->stream->current; + + throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', $current->type, $current->value), $current->cursor); + } + + $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); + $value = $this->parseExpression(); + + $node->addElement($value, $key); + } + $this->stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); + + return $node; + } + + public function parsePostfixExpression($node) + { + $token = $this->stream->current; + while ($token->type == Token::PUNCTUATION_TYPE) { + if ('.' === $token->value) { + $this->stream->next(); + $token = $this->stream->current; + $this->stream->next(); + + if ( + $token->type !== Token::NAME_TYPE + && + $token->type !== Token::NUMBER_TYPE + && + // operators line "not" are valid method or property names + ($token->type !== Token::OPERATOR_TYPE && preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value)) + ) { + throw new SyntaxError('Expected name or number', $token->cursor); + } + + $arg = new Node\ConstantNode($token->value); + + $arguments = new Node\ArgumentsNode(); + if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { + $type = Node\GetAttrNode::METHOD_CALL; + foreach ($this->parseArguments()->nodes as $n) { + $arguments->addElement($n); + } + } else { + $type = Node\GetAttrNode::PROPERTY_CALL; + } + + $node = new Node\GetAttrNode($node, $arg, $arguments, $type); + } elseif ('[' === $token->value) { + if ($node instanceof Node\GetAttrNode && Node\GetAttrNode::METHOD_CALL === $node->attributes['type'] && version_compare(PHP_VERSION, '5.4.0', '<')) { + throw new SyntaxError('Array calls on a method call is only supported on PHP 5.4+', $token->cursor); + } + + $this->stream->next(); + $arg = $this->parseExpression(); + $this->stream->expect(Token::PUNCTUATION_TYPE, ']'); + + $node = new Node\GetAttrNode($node, $arg, new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL); + } else { + break; + } + + $token = $this->stream->current; + } + + return $node; + } + + /** + * Parses arguments. + */ + public function parseArguments() + { + $args = array(); + $this->stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ')')) { + if (!empty($args)) { + $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + } + + $args[] = $this->parseExpression(); + } + $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Node\Node($args); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.php b/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.php new file mode 100644 index 0000000000000..1d2aedb514501 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.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\ExpressionLanguage\ParserCache; + +use Symfony\Component\ExpressionLanguage\ParsedExpression; + +/** + * @author Adrien Brault + */ +class ArrayParserCache implements ParserCacheInterface +{ + /** + * @var array + */ + private $cache = array(); + + /** + * {@inheritdoc} + */ + public function fetch($key) + { + return isset($this->cache[$key]) ? $this->cache[$key] : null; + } + + /** + * {@inheritdoc} + */ + public function save($key, ParsedExpression $expression) + { + $this->cache[$key] = $expression; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheInterface.php b/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheInterface.php new file mode 100644 index 0000000000000..4b7a4b6d6fe6b --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheInterface.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\Component\ExpressionLanguage\ParserCache; + +use Symfony\Component\ExpressionLanguage\ParsedExpression; + +/** + * @author Adrien Brault + */ +interface ParserCacheInterface +{ + /** + * Saves an expression in the cache. + * + * @param string $key The cache key + * @param ParsedExpression $expression A ParsedExpression instance to store in the cache + */ + public function save($key, ParsedExpression $expression); + + /** + * Fetches an expression from the cache. + * + * @param string $key The cache key + * + * @return ParsedExpression|null + */ + public function fetch($key); +} diff --git a/src/Symfony/Component/ExpressionLanguage/README.md b/src/Symfony/Component/ExpressionLanguage/README.md new file mode 100644 index 0000000000000..734e889a7fde3 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/README.md @@ -0,0 +1,43 @@ +ExpressionLanguage Component +============================ + +The ExpressionLanguage component provides an engine that can compile and +evaluate expressions: + + use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + + $language = new ExpressionLanguage(); + + echo $language->evaluate('1 + foo', array('foo' => 2)); + // would output 3 + + echo $language->compile('1 + foo', array('foo')); + // would output (1 + $foo) + +By default, the engine implements simple math and logic functions, method +calls, property accesses, and array accesses. + +You can extend your DSL with functions: + + $compiler = function ($arg) { + return sprintf('strtoupper(%s)', $arg); + }; + $evaluator = function (array $variables, $value) { + return strtoupper($value); + }; + $language->register('upper', $compiler, $evaluator); + + echo $language->evaluate('"foo" ~ upper(foo)', array('foo' => 'bar')); + // would output fooBAR + + echo $language->compile('"foo" ~ upper(foo)'); + // would output ("foo" . strtoupper($foo)) + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/ExpressionLanguage/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Component/ExpressionLanguage/Resources/bin/generate_operator_regex.php b/src/Symfony/Component/ExpressionLanguage/Resources/bin/generate_operator_regex.php new file mode 100644 index 0000000000000..02ed38510657f --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Resources/bin/generate_operator_regex.php @@ -0,0 +1,14 @@ +', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'matches', '**'); +$operators = array_combine($operators, array_map('strlen', $operators)); +arsort($operators); + +$regex = array(); +foreach ($operators as $operator => $length) { + // an operator that ends with a character must be followed by + // a whitespace or a parenthesis + $regex[] = preg_quote($operator, '/').(ctype_alpha($operator[$length - 1]) ? '(?=[\s(])' : ''); +} + +echo '/'.implode('|', $regex).'/A'; diff --git a/src/Symfony/Component/ExpressionLanguage/SerializedParsedExpression.php b/src/Symfony/Component/ExpressionLanguage/SerializedParsedExpression.php new file mode 100644 index 0000000000000..ced25dbc99b1c --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/SerializedParsedExpression.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Represents an already parsed expression. + * + * @author Fabien Potencier + */ +class SerializedParsedExpression extends ParsedExpression +{ + private $nodes; + + /** + * Constructor. + * + * @param string $expression An expression + * @param string $nodes The serialized nodes for the expression + */ + public function __construct($expression, $nodes) + { + $this->expression = (string) $expression; + $this->nodes = $nodes; + } + + public function getNodes() + { + return unserialize($this->nodes); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/SyntaxError.php b/src/Symfony/Component/ExpressionLanguage/SyntaxError.php new file mode 100644 index 0000000000000..d149c00768ad8 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/SyntaxError.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +class SyntaxError extends \LogicException +{ + public function __construct($message, $cursor = 0) + { + parent::__construct(sprintf('%s around position %d.', $message, $cursor)); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php new file mode 100644 index 0000000000000..170ac40d0a672 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.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\ExpressionLanguage\Tests; + +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\ParsedExpression; + +class ExpressionLanguageTest extends \PHPUnit_Framework_TestCase +{ + public function testCachedParse() + { + $cacheMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $savedParsedExpression = null; + $expressionLanguage = new ExpressionLanguage($cacheMock); + + $cacheMock + ->expects($this->exactly(2)) + ->method('fetch') + ->with('1 + 1//') + ->will($this->returnCallback(function () use (&$savedParsedExpression) { + return $savedParsedExpression; + })) + ; + $cacheMock + ->expects($this->exactly(1)) + ->method('save') + ->with('1 + 1//', $this->isInstanceOf('Symfony\Component\ExpressionLanguage\ParsedExpression')) + ->will($this->returnCallback(function ($key, $expression) use (&$savedParsedExpression) { + $savedParsedExpression = $expression; + })) + ; + + $parsedExpression = $expressionLanguage->parse('1 + 1', array()); + $this->assertSame($savedParsedExpression, $parsedExpression); + + $parsedExpression = $expressionLanguage->parse('1 + 1', array()); + $this->assertSame($savedParsedExpression, $parsedExpression); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionTest.php new file mode 100644 index 0000000000000..be87321fd3a9d --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionTest.php @@ -0,0 +1,18 @@ +assertEquals($expression, $unserializedExpression); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php new file mode 100644 index 0000000000000..25c1e5ac5958d --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/LexerTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests; + +use Symfony\Component\ExpressionLanguage\Lexer; +use Symfony\Component\ExpressionLanguage\Token; +use Symfony\Component\ExpressionLanguage\TokenStream; + +class LexerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getTokenizeData + */ + public function testTokenize($tokens, $expression) + { + $tokens[] = new Token('end of expression', null, strlen($expression) + 1); + $lexer = new Lexer(); + $this->assertEquals(new TokenStream($tokens), $lexer->tokenize($expression)); + } + + public function getTokenizeData() + { + return array( + array( + array(new Token('name', 'a', 3)), + ' a ', + ), + array( + array(new Token('name', 'a', 1)), + 'a', + ), + array( + array(new Token('string', 'foo', 1)), + '"foo"', + ), + array( + array(new Token('number', '3', 1)), + '3', + ), + array( + array(new Token('operator', '+', 1)), + '+', + ), + array( + array(new Token('punctuation', '.', 1)), + '.', + ), + array( + array( + new Token('punctuation', '(', 1), + new Token('number', '3', 2), + new Token('operator', '+', 4), + new Token('number', '5', 6), + new Token('punctuation', ')', 7), + new Token('operator', '~', 9), + new Token('name', 'foo', 11), + new Token('punctuation', '(', 14), + new Token('string', 'bar', 15), + new Token('punctuation', ')', 20), + new Token('punctuation', '.', 21), + new Token('name', 'baz', 22), + new Token('punctuation', '[', 25), + new Token('number', '4', 26), + new Token('punctuation', ']', 27), + ), + '(3 + 5) ~ foo("bar").baz[4]', + ), + array( + array(new Token('operator', '..', 1)), + '..', + ), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/AbstractNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/AbstractNodeTest.php new file mode 100644 index 0000000000000..58b0e177e8e1a --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/AbstractNodeTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Compiler; + +abstract class AbstractNodeTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getEvaluateData + */ + public function testEvaluate($expected, $node, $variables = array(), $functions = array()) + { + $this->assertSame($expected, $node->evaluate($functions, $variables)); + } + + abstract public function getEvaluateData(); + + /** + * @dataProvider getCompileData + */ + public function testCompile($expected, $node, $functions = array()) + { + $compiler = new Compiler($functions); + $node->compile($compiler); + $this->assertSame($expected, $compiler->getSource()); + } + + abstract public function getCompileData(); +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArgumentsNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArgumentsNodeTest.php new file mode 100644 index 0000000000000..27e72dfc41ceb --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArgumentsNodeTest.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\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ArgumentsNode; + +class ArgumentsNodeTest extends ArrayNodeTest +{ + public function getCompileData() + { + return array( + array('"a", "b"', $this->getArrayNode()), + ); + } + + protected function createArrayNode() + { + return new ArgumentsNode(); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArrayNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArrayNodeTest.php new file mode 100644 index 0000000000000..f2342f2850047 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ArrayNodeTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ArrayNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class ArrayNodeTest extends AbstractNodeTest +{ + public function testSerialization() + { + $node = $this->createArrayNode(); + $node->addElement(new ConstantNode('foo')); + + $serializedNode = serialize($node); + $unserializedNode = unserialize($serializedNode); + + $this->assertEquals($node, $unserializedNode); + $this->assertNotEquals($this->createArrayNode(), $unserializedNode); + } + + public function getEvaluateData() + { + return array( + array(array('b' => 'a', 'b'), $this->getArrayNode()), + ); + } + + public function getCompileData() + { + return array( + array('array("b" => "a", 0 => "b")', $this->getArrayNode()), + ); + } + + protected function getArrayNode() + { + $array = $this->createArrayNode(); + $array->addElement(new ConstantNode('a'), new ConstantNode('b')); + $array->addElement(new ConstantNode('b')); + + return $array; + } + + protected function createArrayNode() + { + return new ArrayNode(); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php new file mode 100644 index 0000000000000..97ac480244916 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/BinaryNodeTest.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\BinaryNode; +use Symfony\Component\ExpressionLanguage\Node\ArrayNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class BinaryNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + $array = new ArrayNode(); + $array->addElement(new ConstantNode('a')); + $array->addElement(new ConstantNode('b')); + + return array( + array(true, new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))), + array(true, new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))), + array(false, new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))), + array(false, new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))), + + array(0, new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))), + array(6, new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))), + array(6, new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))), + + array(true, new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))), + array(true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))), + array(true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))), + + array(false, new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))), + array(false, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))), + array(true, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))), + + array(true, new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))), + array(false, new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))), + + array(false, new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))), + array(true, new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))), + + array(-1, new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))), + array(3, new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))), + array(4, new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))), + array(1, new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))), + array(1, new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))), + array(25, new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))), + array('ab', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))), + + array(true, new BinaryNode('in', new ConstantNode('a'), $array)), + array(false, new BinaryNode('in', new ConstantNode('c'), $array)), + array(true, new BinaryNode('not in', new ConstantNode('c'), $array)), + array(false, new BinaryNode('not in', new ConstantNode('a'), $array)), + + array(array(1, 2, 3), new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))), + + array(1, new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+$/'))), + ); + } + + public function getCompileData() + { + $array = new ArrayNode(); + $array->addElement(new ConstantNode('a')); + $array->addElement(new ConstantNode('b')); + + return array( + array('(true || false)', new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))), + array('(true || false)', new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))), + array('(true && false)', new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))), + array('(true && false)', new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))), + + array('(2 & 4)', new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))), + array('(2 | 4)', new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))), + array('(2 ^ 4)', new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))), + + array('(1 < 2)', new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))), + array('(1 <= 2)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))), + array('(1 <= 1)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))), + + array('(1 > 2)', new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))), + array('(1 >= 2)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))), + array('(1 >= 1)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))), + + array('(true === true)', new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))), + array('(true !== true)', new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))), + + array('(2 == 1)', new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))), + array('(2 != 1)', new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))), + + array('(1 - 2)', new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))), + array('(1 + 2)', new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))), + array('(2 * 2)', new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))), + array('(2 / 2)', new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))), + array('(5 % 2)', new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))), + array('pow(5, 2)', new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))), + array('("a" . "b")', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))), + + array('in_array("a", array(0 => "a", 1 => "b"))', new BinaryNode('in', new ConstantNode('a'), $array)), + array('in_array("c", array(0 => "a", 1 => "b"))', new BinaryNode('in', new ConstantNode('c'), $array)), + array('!in_array("c", array(0 => "a", 1 => "b"))', new BinaryNode('not in', new ConstantNode('c'), $array)), + array('!in_array("a", array(0 => "a", 1 => "b"))', new BinaryNode('not in', new ConstantNode('a'), $array)), + + array('range(1, 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))), + + array('preg_match("/^[a-z]+/i\$/", "abc")', new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+/i$/'))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConditionalNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConditionalNodeTest.php new file mode 100644 index 0000000000000..9b9f7a27243e0 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConditionalNodeTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ConditionalNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class ConditionalNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array(1, new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))), + array(2, new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))), + ); + } + + public function getCompileData() + { + return array( + array('((true) ? (1) : (2))', new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))), + array('((false) ? (1) : (2))', new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConstantNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConstantNodeTest.php new file mode 100644 index 0000000000000..c1a67a8603829 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/ConstantNodeTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class ConstantNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array(false, new ConstantNode(false)), + array(true, new ConstantNode(true)), + array(null, new ConstantNode(null)), + array(3, new ConstantNode(3)), + array(3.3, new ConstantNode(3.3)), + array('foo', new ConstantNode('foo')), + array(array(1, 'b' => 'a'), new ConstantNode(array(1, 'b' => 'a'))), + ); + } + + public function getCompileData() + { + return array( + array('false', new ConstantNode(false)), + array('true', new ConstantNode(true)), + array('null', new ConstantNode(null)), + array('3', new ConstantNode(3)), + array('3.3', new ConstantNode(3.3)), + array('"foo"', new ConstantNode('foo')), + array('array(0 => 1, "b" => "a")', new ConstantNode(array(1, 'b' => 'a'))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/FunctionNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/FunctionNodeTest.php new file mode 100644 index 0000000000000..ecdc3d63717bc --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/FunctionNodeTest.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\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\FunctionNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; +use Symfony\Component\ExpressionLanguage\Node\Node; + +class FunctionNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array('bar', new FunctionNode('foo', new Node(array(new ConstantNode('bar')))), array(), array('foo' => $this->getCallables())), + ); + } + + public function getCompileData() + { + return array( + array('foo("bar")', new FunctionNode('foo', new Node(array(new ConstantNode('bar')))), array('foo' => $this->getCallables())), + ); + } + + protected function getCallables() + { + return array( + 'compiler' => function ($arg) { + return sprintf('foo(%s)', $arg); + }, + 'evaluator' => function ($variables, $arg) { + return $arg; + }, + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/GetAttrNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/GetAttrNodeTest.php new file mode 100644 index 0000000000000..57bd165075326 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/GetAttrNodeTest.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\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\ArrayNode; +use Symfony\Component\ExpressionLanguage\Node\NameNode; +use Symfony\Component\ExpressionLanguage\Node\GetAttrNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class GetAttrNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array('b', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), array('foo' => array('b' => 'a', 'b'))), + array('a', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), array('foo' => array('b' => 'a', 'b'))), + + array('bar', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), array('foo' => new Obj())), + + array('baz', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), array('foo' => new Obj())), + array('a', new GetAttrNode(new NameNode('foo'), new NameNode('index'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), array('foo' => array('b' => 'a', 'b'), 'index' => 'b')), + ); + } + + public function getCompileData() + { + return array( + array('$foo[0]', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)), + array('$foo["b"]', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)), + + array('$foo->foo', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), array('foo' => new Obj())), + + array('$foo->foo(array("b" => "a", 0 => "b"))', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), array('foo' => new Obj())), + array('$foo[$index]', new GetAttrNode(new NameNode('foo'), new NameNode('index'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)), + ); + } + + protected function getArrayNode() + { + $array = new ArrayNode(); + $array->addElement(new ConstantNode('a'), new ConstantNode('b')); + $array->addElement(new ConstantNode('b')); + + return $array; + } +} + +class Obj +{ + public $foo = 'bar'; + + public function foo() + { + return 'baz'; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/NameNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NameNodeTest.php new file mode 100644 index 0000000000000..b645a6bfffd42 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NameNodeTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\NameNode; + +class NameNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array('bar', new NameNode('foo'), array('foo' => 'bar')), + ); + } + + public function getCompileData() + { + return array( + array('$foo', new NameNode('foo')), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/NodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NodeTest.php new file mode 100644 index 0000000000000..6063c27ba4809 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/NodeTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\Node; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class NodeTest extends \PHPUnit_Framework_TestCase +{ + public function testToString() + { + $node = new Node(array(new ConstantNode('foo'))); + + $this->assertEquals(<< 'bar'), array('bar' => 'foo')); + + $serializedNode = serialize($node); + $unserializedNode = unserialize($serializedNode); + + $this->assertEquals($node, $unserializedNode); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/Node/UnaryNodeTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/Node/UnaryNodeTest.php new file mode 100644 index 0000000000000..6e6f117fda04c --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/Node/UnaryNodeTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests\Node; + +use Symfony\Component\ExpressionLanguage\Node\UnaryNode; +use Symfony\Component\ExpressionLanguage\Node\ConstantNode; + +class UnaryNodeTest extends AbstractNodeTest +{ + public function getEvaluateData() + { + return array( + array(-1, new UnaryNode('-', new ConstantNode(1))), + array(3, new UnaryNode('+', new ConstantNode(3))), + array(false, new UnaryNode('!', new ConstantNode(true))), + array(false, new UnaryNode('not', new ConstantNode(true))), + ); + } + + public function getCompileData() + { + return array( + array('(-1)', new UnaryNode('-', new ConstantNode(1))), + array('(+3)', new UnaryNode('+', new ConstantNode(3))), + array('(!true)', new UnaryNode('!', new ConstantNode(true))), + array('(!true)', new UnaryNode('not', new ConstantNode(true))), + ); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ParsedExpressionTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ParsedExpressionTest.php new file mode 100644 index 0000000000000..c91b9ef666f8b --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ParsedExpressionTest.php @@ -0,0 +1,19 @@ +assertEquals($expression, $unserializedExpression); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php new file mode 100644 index 0000000000000..3be677ca76a7f --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ParserTest.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests; + +use Symfony\Component\ExpressionLanguage\Parser; +use Symfony\Component\ExpressionLanguage\Lexer; +use Symfony\Component\ExpressionLanguage\Node; + +class ParserTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError + * @expectedExceptionMessage Variable "foo" is not valid around position 1. + */ + public function testParseWithInvalidName() + { + $lexer = new Lexer(); + $parser = new Parser(array()); + $parser->parse($lexer->tokenize('foo')); + } + + /** + * @dataProvider getParseData + */ + public function testParse($node, $expression, $names = array()) + { + $lexer = new Lexer(); + $parser = new Parser(array()); + $this->assertEquals($node, $parser->parse($lexer->tokenize($expression), $names)); + } + + public function getParseData() + { + $arguments = new Node\ArgumentsNode(); + $arguments->addElement(new Node\ConstantNode('arg1')); + $arguments->addElement(new Node\ConstantNode(2)); + $arguments->addElement(new Node\ConstantNode(true)); + + return array( + array( + new Node\NameNode('a'), + 'a', + array('a'), + ), + array( + new Node\ConstantNode('a'), + '"a"', + ), + array( + new Node\ConstantNode(3), + '3', + ), + array( + new Node\ConstantNode(false), + 'false', + ), + array( + new Node\ConstantNode(true), + 'true', + ), + array( + new Node\ConstantNode(null), + 'null', + ), + array( + new Node\UnaryNode('-', new Node\ConstantNode(3)), + '-3', + ), + array( + new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)), + '3 - 3', + ), + array( + new Node\BinaryNode('*', + new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)), + new Node\ConstantNode(2) + ), + '(3 - 3) * 2', + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar'), new Node\ArgumentsNode(), Node\GetAttrNode::PROPERTY_CALL), + 'foo.bar', + array('foo'), + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar'), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL), + 'foo.bar()', + array('foo'), + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('not'), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL), + 'foo.not()', + array('foo'), + ), + array( + new Node\GetAttrNode( + new Node\NameNode('foo'), + new Node\ConstantNode('bar'), + $arguments, + Node\GetAttrNode::METHOD_CALL + ), + 'foo.bar("arg1", 2, true)', + array('foo'), + ), + array( + new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode(3), new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL), + 'foo[3]', + array('foo'), + ), + array( + new Node\ConditionalNode(new Node\ConstantNode(true), new Node\ConstantNode(true), new Node\ConstantNode(false)), + 'true ? true : false', + ), + array( + new Node\BinaryNode('matches', new Node\ConstantNode('foo'), new Node\ConstantNode('/foo/')), + '"foo" matches "/foo/"', + ), + + // chained calls + array( + $this->createGetAttrNode( + $this->createGetAttrNode( + $this->createGetAttrNode( + $this->createGetAttrNode(new Node\NameNode('foo'), 'bar', Node\GetAttrNode::METHOD_CALL), + 'foo', Node\GetAttrNode::METHOD_CALL), + 'baz', Node\GetAttrNode::PROPERTY_CALL), + '3', Node\GetAttrNode::ARRAY_CALL), + 'foo.bar().foo().baz[3]', + array('foo'), + ), + ); + } + + private function createGetAttrNode($node, $item, $type) + { + return new Node\GetAttrNode($node, new Node\ConstantNode($item), new Node\ArgumentsNode(), $type); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/Token.php b/src/Symfony/Component/ExpressionLanguage/Token.php new file mode 100644 index 0000000000000..4b9e184551507 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Token.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Represents a Token. + * + * @author Fabien Potencier + */ +class Token +{ + public $value; + public $type; + public $cursor; + + const EOF_TYPE = 'end of expression'; + const NAME_TYPE = 'name'; + const NUMBER_TYPE = 'number'; + const STRING_TYPE = 'string'; + const OPERATOR_TYPE = 'operator'; + const PUNCTUATION_TYPE = 'punctuation'; + + /** + * Constructor. + * + * @param integer $type The type of the token + * @param string $value The token value + * @param integer $cursor The cursor position in the source + */ + public function __construct($type, $value, $cursor) + { + $this->type = $type; + $this->value = $value; + $this->cursor = $cursor; + } + + /** + * Returns a string representation of the token. + * + * @return string A string representation of the token + */ + public function __toString() + { + return sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value); + } + + /** + * Tests the current token for a type and/or a value. + * + * @param array|integer $type The type to test + * @param string|null $value The token value + * + * @return Boolean + */ + public function test($type, $value = null) + { + return $this->type === $type && (null === $value || $this->value == $value); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/TokenStream.php b/src/Symfony/Component/ExpressionLanguage/TokenStream.php new file mode 100644 index 0000000000000..8516273a661d1 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/TokenStream.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage; + +/** + * Represents a token stream. + * + * @author Fabien Potencier + */ +class TokenStream +{ + public $current; + + private $tokens; + private $position = 0; + + /** + * Constructor. + * + * @param array $tokens An array of tokens + */ + public function __construct(array $tokens) + { + $this->tokens = $tokens; + $this->current = $tokens[0]; + } + + /** + * Returns a string representation of the token stream. + * + * @return string + */ + public function __toString() + { + return implode("\n", $this->tokens); + } + + /** + * Sets the pointer to the next token and returns the old one. + */ + public function next() + { + if (!isset($this->tokens[$this->position])) { + throw new SyntaxError('Unexpected end of expression', $this->current->cursor); + } + + ++$this->position; + + $this->current = $this->tokens[$this->position]; + } + + /** + * Tests a token. + */ + public function expect($type, $value = null, $message = null) + { + $token = $this->current; + if (!$token->test($type, $value)) { + throw new SyntaxError(sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s)', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? sprintf(' with value "%s"', $value) : ''), $token->cursor); + } + $this->next(); + } + + /** + * Checks if end of stream was reached + * + * @return bool + */ + public function isEOF() + { + return $this->current->type === Token::EOF_TYPE; + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/composer.json b/src/Symfony/Component/ExpressionLanguage/composer.json new file mode 100644 index 0000000000000..3828f0435aa76 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/composer.json @@ -0,0 +1,31 @@ +{ + "name": "symfony/expression-language", + "type": "library", + "description": "Symfony ExpressionLanguage 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.3" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\ExpressionLanguage\\": "" } + }, + "target-dir": "Symfony/Component/ExpressionLanguage", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/phpunit.xml.dist b/src/Symfony/Component/ExpressionLanguage/phpunit.xml.dist new file mode 100644 index 0000000000000..41d9128824d98 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php b/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php index bc9748d752e71..c212e664d487a 100644 --- a/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php +++ b/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php @@ -20,5 +20,4 @@ */ interface ExceptionInterface { - } diff --git a/src/Symfony/Component/Filesystem/Exception/FileNotFoundException.php b/src/Symfony/Component/Filesystem/Exception/FileNotFoundException.php new file mode 100644 index 0000000000000..15533db408966 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Exception/FileNotFoundException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a file couldn't be found + * + * @author Fabien Potencier + * @author Christian Gärtner + */ +class FileNotFoundException extends IOException +{ + public function __construct($message = null, $code = 0, \Exception $previous = null, $path = null) + { + if (null === $message) { + if (null === $path) { + $message = 'File could not be found.'; + } else { + $message = sprintf('File "%s" could not be found.', $path); + } + } + + parent::__construct($message, $code, $previous, $path); + } +} diff --git a/src/Symfony/Component/Filesystem/Exception/IOException.php b/src/Symfony/Component/Filesystem/Exception/IOException.php index 5b27e661eee38..4b551af71b76d 100644 --- a/src/Symfony/Component/Filesystem/Exception/IOException.php +++ b/src/Symfony/Component/Filesystem/Exception/IOException.php @@ -15,10 +15,27 @@ * Exception class thrown when a filesystem operation failure happens * * @author Romain Neutron + * @author Christian Gärtner + * @author Fabien Potencier * * @api */ -class IOException extends \RuntimeException implements ExceptionInterface +class IOException extends \RuntimeException implements IOExceptionInterface { + private $path; + public function __construct($message, $code = 0, \Exception $previous = null, $path = null) + { + $this->path = $path; + + parent::__construct($message, $code, $previous); + } + + /** + * {@inheritdoc} + */ + public function getPath() + { + return $this->path; + } } diff --git a/src/Symfony/Component/Filesystem/Exception/IOExceptionInterface.php b/src/Symfony/Component/Filesystem/Exception/IOExceptionInterface.php new file mode 100644 index 0000000000000..c88c763173c4a --- /dev/null +++ b/src/Symfony/Component/Filesystem/Exception/IOExceptionInterface.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\Filesystem\Exception; + +/** + * IOException interface for file and input/output stream related exceptions thrown by the component. + * + * @author Christian Gärtner + */ +interface IOExceptionInterface extends ExceptionInterface +{ + /** + * Returns the associated path for the exception + * + * @return string The path. + */ + public function getPath(); +} diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 1dc6ac8177359..7870eaa8b65b9 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Filesystem; use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Exception\FileNotFoundException; /** * Provides basic utility to manipulate the file system. @@ -31,12 +32,13 @@ class Filesystem * @param string $targetFile The target filename * @param boolean $override Whether to override an existing file or not * - * @throws IOException When copy fails + * @throws FileNotFoundException When originFile doesn't exist + * @throws IOException When copy fails */ public function copy($originFile, $targetFile, $override = false) { if (stream_is_local($originFile) && !is_file($originFile)) { - throw new IOException(sprintf('Failed to copy %s because file not exists', $originFile)); + throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); } $this->mkdir(dirname($targetFile)); @@ -57,7 +59,7 @@ public function copy($originFile, $targetFile, $override = false) unset($source, $target); if (!is_file($targetFile)) { - throw new IOException(sprintf('Failed to copy %s to %s', $originFile, $targetFile)); + throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); } } } @@ -78,7 +80,7 @@ public function mkdir($dirs, $mode = 0777) } if (true !== @mkdir($dir, $mode, true)) { - throw new IOException(sprintf('Failed to create %s', $dir)); + throw new IOException(sprintf('Failed to create "%s".', $dir), 0, null, $dir); } } } @@ -115,7 +117,7 @@ public function touch($files, $time = null, $atime = null) foreach ($this->toIterator($files) as $file) { $touch = $time ? @touch($file, $time, $atime) : @touch($file); if (true !== $touch) { - throw new IOException(sprintf('Failed to touch %s', $file)); + throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file); } } } @@ -140,17 +142,17 @@ public function remove($files) $this->remove(new \FilesystemIterator($file)); if (true !== @rmdir($file)) { - throw new IOException(sprintf('Failed to remove directory %s', $file)); + throw new IOException(sprintf('Failed to remove directory "%s".', $file), 0, null, $file); } } else { // https://bugs.php.net/bug.php?id=52176 if (defined('PHP_WINDOWS_VERSION_MAJOR') && is_dir($file)) { if (true !== @rmdir($file)) { - throw new IOException(sprintf('Failed to remove file %s', $file)); + throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file); } } else { if (true !== @unlink($file)) { - throw new IOException(sprintf('Failed to remove file %s', $file)); + throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file); } } } @@ -174,7 +176,7 @@ public function chmod($files, $mode, $umask = 0000, $recursive = false) $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); } if (true !== @chmod($file, $mode & ~$umask)) { - throw new IOException(sprintf('Failed to chmod file %s', $file)); + throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file); } } } @@ -196,11 +198,11 @@ public function chown($files, $user, $recursive = false) } if (is_link($file) && function_exists('lchown')) { if (true !== @lchown($file, $user)) { - throw new IOException(sprintf('Failed to chown file %s', $file)); + throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); } } else { if (true !== @chown($file, $user)) { - throw new IOException(sprintf('Failed to chown file %s', $file)); + throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); } } } @@ -223,11 +225,11 @@ public function chgrp($files, $group, $recursive = false) } if (is_link($file) && function_exists('lchgrp')) { if (true !== @lchgrp($file, $group)) { - throw new IOException(sprintf('Failed to chgrp file %s', $file)); + throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); } } else { if (true !== @chgrp($file, $group)) { - throw new IOException(sprintf('Failed to chgrp file %s', $file)); + throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); } } } @@ -247,11 +249,11 @@ public function rename($origin, $target, $overwrite = false) { // we check that target does not exist if (!$overwrite && is_readable($target)) { - throw new IOException(sprintf('Cannot rename because the target "%s" already exist.', $target)); + throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); } if (true !== @rename($origin, $target)) { - throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target)); + throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target); } } @@ -291,7 +293,8 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false) throw new IOException('Unable to create symlink due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?'); } } - throw new IOException(sprintf('Failed to create symbolic link from %s to %s', $originDir, $targetDir)); + + throw new IOException(sprintf('Failed to create symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir); } } } @@ -389,7 +392,7 @@ public function mirror($originDir, $targetDir, \Traversable $iterator = null, $o } elseif (is_dir($file)) { $this->mkdir($target); } else { - throw new IOException(sprintf('Unable to guess "%s" file type.', $file)); + throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); } } else { if (is_link($file)) { @@ -399,7 +402,7 @@ public function mirror($originDir, $targetDir, \Traversable $iterator = null, $o } elseif (is_file($file)) { $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); } else { - throw new IOException(sprintf('Unable to guess "%s" file type.', $file)); + throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); } } } @@ -427,20 +430,6 @@ public function isAbsolutePath($file) return false; } - /** - * @param mixed $files - * - * @return \Traversable - */ - private function toIterator($files) - { - if (!$files instanceof \Traversable) { - $files = new \ArrayObject(is_array($files) ? $files : array($files)); - } - - return $files; - } - /** * Atomically dumps content into a file. * @@ -456,16 +445,30 @@ public function dumpFile($filename, $content, $mode = 0666) if (!is_dir($dir)) { $this->mkdir($dir); } elseif (!is_writable($dir)) { - throw new IOException(sprintf('Unable to write in the %s directory\n', $dir)); + throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); } $tmpFile = tempnam($dir, basename($filename)); if (false === @file_put_contents($tmpFile, $content)) { - throw new IOException(sprintf('Failed to write file "%s".', $filename)); + throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); } $this->rename($tmpFile, $filename, true); $this->chmod($filename, $mode); } + + /** + * @param mixed $files + * + * @return \Traversable + */ + private function toIterator($files) + { + if (!$files instanceof \Traversable) { + $files = new \ArrayObject(is_array($files) ? $files : array($files)); + } + + return $files; + } } diff --git a/src/Symfony/Component/Filesystem/Tests/ExceptionTest.php b/src/Symfony/Component/Filesystem/Tests/ExceptionTest.php new file mode 100644 index 0000000000000..53bd8db76a00e --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/ExceptionTest.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\Filesystem\Tests; + +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Exception\FileNotFoundException; + +/** + * Test class for Filesystem. + */ +class ExceptionTest extends \PHPUnit_Framework_TestCase +{ + public function testGetPath() + { + $e = new IOException('', 0, null, '/foo'); + $this->assertEquals('/foo', $e->getPath(), 'The pass should be returned.'); + } + + public function testGeneratedMessage() + { + $e = new FileNotFoundException(null, 0, null, '/foo'); + $this->assertEquals('/foo', $e->getPath()); + $this->assertEquals('File "/foo" could not be found.', $e->getMessage(), 'A message should be generated.'); + } + + public function testGeneratedMessageWithoutPath() + { + $e = new FileNotFoundException(); + $this->assertEquals('File could not be found.', $e->getMessage(), 'A message should be generated.'); + } + + public function testCustomMessage() + { + $e = new FileNotFoundException('bar', 0, null, '/foo'); + $this->assertEquals('bar', $e->getMessage(), 'A custom message should be possible still.'); + } +} diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 5877e484c6350..44118fcdefe2d 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -16,63 +16,17 @@ /** * Test class for Filesystem. */ -class FilesystemTest extends \PHPUnit_Framework_TestCase +class FilesystemTest extends FilesystemTestCase { - /** - * @var string $workspace - */ - private $workspace = null; - /** * @var \Symfony\Component\Filesystem\Filesystem $filesystem */ private $filesystem = null; - private static $symlinkOnWindows = null; - - public static function setUpBeforeClass() - { - if (defined('PHP_WINDOWS_VERSION_MAJOR')) { - self::$symlinkOnWindows = true; - $originDir = tempnam(sys_get_temp_dir(), 'sl'); - $targetDir = tempnam(sys_get_temp_dir(), 'sl'); - if (true !== @symlink($originDir, $targetDir)) { - $report = error_get_last(); - if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) { - self::$symlinkOnWindows = false; - } - } - } - } - public function setUp() { + parent::setUp(); $this->filesystem = new Filesystem(); - $this->workspace = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.time().rand(0, 1000); - mkdir($this->workspace, 0777, true); - $this->workspace = realpath($this->workspace); - } - - public function tearDown() - { - $this->clean($this->workspace); - } - - /** - * @param string $file - */ - private function clean($file) - { - if (is_dir($file) && !is_link($file)) { - $dir = new \FilesystemIterator($file); - foreach ($dir as $childFile) { - $this->clean($childFile); - } - - rmdir($file); - } else { - unlink($file); - } } public function testCopyCreatesNewFile() @@ -419,8 +373,8 @@ public function testChmodChangesFileMode() $this->filesystem->chmod($file, 0400); $this->filesystem->chmod($dir, 0753); - $this->assertEquals(753, $this->getFilePermissions($dir)); - $this->assertEquals(400, $this->getFilePermissions($file)); + $this->assertFilePermissions(753, $dir); + $this->assertFilePermissions(400, $file); } public function testChmodWrongMod() @@ -445,8 +399,8 @@ public function testChmodRecursive() $this->filesystem->chmod($file, 0400, 0000, true); $this->filesystem->chmod($dir, 0753, 0000, true); - $this->assertEquals(753, $this->getFilePermissions($dir)); - $this->assertEquals(753, $this->getFilePermissions($file)); + $this->assertFilePermissions(753, $dir); + $this->assertFilePermissions(753, $file); } public function testChmodAppliesUmask() @@ -457,7 +411,7 @@ public function testChmodAppliesUmask() touch($file); $this->filesystem->chmod($file, 0770, 0022); - $this->assertEquals(750, $this->getFilePermissions($file)); + $this->assertFilePermissions(750, $file); } public function testChmodChangesModeOfArrayOfFiles() @@ -473,8 +427,8 @@ public function testChmodChangesModeOfArrayOfFiles() $this->filesystem->chmod($files, 0753); - $this->assertEquals(753, $this->getFilePermissions($file)); - $this->assertEquals(753, $this->getFilePermissions($directory)); + $this->assertFilePermissions(753, $file); + $this->assertFilePermissions(753, $directory); } public function testChmodChangesModeOfTraversableFileObject() @@ -490,8 +444,8 @@ public function testChmodChangesModeOfTraversableFileObject() $this->filesystem->chmod($files, 0753); - $this->assertEquals(753, $this->getFilePermissions($file)); - $this->assertEquals(753, $this->getFilePermissions($directory)); + $this->assertFilePermissions(753, $file); + $this->assertFilePermissions(753, $directory); } public function testChown() @@ -921,7 +875,7 @@ public function testDumpFile() // skip mode check on Windows if (!defined('PHP_WINDOWS_VERSION_MAJOR')) { - $this->assertEquals(753, $this->getFilePermissions($filename)); + $this->assertFilePermissions(753, $filename); } } @@ -935,61 +889,4 @@ public function testDumpFileOverwritesAnExistingFile() $this->assertFileExists($filename); $this->assertSame('bar', file_get_contents($filename)); } - - /** - * Returns file permissions as three digits (i.e. 755) - * - * @param string $filePath - * - * @return integer - */ - private function getFilePermissions($filePath) - { - return (int) substr(sprintf('%o', fileperms($filePath)), -3); - } - - private function getFileOwner($filepath) - { - $this->markAsSkippedIfPosixIsMissing(); - - $infos = stat($filepath); - if ($datas = posix_getpwuid($infos['uid'])) { - return $datas['name']; - } - } - - private function getFileGroup($filepath) - { - $this->markAsSkippedIfPosixIsMissing(); - - $infos = stat($filepath); - if ($datas = posix_getgrgid($infos['gid'])) { - return $datas['name']; - } - } - - private function markAsSkippedIfSymlinkIsMissing() - { - if (!function_exists('symlink')) { - $this->markTestSkipped('symlink is not supported'); - } - - if (defined('PHP_WINDOWS_VERSION_MAJOR') && false === self::$symlinkOnWindows) { - $this->markTestSkipped('symlink requires "Create symbolic links" privilege on Windows'); - } - } - - private function markAsSkippedIfChmodIsMissing() - { - if (defined('PHP_WINDOWS_VERSION_MAJOR')) { - $this->markTestSkipped('chmod is not supported on Windows'); - } - } - - private function markAsSkippedIfPosixIsMissing() - { - if (defined('PHP_WINDOWS_VERSION_MAJOR') || !function_exists('posix_isatty')) { - $this->markTestSkipped('POSIX is not supported'); - } - } } diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php new file mode 100644 index 0000000000000..b4b2230189a2d --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Tests; + +class FilesystemTestCase extends \PHPUnit_Framework_TestCase +{ + /** + * @var string $workspace + */ + protected $workspace = null; + + protected static $symlinkOnWindows = null; + + public static function setUpBeforeClass() + { + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + static::$symlinkOnWindows = true; + $originDir = tempnam(sys_get_temp_dir(), 'sl'); + $targetDir = tempnam(sys_get_temp_dir(), 'sl'); + if (true !== @symlink($originDir, $targetDir)) { + $report = error_get_last(); + if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) { + static::$symlinkOnWindows = false; + } + } + } + } + + public function setUp() + { + $this->workspace = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.time().rand(0, 1000); + mkdir($this->workspace, 0777, true); + $this->workspace = realpath($this->workspace); + } + + public function tearDown() + { + $this->clean($this->workspace); + } + + /** + * @param string $file + */ + protected function clean($file) + { + if (is_dir($file) && !is_link($file)) { + $dir = new \FilesystemIterator($file); + foreach ($dir as $childFile) { + $this->clean($childFile); + } + + rmdir($file); + } else { + unlink($file); + } + } + + /** + * @param int $expectedFilePerms expected file permissions as three digits (i.e. 755) + * @param string $filePath + */ + protected function assertFilePermissions($expectedFilePerms, $filePath) + { + $actualFilePerms = (int) substr(sprintf('%o', fileperms($filePath)), -3); + $this->assertEquals( + $expectedFilePerms, + $actualFilePerms, + sprintf('File permissions for %s must be %s. Actual %s', $filePath, $expectedFilePerms, $actualFilePerms) + ); + } + + protected function getFileOwner($filepath) + { + $this->markAsSkippedIfPosixIsMissing(); + + $infos = stat($filepath); + if ($datas = posix_getpwuid($infos['uid'])) { + return $datas['name']; + } + } + + protected function getFileGroup($filepath) + { + $this->markAsSkippedIfPosixIsMissing(); + + $infos = stat($filepath); + if ($datas = posix_getgrgid($infos['gid'])) { + return $datas['name']; + } + } + + protected function markAsSkippedIfSymlinkIsMissing() + { + if (!function_exists('symlink')) { + $this->markTestSkipped('symlink is not supported'); + } + + if (defined('PHP_WINDOWS_VERSION_MAJOR') && false === static::$symlinkOnWindows) { + $this->markTestSkipped('symlink requires "Create symbolic links" privilege on windows'); + } + } + + protected function markAsSkippedIfChmodIsMissing() + { + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + $this->markTestSkipped('chmod is not supported on windows'); + } + } + + protected function markAsSkippedIfPosixIsMissing() + { + if (defined('PHP_WINDOWS_VERSION_MAJOR') || !function_exists('posix_isatty')) { + $this->markTestSkipped('Posix is not supported'); + } + } +} diff --git a/src/Symfony/Component/Filesystem/composer.json b/src/Symfony/Component/Filesystem/composer.json index 167dd506a9958..dfa633c1d06ce 100644 --- a/src/Symfony/Component/Filesystem/composer.json +++ b/src/Symfony/Component/Filesystem/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Finder/CHANGELOG.md b/src/Symfony/Component/Finder/CHANGELOG.md index 7ad230813241b..f1dd7d526b288 100644 --- a/src/Symfony/Component/Finder/CHANGELOG.md +++ b/src/Symfony/Component/Finder/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +2.5.0 +----- + * added support for GLOB_BRACE in the paths passed to Finder::in() + 2.3.0 ----- diff --git a/src/Symfony/Component/Finder/Finder.php b/src/Symfony/Component/Finder/Finder.php index c075e0ff851b8..b02a71124ebbc 100644 --- a/src/Symfony/Component/Finder/Finder.php +++ b/src/Symfony/Component/Finder/Finder.php @@ -661,7 +661,7 @@ public function in($dirs) foreach ((array) $dirs as $dir) { if (is_dir($dir)) { $resolvedDirs[] = $dir; - } elseif ($glob = glob($dir, GLOB_ONLYDIR)) { + } elseif ($glob = glob($dir, GLOB_BRACE | GLOB_ONLYDIR)) { $resolvedDirs = array_merge($resolvedDirs, $glob); } else { throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.', $dir)); diff --git a/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php b/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php index 3e5713b8e6968..fbdff76ef40b3 100644 --- a/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php @@ -44,10 +44,6 @@ public function accept() { $fileinfo = $this->current(); - if (!$fileinfo->isFile()) { - return true; - } - $filedate = $fileinfo->getMTime(); foreach ($this->comparators as $compare) { if (!$compare->test($filedate)) { diff --git a/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php b/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php index 42c8ce760d26f..e4eabbf804bbe 100644 --- a/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php @@ -18,7 +18,7 @@ */ class ExcludeDirectoryFilterIterator extends FilterIterator { - private $patterns; + private $patterns = array(); /** * Constructor. @@ -28,7 +28,6 @@ class ExcludeDirectoryFilterIterator extends FilterIterator */ public function __construct(\Iterator $iterator, array $directories) { - $this->patterns = array(); foreach ($directories as $directory) { $this->patterns[] = '#(^|/)'.preg_quote($directory, '#').'(/|$)#'; } diff --git a/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php b/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php index 3a9dd55582859..df6d6dc6a7bd9 100644 --- a/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php @@ -20,8 +20,8 @@ */ abstract class MultiplePcreFilterIterator extends FilterIterator { - protected $matchRegexps; - protected $noMatchRegexps; + protected $matchRegexps = array(); + protected $noMatchRegexps = array(); /** * Constructor. @@ -32,12 +32,10 @@ abstract class MultiplePcreFilterIterator extends FilterIterator */ public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns) { - $this->matchRegexps = array(); foreach ($matchPatterns as $pattern) { $this->matchRegexps[] = $this->toRegex($pattern); } - $this->noMatchRegexps = array(); foreach ($noMatchPatterns as $pattern) { $this->noMatchRegexps[] = $this->toRegex($pattern); } diff --git a/src/Symfony/Component/Finder/Shell/Command.php b/src/Symfony/Component/Finder/Shell/Command.php index 54f4f176fb8d6..a891016b815c4 100644 --- a/src/Symfony/Component/Finder/Shell/Command.php +++ b/src/Symfony/Component/Finder/Shell/Command.php @@ -24,12 +24,12 @@ class Command /** * @var array */ - private $bits; + private $bits = array(); /** * @var array */ - private $labels; + private $labels = array(); /** * @var \Closure|null @@ -39,13 +39,11 @@ class Command /** * Constructor. * - * @param Command $parent Parent command + * @param Command|null $parent Parent command */ public function __construct(Command $parent = null) { $this->parent = $parent; - $this->bits = array(); - $this->labels = array(); } /** @@ -61,7 +59,7 @@ public function __toString() /** * Creates a new Command instance. * - * @param Command $parent Parent command + * @param Command|null $parent Parent command * * @return Command New Command instance */ @@ -232,7 +230,7 @@ public function setErrorHandler(\Closure $errorHandler) } /** - * @return callable|null + * @return \Closure|null */ public function getErrorHandler() { diff --git a/src/Symfony/Component/Finder/Tests/FinderTest.php b/src/Symfony/Component/Finder/Tests/FinderTest.php index 86cb1d3cf992d..92eea024a5052 100644 --- a/src/Symfony/Component/Finder/Tests/FinderTest.php +++ b/src/Symfony/Component/Finder/Tests/FinderTest.php @@ -333,6 +333,17 @@ public function testInWithNonDirectoryGlob($adapter) $finder->in(__DIR__.'/Fixtures/A/a*'); } + /** + * @dataProvider getAdaptersTestData + */ + public function testInWithGlobBrace($adapter) + { + $finder = $this->buildFinder($adapter); + $finder->in(array(__DIR__.'/Fixtures/{A,copy/A}/B/C'))->getIterator(); + + $this->assertIterator($this->toAbsoluteFixtures(array('A/B/C/abc.dat', 'copy/A/B/C/abc.dat.copy')), $finder); + } + /** * @dataProvider getAdaptersTestData */ diff --git a/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php index 18896d552891d..008f966b90dd5 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php @@ -57,12 +57,8 @@ public function getAcceptData() ); $untilLastMonth = array( - '.git', - 'foo', 'foo/bar.tmp', 'test.php', - 'toto', - '.foo', ); return array( diff --git a/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php b/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php index 7ffd38267b99f..849bf081e2cc9 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php @@ -13,11 +13,10 @@ class Iterator implements \Iterator { - protected $values; + protected $values = array(); public function __construct(array $values = array()) { - $this->values = array(); foreach ($values as $value) { $this->attach(new \SplFileInfo($value)); } diff --git a/src/Symfony/Component/Finder/Tests/Iterator/RecursiveDirectoryIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/RecursiveDirectoryIteratorTest.php index f762514346474..4a54d1d33f8c3 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/RecursiveDirectoryIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/RecursiveDirectoryIteratorTest.php @@ -20,7 +20,7 @@ class RecursiveDirectoryIteratorTest extends IteratorTestCase * * @param string $path * @param Boolean $seekable - * @param Boolean $supports + * @param array $contains * @param string $message */ public function testRewind($path, $seekable, $contains, $message = null) @@ -41,7 +41,7 @@ public function testRewind($path, $seekable, $contains, $message = null) * * @param string $path * @param Boolean $seekable - * @param Boolean $supports + * @param array $contains * @param string $message */ public function testSeek($path, $seekable, $contains, $message = null) diff --git a/src/Symfony/Component/Finder/composer.json b/src/Symfony/Component/Finder/composer.json index 7480b3edb1ab1..b6e6997884e0f 100644 --- a/src/Symfony/Component/Finder/composer.json +++ b/src/Symfony/Component/Finder/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Form/Button.php b/src/Symfony/Component/Form/Button.php index f12d11afe417f..25ec861735cda 100644 --- a/src/Symfony/Component/Form/Button.php +++ b/src/Symfony/Component/Form/Button.php @@ -412,7 +412,15 @@ public function createView(FormView $parent = null) $parent = $this->parent->createView(); } - return $this->config->getType()->createView($this, $parent); + $type = $this->config->getType(); + $options = $this->config->getOptions(); + + $view = $type->createView($this, $parent); + + $type->buildView($view, $this, $options); + $type->finishView($view, $this, $options); + + return $view; } /** diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index a696c7be9a9c8..ff4e46284f547 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -1,9 +1,22 @@ CHANGELOG ========= +2.5.0 +------ + + * added an option for multiple files upload + * form errors now reference their cause (constraint violation, exception, ...) + * form errors now remember which form they were originally added to + +2.4.0 +----- + + * moved CSRF implementation to the new Security CSRF sub-component + * deprecated CsrfProviderInterface and its implementations + * deprecated options "csrf_provider" and "intention" in favor of the new options "csrf_token_generator" and "csrf_token_id" 2.3.0 ------- +----- * deprecated FormPerformanceTestCase and FormIntegrationTestCase in the Symfony\Component\Form\Tests namespace and moved them to the Symfony\Component\Form\Test namespace * deprecated TypeTestCase in the Symfony\Component\Form\Tests\Extension\Core\Type namespace and moved it to the Symfony\Component\Form\Test namespace diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php index 17ae0a3d9eb04..413f7c4f741a6 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceList.php @@ -198,6 +198,8 @@ public function getValuesForChoices(array $choices) /** * {@inheritdoc} + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForChoices(array $choices) { @@ -222,6 +224,8 @@ public function getIndicesForChoices(array $choices) /** * {@inheritdoc} + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForValues(array $values) { @@ -485,9 +489,9 @@ protected function fixIndices(array $indices) * Extension point. In this implementation, choices are guaranteed to * always maintain their type and thus can be typesafely compared. * - * @param mixed $choice The choice. + * @param mixed $choice The choice * - * @return mixed The fixed choice. + * @return mixed The fixed choice */ protected function fixChoice($choice) { @@ -495,14 +499,14 @@ protected function fixChoice($choice) } /** - * Fixes the data type of the given choices to avoid comparison problems. - * - * @param array $choices The choices. - * - * @return array The fixed choices. - * - * @see fixChoice - */ + * Fixes the data type of the given choices to avoid comparison problems. + * + * @param array $choices The choices. + * + * @return array The fixed choices. + * + * @see fixChoice + */ protected function fixChoices(array $choices) { return $choices; diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php index febf80ef059e6..0e5f9459b9ede 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ChoiceListInterface.php @@ -138,6 +138,8 @@ public function getValuesForChoices(array $choices); * array are ignored * * @return array An array of indices with ascending, 0-based numeric keys + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForChoices(array $choices); @@ -156,6 +158,8 @@ public function getIndicesForChoices(array $choices); * this array are ignored * * @return array An array of indices with ascending, 0-based numeric keys + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForValues(array $values); } diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php index 996f900cf3850..23f3cccbe2fa2 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/LazyChoiceList.php @@ -105,6 +105,8 @@ public function getValuesForChoices(array $choices) /** * {@inheritdoc} + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForChoices(array $choices) { @@ -117,6 +119,8 @@ public function getIndicesForChoices(array $choices) /** * {@inheritdoc} + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForValues(array $values) { diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php index 914dbe5fdb174..e173e742c7349 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/SimpleChoiceList.php @@ -135,9 +135,9 @@ protected function isPreferred($choice, array $preferredChoices) /** * Converts the choice to a valid PHP array key. * - * @param mixed $choice The choice. + * @param mixed $choice The choice * - * @return string|integer A valid PHP array key. + * @return string|integer A valid PHP array key */ protected function fixChoice($choice) { diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DataTransformerChain.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DataTransformerChain.php index 9cc185e1abde0..800fc7a98b818 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DataTransformerChain.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DataTransformerChain.php @@ -83,4 +83,12 @@ public function reverseTransform($value) return $value; } + + /** + * @return DataTransformerInterface[] + */ + public function getTransformers() + { + return $this->transformers; + } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php index 6bb48a9a03b4f..3f6463eb5ac47 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Form\Extension\Core\DataTransformer; -use Symfony\Component\Form\Exception\TransformationFailedException; - /** * Transforms between an integer and a localized number with grouping * (each thousand) and comma separators. @@ -22,32 +20,28 @@ class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransformer { /** - * {@inheritDoc} + * Constructs a transformer. + * + * @param integer $precision Unused. + * @param Boolean $grouping Whether thousands should be grouped. + * @param integer $roundingMode One of the ROUND_ constants in this class. */ - public function reverseTransform($value) + public function __construct($precision = 0, $grouping = false, $roundingMode = self::ROUND_DOWN) { - if (!is_string($value)) { - throw new TransformationFailedException('Expected a string.'); - } - - if ('' === $value) { - return null; - } - - if ('NaN' === $value) { - throw new TransformationFailedException('"NaN" is not a valid integer'); + if (null === $roundingMode) { + $roundingMode = self::ROUND_DOWN; } - $formatter = $this->getNumberFormatter(); - $value = $formatter->parse( - $value, - PHP_INT_SIZE == 8 ? $formatter::TYPE_INT64 : $formatter::TYPE_INT32 - ); + parent::__construct(0, $grouping, $roundingMode); + } - if (intl_is_failure($formatter->getErrorCode())) { - throw new TransformationFailedException($formatter->getErrorMessage()); - } + /** + * {@inheritDoc} + */ + public function reverseTransform($value) + { + $result = parent::reverseTransform($value); - return $value; + return null !== $result ? (int) $result : null; } } diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php index 5b8e9d96641b7..cfe4b954705a1 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/MoneyToLocalizedStringTransformer.php @@ -21,10 +21,9 @@ */ class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer { - private $divisor; - public function __construct($precision = null, $grouping = null, $roundingMode = null, $divisor = null) + public function __construct($precision = 2, $grouping = true, $roundingMode = self::ROUND_HALF_UP, $divisor = 1) { if (null === $grouping) { $grouping = true; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index b0c59b3ede83b..57416710d8663 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -23,13 +23,75 @@ */ class NumberToLocalizedStringTransformer implements DataTransformerInterface { - const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR; - const ROUND_DOWN = \NumberFormatter::ROUND_DOWN; - const ROUND_HALFDOWN = \NumberFormatter::ROUND_HALFDOWN; - const ROUND_HALFEVEN = \NumberFormatter::ROUND_HALFEVEN; - const ROUND_HALFUP = \NumberFormatter::ROUND_HALFUP; - const ROUND_UP = \NumberFormatter::ROUND_UP; - const ROUND_CEILING = \NumberFormatter::ROUND_CEILING; + /** + * Rounds a number towards positive infinity. + * + * Rounds 1.4 to 2 and -1.4 to -1. + */ + const ROUND_CEILING = \NumberFormatter::ROUND_CEILING; + + /** + * Rounds a number towards negative infinity. + * + * Rounds 1.4 to 1 and -1.4 to -2. + */ + const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR; + + /** + * Rounds a number away from zero. + * + * Rounds 1.4 to 2 and -1.4 to -2. + */ + const ROUND_UP = \NumberFormatter::ROUND_UP; + + /** + * Rounds a number towards zero. + * + * Rounds 1.4 to 1 and -1.4 to -1. + */ + const ROUND_DOWN = \NumberFormatter::ROUND_DOWN; + + /** + * Rounds to the nearest number and halves to the next even number. + * + * Rounds 2.5, 1.6 and 1.5 to 2 and 1.4 to 1. + */ + const ROUND_HALF_EVEN = \NumberFormatter::ROUND_HALFEVEN; + + /** + * Rounds to the nearest number and halves away from zero. + * + * Rounds 2.5 to 3, 1.6 and 1.5 to 2 and 1.4 to 1. + */ + const ROUND_HALF_UP = \NumberFormatter::ROUND_HALFUP; + + /** + * Rounds to the nearest number and halves towards zero. + * + * Rounds 2.5 and 1.6 to 2, 1.5 and 1.4 to 1. + */ + const ROUND_HALF_DOWN = \NumberFormatter::ROUND_HALFDOWN; + + /** + * Alias for {@link self::ROUND_HALF_EVEN}. + * + * @deprecated Deprecated as of Symfony 2.4, to be removed in Symfony 3.0. + */ + const ROUND_HALFEVEN = self::ROUND_HALF_EVEN; + + /** + * Alias for {@link self::ROUND_HALF_UP}. + * + * @deprecated Deprecated as of Symfony 2.4, to be removed in Symfony 3.0. + */ + const ROUND_HALFUP = self::ROUND_HALF_UP; + + /** + * Alias for {@link self::ROUND_HALF_DOWN}. + * + * @deprecated Deprecated as of Symfony 2.4, to be removed in Symfony 3.0. + */ + const ROUND_HALFDOWN = self::ROUND_HALF_DOWN; protected $precision; @@ -37,14 +99,14 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface protected $roundingMode; - public function __construct($precision = null, $grouping = null, $roundingMode = null) + public function __construct($precision = null, $grouping = false, $roundingMode = self::ROUND_HALF_UP) { if (null === $grouping) { $grouping = false; } if (null === $roundingMode) { - $roundingMode = self::ROUND_HALFUP; + $roundingMode = self::ROUND_HALF_UP; } $this->precision = $precision; @@ -160,7 +222,8 @@ public function reverseTransform($value) } } - return $result; + // NumberFormatter::parse() does not round + return $this->round($result); } /** @@ -181,4 +244,48 @@ protected function getNumberFormatter() return $formatter; } + + /** + * Rounds a number according to the configured precision and rounding mode. + * + * @param integer|float $number A number. + * + * @return integer|float The rounded number. + */ + private function round($number) + { + if (null !== $this->precision && null !== $this->roundingMode) { + // shift number to maintain the correct precision during rounding + $roundingCoef = pow(10, $this->precision); + $number *= $roundingCoef; + + switch ($this->roundingMode) { + case self::ROUND_CEILING: + $number = ceil($number); + break; + case self::ROUND_FLOOR: + $number = floor($number); + break; + case self::ROUND_UP: + $number = $number > 0 ? ceil($number) : floor($number); + break; + case self::ROUND_DOWN: + $number = $number > 0 ? floor($number) : ceil($number); + break; + case self::ROUND_HALF_EVEN: + $number = round($number, 0, PHP_ROUND_HALF_EVEN); + break; + case self::ROUND_HALF_UP: + $number = round($number, 0, PHP_ROUND_HALF_UP); + break; + case self::ROUND_HALF_DOWN: + $number = round($number, 0, PHP_ROUND_HALF_DOWN); + break; + } + + $number /= $roundingCoef; + } + + return $number; + } } diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php index f1c39db245427..3f6fb439d7704 100644 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php +++ b/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php @@ -45,12 +45,18 @@ class ResizeFormListener implements EventSubscriberInterface */ protected $allowDelete; - public function __construct($type, array $options = array(), $allowAdd = false, $allowDelete = false) + /** + * @var Boolean + */ + private $deleteEmpty; + + public function __construct($type, array $options = array(), $allowAdd = false, $allowDelete = false, $deleteEmpty = false) { $this->type = $type; $this->allowAdd = $allowAdd; $this->allowDelete = $allowDelete; $this->options = $options; + $this->deleteEmpty = $deleteEmpty; } public static function getSubscribedEvents() @@ -128,6 +134,10 @@ public function onSubmit(FormEvent $event) $form = $event->getForm(); $data = $event->getData(); + // At this point, $data is an array or an array-like object that already contains the + // new entries, which were added by the data mapper. The data mapper ignores existing + // entries, so we need to manually unset removed entries in the collection. + if (null === $data) { $data = array(); } @@ -136,10 +146,24 @@ public function onSubmit(FormEvent $event) throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); } + if ($this->deleteEmpty) { + $previousData = $event->getForm()->getData(); + foreach ($form as $name => $child) { + $isNew = !isset($previousData[$name]); + + // $isNew can only be true if allowAdd is true, so we don't + // need to check allowAdd again + if ($child->isEmpty() && ($isNew || $this->allowDelete)) { + unset($data[$name]); + $form->remove($name); + } + } + } + // The data mapper only adds, but does not remove items, so do this // here if ($this->allowDelete) { - foreach ($data as $name => $child) { + foreach ($data as $name => $childData) { if (!$form->has($name)) { unset($data[$name]); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index 692f91e95c8fa..cdaee86b25b03 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -166,7 +166,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) $choices = null !== $options['choices'] ? $options['choices'] : array(); // Reuse existing choice lists in order to increase performance - $hash = md5(json_encode(array($choices, $options['preferred_choices']))); + $hash = hash('sha256', json_encode(array($choices, $options['preferred_choices']))); if (!isset($choiceListCache[$hash])) { $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php index 0cb3af1bb7f2a..d039f98d6810e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php @@ -37,7 +37,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) $options['type'], $options['options'], $options['allow_add'], - $options['allow_delete'] + $options['allow_delete'], + $options['delete_empty'] ); $builder->addEventSubscriber($resizeListener); @@ -86,6 +87,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'prototype_name' => '__name__', 'type' => 'text', 'options' => array(), + 'delete_empty' => false, )); $resolver->setNormalizers(array( diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index 2c09da6f6b6cf..26dd9f13bb448 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -23,6 +23,11 @@ class FileType extends AbstractType */ public function buildView(FormView $view, FormInterface $form, array $options) { + if ($options['multiple']) { + $view->vars['full_name'] .= '[]'; + $view->vars['attr']['multiple'] = 'multiple'; + } + $view->vars = array_replace($view->vars, array( 'type' => 'file', 'value' => '', @@ -48,6 +53,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'compound' => false, 'data_class' => 'Symfony\Component\HttpFoundation\File\File', 'empty_data' => null, + 'multiple' => false, )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index 8cbfa6fc9949f..7c6e60278324d 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -99,6 +99,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) 'compound' => $form->getConfig()->getCompound(), 'method' => $form->getConfig()->getMethod(), 'action' => $form->getConfig()->getAction(), + 'submitted' => $form->isSubmitted(), )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php index b224cac5fd7e2..8e69f89661cf8 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php @@ -41,19 +41,19 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) 'precision' => null, 'grouping' => false, // Integer cast rounds towards 0, so do the same when displaying fractions - 'rounding_mode' => \NumberFormatter::ROUND_DOWN, + 'rounding_mode' => IntegerToLocalizedStringTransformer::ROUND_DOWN, 'compound' => false, )); $resolver->setAllowedValues(array( 'rounding_mode' => array( - \NumberFormatter::ROUND_FLOOR, - \NumberFormatter::ROUND_DOWN, - \NumberFormatter::ROUND_HALFDOWN, - \NumberFormatter::ROUND_HALFEVEN, - \NumberFormatter::ROUND_HALFUP, - \NumberFormatter::ROUND_UP, - \NumberFormatter::ROUND_CEILING, + IntegerToLocalizedStringTransformer::ROUND_FLOOR, + IntegerToLocalizedStringTransformer::ROUND_DOWN, + IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN, + IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN, + IntegerToLocalizedStringTransformer::ROUND_HALF_UP, + IntegerToLocalizedStringTransformer::ROUND_UP, + IntegerToLocalizedStringTransformer::ROUND_CEILING, ), )); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php index beb3c89a41348..321f25990b728 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/NumberType.php @@ -39,19 +39,19 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) // default precision is locale specific (usually around 3) 'precision' => null, 'grouping' => false, - 'rounding_mode' => \NumberFormatter::ROUND_HALFUP, + 'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALF_UP, 'compound' => false, )); $resolver->setAllowedValues(array( 'rounding_mode' => array( - \NumberFormatter::ROUND_FLOOR, - \NumberFormatter::ROUND_DOWN, - \NumberFormatter::ROUND_HALFDOWN, - \NumberFormatter::ROUND_HALFEVEN, - \NumberFormatter::ROUND_HALFUP, - \NumberFormatter::ROUND_UP, - \NumberFormatter::ROUND_CEILING, + NumberToLocalizedStringTransformer::ROUND_FLOOR, + NumberToLocalizedStringTransformer::ROUND_DOWN, + NumberToLocalizedStringTransformer::ROUND_HALF_DOWN, + NumberToLocalizedStringTransformer::ROUND_HALF_EVEN, + NumberToLocalizedStringTransformer::ROUND_HALF_UP, + NumberToLocalizedStringTransformer::ROUND_UP, + NumberToLocalizedStringTransformer::ROUND_CEILING, ), )); } diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php index 7653e97ee4ced..765a012e8dc73 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php @@ -11,8 +11,11 @@ namespace Symfony\Component\Form\Extension\Csrf; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Translation\TranslatorInterface; /** @@ -23,9 +26,9 @@ class CsrfExtension extends AbstractExtension { /** - * @var CsrfProviderInterface + * @var CsrfTokenManagerInterface */ - private $csrfProvider; + private $tokenManager; /** * @var TranslatorInterface @@ -40,13 +43,19 @@ class CsrfExtension extends AbstractExtension /** * Constructor. * - * @param CsrfProviderInterface $csrfProvider The CSRF provider - * @param TranslatorInterface $translator The translator for translating error messages. - * @param null|string $translationDomain The translation domain for translating. + * @param CsrfTokenManagerInterface $tokenManager The CSRF token manager + * @param TranslatorInterface $translator The translator for translating error messages + * @param null|string $translationDomain The translation domain for translating */ - public function __construct(CsrfProviderInterface $csrfProvider, TranslatorInterface $translator = null, $translationDomain = null) + public function __construct($tokenManager, TranslatorInterface $translator = null, $translationDomain = null) { - $this->csrfProvider = $csrfProvider; + if ($tokenManager instanceof CsrfProviderInterface) { + $tokenManager = new CsrfProviderAdapter($tokenManager); + } elseif (!$tokenManager instanceof CsrfTokenManagerInterface) { + throw new UnexpectedTypeException($tokenManager, 'CsrfProviderInterface or CsrfTokenManagerInterface'); + } + + $this->tokenManager = $tokenManager; $this->translator = $translator; $this->translationDomain = $translationDomain; } @@ -57,7 +66,7 @@ public function __construct(CsrfProviderInterface $csrfProvider, TranslatorInter protected function loadTypeExtensions() { return array( - new Type\FormTypeCsrfExtension($this->csrfProvider, true, '_token', $this->translator, $this->translationDomain), + new Type\FormTypeCsrfExtension($this->tokenManager, true, '_token', $this->translator, $this->translationDomain), ); } } diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderAdapter.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderAdapter.php new file mode 100644 index 0000000000000..39d52e5c6db48 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderAdapter.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Csrf\CsrfProvider; + +use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; + +/** + * Adapter for using old CSRF providers where the new {@link CsrfTokenManagerInterface} + * is expected. + * + * @since 2.4 + * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. + */ +class CsrfProviderAdapter implements CsrfTokenManagerInterface +{ + /** + * @var CsrfProviderInterface + */ + private $csrfProvider; + + public function __construct(CsrfProviderInterface $csrfProvider) + { + $this->csrfProvider = $csrfProvider; + } + + public function getCsrfProvider() + { + return $this->csrfProvider; + } + + /** + * {@inheritdoc} + */ + public function getToken($tokenId) + { + return new CsrfToken($tokenId, $this->csrfProvider->generateCsrfToken($tokenId)); + } + + /** + * {@inheritdoc} + */ + public function refreshToken($tokenId) + { + throw new BadMethodCallException('Not supported'); + } + + /** + * {@inheritdoc} + */ + public function removeToken($tokenId) + { + throw new BadMethodCallException('Not supported'); + } + + /** + * {@inheritdoc} + */ + public function isTokenValid(CsrfToken $token) + { + return $this->csrfProvider->isCsrfTokenValid($token->getId(), $token->getValue()); + } +} diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderInterface.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderInterface.php index 7143b130c8f74..abd681626c0ce 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderInterface.php +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderInterface.php @@ -26,6 +26,10 @@ * the same intention string to validate the CSRF token with isCsrfTokenValid(). * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use + * {@link \Symfony\Component\Security\Csrf\CsrfTokenManagerInterface} + * instead. */ interface CsrfProviderInterface { @@ -34,6 +38,8 @@ interface CsrfProviderInterface * * @param string $intention Some value that identifies the action intention * (i.e. "authenticate"). Doesn't have to be a secret value. + * + * @return string The generated token */ public function generateCsrfToken($intention); diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfTokenManagerAdapter.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfTokenManagerAdapter.php new file mode 100644 index 0000000000000..5dbffc2cbcc79 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfTokenManagerAdapter.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\Form\Extension\Csrf\CsrfProvider; + +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; + +/** + * Adapter for using the new token generator with the old interface. + * + * @since 2.4 + * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. + */ +class CsrfTokenManagerAdapter implements CsrfProviderInterface +{ + /** + * @var CsrfTokenManagerInterface + */ + private $tokenManager; + + public function __construct(CsrfTokenManagerInterface $tokenManager) + { + $this->tokenManager = $tokenManager; + } + + public function getTokenManager() + { + return $this->tokenManager; + } + + /** + * {@inheritdoc} + */ + public function generateCsrfToken($intention) + { + return $this->tokenManager->getToken($intention)->getValue(); + } + + /** + * {@inheritdoc} + */ + public function isCsrfTokenValid($intention, $token) + { + return $this->tokenManager->isTokenValid(new CsrfToken($intention, $token)); + } +} diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/DefaultCsrfProvider.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/DefaultCsrfProvider.php index 5354886cba2c9..db6cde97fca86 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/DefaultCsrfProvider.php +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/DefaultCsrfProvider.php @@ -18,6 +18,11 @@ * user-defined secret value to secure the CSRF token. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use + * {@link \Symfony\Component\Security\Csrf\CsrfTokenManager} in + * combination with {@link \Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage} + * instead. */ class DefaultCsrfProvider implements CsrfProviderInterface { diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/SessionCsrfProvider.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/SessionCsrfProvider.php index ea1fa58547321..741db1ec23395 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/SessionCsrfProvider.php +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfProvider/SessionCsrfProvider.php @@ -20,6 +20,11 @@ * @see DefaultCsrfProvider * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use + * {@link \Symfony\Component\Security\Csrf\CsrfTokenManager} in + * combination with {@link \Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage} + * instead. */ class SessionCsrfProvider extends DefaultCsrfProvider { diff --git a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php index 547e9d756c38b..4e7bc553a9469 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php @@ -12,10 +12,14 @@ namespace Symfony\Component\Form\Extension\Csrf\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Translation\TranslatorInterface; /** @@ -30,20 +34,20 @@ class CsrfValidationListener implements EventSubscriberInterface private $fieldName; /** - * The provider for generating and validating CSRF tokens - * @var CsrfProviderInterface + * The generator for CSRF tokens + * @var CsrfTokenManagerInterface */ - private $csrfProvider; + private $tokenManager; /** - * A text mentioning the intention of the CSRF token + * A text mentioning the tokenId of the CSRF token * * Validation of the token will only succeed if it was generated in the - * same session and with the same intention. + * same session and with the same tokenId. * * @var string */ - private $intention; + private $tokenId; /** * The message displayed in case of an error. @@ -68,11 +72,17 @@ public static function getSubscribedEvents() ); } - public function __construct($fieldName, CsrfProviderInterface $csrfProvider, $intention, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null) + public function __construct($fieldName, $tokenManager, $tokenId, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null) { + if ($tokenManager instanceof CsrfProviderInterface) { + $tokenManager = new CsrfProviderAdapter($tokenManager); + } elseif (!$tokenManager instanceof CsrfTokenManagerInterface) { + throw new UnexpectedTypeException($tokenManager, 'CsrfProviderInterface or CsrfTokenManagerInterface'); + } + $this->fieldName = $fieldName; - $this->csrfProvider = $csrfProvider; - $this->intention = $intention; + $this->tokenManager = $tokenManager; + $this->tokenId = $tokenId; $this->errorMessage = $errorMessage; $this->translator = $translator; $this->translationDomain = $translationDomain; @@ -84,7 +94,7 @@ public function preSubmit(FormEvent $event) $data = $event->getData(); if ($form->isRoot() && $form->getConfig()->getOption('compound')) { - if (!isset($data[$this->fieldName]) || !$this->csrfProvider->isCsrfTokenValid($this->intention, $data[$this->fieldName])) { + if (!isset($data[$this->fieldName]) || !$this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName]))) { $errorMessage = $this->errorMessage; if (null !== $this->translator) { diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php index 49d97bb395f77..d5a111ca0adfd 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php @@ -12,12 +12,17 @@ namespace Symfony\Component\Form\Extension\Csrf\Type; use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfTokenManagerAdapter; use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormInterface; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Translation\TranslatorInterface; /** @@ -26,9 +31,9 @@ class FormTypeCsrfExtension extends AbstractTypeExtension { /** - * @var CsrfProviderInterface + * @var CsrfTokenManagerInterface */ - private $defaultCsrfProvider; + private $defaultTokenManager; /** * @var Boolean @@ -50,9 +55,15 @@ class FormTypeCsrfExtension extends AbstractTypeExtension */ private $translationDomain; - public function __construct(CsrfProviderInterface $defaultCsrfProvider, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null) + public function __construct($defaultTokenManager, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null) { - $this->defaultCsrfProvider = $defaultCsrfProvider; + if ($defaultTokenManager instanceof CsrfProviderInterface) { + $defaultTokenManager = new CsrfProviderAdapter($defaultTokenManager); + } elseif (!$defaultTokenManager instanceof CsrfTokenManagerInterface) { + throw new UnexpectedTypeException($defaultTokenManager, 'CsrfProviderInterface or CsrfTokenManagerInterface'); + } + + $this->defaultTokenManager = $defaultTokenManager; $this->defaultEnabled = $defaultEnabled; $this->defaultFieldName = $defaultFieldName; $this->translator = $translator; @@ -72,11 +83,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) } $builder - ->setAttribute('csrf_factory', $builder->getFormFactory()) ->addEventSubscriber(new CsrfValidationListener( $options['csrf_field_name'], - $options['csrf_provider'], - $options['intention'] ?: ($builder->getName() ?: get_class($builder->getType()->getInnerType())), + $options['csrf_token_manager'], + $options['csrf_token_id'] ?: ($builder->getName() ?: get_class($builder->getType()->getInnerType())), $options['csrf_message'], $this->translator, $this->translationDomain @@ -94,9 +104,9 @@ public function buildForm(FormBuilderInterface $builder, array $options) public function finishView(FormView $view, FormInterface $form, array $options) { if ($options['csrf_protection'] && !$view->parent && $options['compound']) { - $factory = $form->getConfig()->getAttribute('csrf_factory'); - $intention = $options['intention'] ?: ($form->getName() ?: get_class($form->getConfig()->getType()->getInnerType())); - $data = $options['csrf_provider']->generateCsrfToken($intention); + $factory = $form->getConfig()->getFormFactory(); + $tokenId = $options['csrf_token_id'] ?: ($form->getName() ?: get_class($form->getConfig()->getType()->getInnerType())); + $data = (string) $options['csrf_token_manager']->getToken($tokenId); $csrfForm = $factory->createNamed($options['csrf_field_name'], 'hidden', $data, array( 'mapped' => false, @@ -111,12 +121,26 @@ public function finishView(FormView $view, FormInterface $form, array $options) */ public function setDefaultOptions(OptionsResolverInterface $resolver) { + // BC clause for the "intention" option + $csrfTokenId = function (Options $options) { + return $options['intention']; + }; + + // BC clause for the "csrf_provider" option + $csrfTokenManager = function (Options $options) { + return $options['csrf_provider'] instanceof CsrfTokenManagerAdapter + ? $options['csrf_provider']->getTokenManager() + : new CsrfProviderAdapter($options['csrf_provider']); + }; + $resolver->setDefaults(array( - 'csrf_protection' => $this->defaultEnabled, - 'csrf_field_name' => $this->defaultFieldName, - 'csrf_provider' => $this->defaultCsrfProvider, - 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', - 'intention' => null, + 'csrf_protection' => $this->defaultEnabled, + 'csrf_field_name' => $this->defaultFieldName, + 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', + 'csrf_token_manager' => $csrfTokenManager, + 'csrf_token_id' => $csrfTokenId, + 'csrf_provider' => new CsrfTokenManagerAdapter($this->defaultTokenManager), + 'intention' => null, )); } diff --git a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php new file mode 100644 index 0000000000000..941bd2102e07a --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/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\DataCollector; + +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/DataCollector/EventListener/DataCollectorListener.php b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php new file mode 100644 index 0000000000000..4822989b58536 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Listener that invokes a data collector for the {@link FormEvents::POST_SET_DATA} + * and {@link FormEvents::POST_SUBMIT} events. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class DataCollectorListener implements EventSubscriberInterface +{ + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + /** + * {@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), + ); + } + + /** + * 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->dataCollector->collectConfiguration($event->getForm()); + + // Collect the default data + $this->dataCollector->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->dataCollector->collectSubmittedData($event->getForm()); + + // Assemble a form tree + // This is done again in collectViewVariables(), but that method + // is not guaranteed to be called (i.e. when no view is created) + $this->dataCollector->buildPreliminaryFormTree($event->getForm()); + } + } + +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php new file mode 100644 index 0000000000000..bcadfc59773ef --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php @@ -0,0 +1,279 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; + +/** + * Data collector for {@link \Symfony\Component\Form\FormInterface} instances. + * + * @since 2.4 + * @author Robert Schönthal + * @author Bernhard Schussek + */ +class FormDataCollector extends DataCollector implements FormDataCollectorInterface +{ + /** + * @var FormDataExtractor + */ + private $dataExtractor; + + /** + * Stores the collected data per {@link FormInterface} instance. + * + * Uses the hashes of the forms as keys. This is preferable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormInterface} instances. + * + * @var array + */ + private $dataByForm; + + /** + * Stores the collected data per {@link FormView} instance. + * + * Uses the hashes of the views as keys. This is preferable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormView} instances. + * + * @var array + */ + private $dataByView; + + /** + * Connects {@link FormView} with {@link FormInterface} instances. + * + * Uses the hashes of the views as keys and the hashes of the forms as + * values. This is preferable over storing the objects directly, because + * this way they can safely be discarded by the GC. + * + * @var array + */ + private $formsByView; + + public function __construct(FormDataExtractorInterface $dataExtractor) + { + $this->dataExtractor = $dataExtractor; + $this->data = array( + 'forms' => array(), + 'forms_by_hash' => array(), + 'nb_errors' => 0, + ); + } + + /** + * Does nothing. The data is collected during the form event listeners. + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + } + + /** + * {@inheritdoc} + */ + public function associateFormWithView(FormInterface $form, FormView $view) + { + $this->formsByView[spl_object_hash($view)] = spl_object_hash($form); + } + + /** + * {@inheritdoc} + */ + public 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} + */ + public 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} + */ + public function collectSubmittedData(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->extractSubmittedData($form) + ); + + // Count errors + if (isset($this->dataByForm[$hash]['errors'])) { + $this->data['nb_errors'] += count($this->dataByForm[$hash]['errors']); + } + + foreach ($form as $child) { + $this->collectSubmittedData($child); + } + } + + /** + * {@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} + */ + public function buildPreliminaryFormTree(FormInterface $form) + { + $this->data['forms'][$form->getName()] = array(); + + $this->recursiveBuildPreliminaryFormTree($form, $this->data['forms'][$form->getName()], $this->data['forms_by_hash']); + } + + /** + * {@inheritdoc} + */ + public function buildFinalFormTree(FormInterface $form, FormView $view) + { + $this->data['forms'][$form->getName()] = array(); + + $this->recursiveBuildFinalFormTree($form, $view, $this->data['forms'][$form->getName()], $this->data['forms_by_hash']); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'form'; + } + + /** + * {@inheritdoc} + */ + public function getData() + { + return $this->data; + } + + private function recursiveBuildPreliminaryFormTree(FormInterface $form, &$output = null, array &$outputByHash) + { + $hash = spl_object_hash($form); + + $output = isset($this->dataByForm[$hash]) + ? $this->dataByForm[$hash] + : array(); + + $outputByHash[$hash] = &$output; + + $output['children'] = array(); + + foreach ($form as $name => $child) { + $output['children'][$name] = array(); + + $this->recursiveBuildPreliminaryFormTree($child, $output['children'][$name], $outputByHash); + } + } + + private function recursiveBuildFinalFormTree(FormInterface $form = null, FormView $view, &$output = null, array &$outputByHash) + { + $viewHash = spl_object_hash($view); + $formHash = null; + + if (null !== $form) { + $formHash = spl_object_hash($form); + } 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 = isset($this->dataByView[$viewHash]) + ? $this->dataByView[$viewHash] + : array(); + + if (null !== $formHash) { + $output = array_replace( + $output, + isset($this->dataByForm[$formHash]) + ? $this->dataByForm[$formHash] + : array() + ); + + $outputByHash[$formHash] = &$output; + } + + $output['children'] = 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; + + $output['children'][$name] = array(); + + $this->recursiveBuildFinalFormTree($childForm, $childView, $output['children'][$name], $outputByHash); + } + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php new file mode 100644 index 0000000000000..9a805029f859e --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.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\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; + +/** + * Collects and structures information about forms. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface FormDataCollectorInterface extends DataCollectorInterface +{ + /** + * Stores configuration data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectConfiguration(FormInterface $form); + + /** + * Stores the default data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectDefaultData(FormInterface $form); + + /** + * Stores the submitted data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectSubmittedData(FormInterface $form); + + /** + * 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()}. + * + * @param FormInterface $form A root form + */ + public function buildPreliminaryFormTree(FormInterface $form); + + /** + * 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); + + /** + * Returns all collected data. + * + * @return array + */ + public function getData(); + +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php new file mode 100644 index 0000000000000..80e910177a894 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpKernel\DataCollector\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. + */ + 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, + ); + + $cause = $error->getCause(); + + if ($cause instanceof ConstraintViolationInterface) { + $errorData['cause'] = array( + 'root' => $this->valueExporter->exportValue($cause->getRoot()), + 'path' => $this->valueExporter->exportValue($cause->getPropertyPath()), + 'value' => $this->valueExporter->exportValue($cause->getInvalidValue()), + ); + } else { + $errorData['cause'] = null !== $cause ? $this->valueExporter->exportValue($cause) : null; + } + + $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/DataCollector/FormDataExtractorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php new file mode 100644 index 0000000000000..d47496552d0d0 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/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\DataCollector; + +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/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php new file mode 100644 index 0000000000000..960048a0c2ee6 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/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\DataCollector\Proxy; + +use Symfony\Component\Form\Extension\DataCollector\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/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php new file mode 100644 index 0000000000000..c2cb3a03469fe --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/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\DataCollector\Proxy; + +use Symfony\Component\Form\Extension\DataCollector\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/DataCollector/Type/DataCollectorTypeExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php new file mode 100644 index 0000000000000..1b5cfb695d1e8 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.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\Form\Extension\DataCollector\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener; +use Symfony\Component\Form\Extension\DataCollector\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 $listener; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->listener = new DataCollectorListener($dataCollector); + } + + /** + * {@inheritDoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber($this->listener); + } + + /** + * {@inheritDoc} + */ + public function getExtendedType() + { + return 'form'; + } +} diff --git a/src/Symfony/Component/Form/Extension/DependencyInjection/DependencyInjectionExtension.php b/src/Symfony/Component/Form/Extension/DependencyInjection/DependencyInjectionExtension.php index 6637ac8c6397f..915c43fa629a6 100644 --- a/src/Symfony/Component/Form/Extension/DependencyInjection/DependencyInjectionExtension.php +++ b/src/Symfony/Component/Form/Extension/DependencyInjection/DependencyInjectionExtension.php @@ -22,6 +22,8 @@ class DependencyInjectionExtension implements FormExtensionInterface private $typeServiceIds; + private $typeExtensionServiceIds; + private $guesserServiceIds; private $guesser; diff --git a/src/Symfony/Component/Form/Extension/Templating/TemplatingExtension.php b/src/Symfony/Component/Form/Extension/Templating/TemplatingExtension.php index 573cb518d4c96..09185a3e4d82d 100644 --- a/src/Symfony/Component/Form/Extension/Templating/TemplatingExtension.php +++ b/src/Symfony/Component/Form/Extension/Templating/TemplatingExtension.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Form\Extension\Templating; use Symfony\Component\Form\AbstractExtension; -use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Templating\PhpEngine; use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper; @@ -24,10 +27,16 @@ */ class TemplatingExtension extends AbstractExtension { - public function __construct(PhpEngine $engine, CsrfProviderInterface $csrfProvider = null, array $defaultThemes = array()) + public function __construct(PhpEngine $engine, $csrfTokenManager = null, array $defaultThemes = array()) { + if ($csrfTokenManager instanceof CsrfProviderInterface) { + $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); + } elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) { + throw new UnexpectedTypeException($csrfTokenManager, 'CsrfProviderInterface or CsrfTokenManagerInterface'); + } + $engine->addHelpers(array( - new FormHelper(new FormRenderer(new TemplatingRendererEngine($engine, $defaultThemes), $csrfProvider)) + new FormHelper(new FormRenderer(new TemplatingRendererEngine($engine, $defaultThemes), $csrfTokenManager)) )); } } diff --git a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php index 1414753153452..26d2bfdaa7eb8 100644 --- a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php @@ -54,14 +54,12 @@ public function validateForm(FormEvent $event) // Validate the form in group "Default" $violations = $this->validator->validate($form); - if (count($violations) > 0) { - foreach ($violations as $violation) { - // Allow the "invalid" constraint to be put onto - // non-synchronized forms - $allowNonSynchronized = Form::ERR_INVALID === $violation->getCode(); + foreach ($violations as $violation) { + // Allow the "invalid" constraint to be put onto + // non-synchronized forms + $allowNonSynchronized = Form::ERR_INVALID === $violation->getCode(); - $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized); - } + $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized); } } } diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php index 6ea56eb03b242..15ceb0cf08bd7 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -128,7 +128,8 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form $violation->getMessage(), $violation->getMessageTemplate(), $violation->getMessageParameters(), - $violation->getMessagePluralization() + $violation->getMessagePluralization(), + $violation )); } } diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 86cf7921a1fe1..f4696c0dfed34 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -556,7 +556,7 @@ public function submit($submittedData, $clearMissing = true) } foreach ($this->children as $name => $child) { - if (isset($submittedData[$name]) || $clearMissing) { + if (array_key_exists($name, $submittedData) || $clearMissing) { $child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing); unset($submittedData[$name]); @@ -673,6 +673,10 @@ public function bind($submittedData) public function addError(FormError $error) { if ($this->parent && $this->config->getErrorBubbling()) { + if (null === $error->getOrigin()) { + $error->setOrigin($this); + } + $this->parent->addError($error); } else { $this->errors[] = $error; @@ -899,7 +903,9 @@ public function remove($name) } if (isset($this->children[$name])) { - $this->children[$name]->setParent(null); + if (!$this->children[$name]->isSubmitted()) { + $this->children[$name]->setParent(null); + } unset($this->children[$name]); } @@ -1010,7 +1016,23 @@ public function createView(FormView $parent = null) $parent = $this->parent->createView(); } - return $this->config->getType()->createView($this, $parent); + $type = $this->config->getType(); + $options = $this->config->getOptions(); + + // The methods createView(), buildView() and finishView() are called + // explicitly here in order to be able to override either of them + // in a custom resolved form type. + $view = $type->createView($this, $parent); + + $type->buildView($view, $this, $options); + + foreach ($this->children as $name => $child) { + $view->children[$name] = $child->createView($view); + } + + $type->finishView($view, $this, $options); + + return $view; } /** diff --git a/src/Symfony/Component/Form/FormError.php b/src/Symfony/Component/Form/FormError.php index 343165ca465d8..6970244cf7c18 100644 --- a/src/Symfony/Component/Form/FormError.php +++ b/src/Symfony/Component/Form/FormError.php @@ -11,12 +11,14 @@ namespace Symfony\Component\Form; +use Symfony\Component\Form\Exception\BadMethodCallException; + /** * Wraps errors in forms * * @author Bernhard Schussek */ -class FormError +class FormError implements \Serializable { /** * @var string @@ -41,6 +43,18 @@ class FormError */ protected $messagePluralization; + /** + * The cause for this error + * @var mixed + */ + private $cause; + + /** + * The form that spawned this error + * @var FormInterface + */ + private $origin; + /** * Constructor * @@ -50,17 +64,19 @@ class FormError * @param string $message The translated error message * @param string|null $messageTemplate The template for the error message * @param array $messageParameters The parameters that should be - * substituted in the message template. + * substituted in the message template * @param integer|null $messagePluralization The value for error message pluralization + * @param mixed $cause The cause of the error * * @see \Symfony\Component\Translation\Translator */ - public function __construct($message, $messageTemplate = null, array $messageParameters = array(), $messagePluralization = null) + public function __construct($message, $messageTemplate = null, array $messageParameters = array(), $messagePluralization = null, $cause = null) { $this->message = $message; $this->messageTemplate = $messageTemplate ?: $message; $this->messageParameters = $messageParameters; $this->messagePluralization = $messagePluralization; + $this->cause = $cause; } /** @@ -102,4 +118,68 @@ public function getMessagePluralization() { return $this->messagePluralization; } + + /** + * Returns the cause of this error. + * + * @return mixed The cause of this error + */ + public function getCause() + { + return $this->cause; + } + + /** + * Sets the form that caused this error. + * + * This method must only be called once. + * + * @param FormInterface $origin The form that caused this error + * + * @throws BadMethodCallException If the method is called more than once + */ + public function setOrigin(FormInterface $origin) + { + if (null !== $this->origin) { + throw new BadMethodCallException('setOrigin() must only be called once.'); + } + + $this->origin = $origin; + } + + /** + * Returns the form that caused this error. + * + * @return FormInterface The form that caused this error + */ + public function getOrigin() + { + return $this->origin; + } + + /** + * Serializes this error. + * + * @return string The serialized error + */ + public function serialize() + { + return serialize(array( + $this->message, + $this->messageTemplate, + $this->messageParameters, + $this->messagePluralization, + $this->cause + )); + } + + /** + * Unserializes a serialized error. + * + * @param string $serialized The serialized error + */ + public function unserialize($serialized) + { + list($this->message, $this->messageTemplate, $this->messageParameters, $this->messagePluralization, $this->cause) = unserialize($serialized); + } } diff --git a/src/Symfony/Component/Form/FormFactory.php b/src/Symfony/Component/Form/FormFactory.php index a5fd9cc0c67f2..d76e73101b938 100644 --- a/src/Symfony/Component/Form/FormFactory.php +++ b/src/Symfony/Component/Form/FormFactory.php @@ -84,7 +84,13 @@ public function createNamedBuilder($name, $type = 'form', $data = null, array $o throw new UnexpectedTypeException($type, 'string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface'); } - return $type->createBuilder($this, $name, $options); + $builder = $type->createBuilder($this, $name, $options); + + // Explicitly call buildForm() in order to be able to override either + // createBuilder() or buildForm() in the resolved form type + $type->buildForm($builder, $builder->getOptions()); + + return $builder; } /** diff --git a/src/Symfony/Component/Form/FormRenderer.php b/src/Symfony/Component/Form/FormRenderer.php index 09b010563f8c5..c5faee1548ce5 100644 --- a/src/Symfony/Component/Form/FormRenderer.php +++ b/src/Symfony/Component/Form/FormRenderer.php @@ -13,7 +13,10 @@ use Symfony\Component\Form\Exception\LogicException; use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; /** * Renders a form into HTML using a rendering engine. @@ -30,9 +33,9 @@ class FormRenderer implements FormRendererInterface private $engine; /** - * @var CsrfProviderInterface + * @var CsrfTokenManagerInterface */ - private $csrfProvider; + private $csrfTokenManager; /** * @var array @@ -49,10 +52,24 @@ class FormRenderer implements FormRendererInterface */ private $variableStack = array(); - public function __construct(FormRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null) + /** + * Constructor. + * + * @param FormRendererEngineInterface $engine + * @param CsrfTokenManagerInterface|null $csrfTokenManager + * + * @throws UnexpectedTypeException + */ + public function __construct(FormRendererEngineInterface $engine, $csrfTokenManager = null) { + if ($csrfTokenManager instanceof CsrfProviderInterface) { + $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); + } elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) { + throw new UnexpectedTypeException($csrfTokenManager, 'CsrfProviderInterface or CsrfTokenManagerInterface or null'); + } + $this->engine = $engine; - $this->csrfProvider = $csrfProvider; + $this->csrfTokenManager = $csrfTokenManager; } /** @@ -74,13 +91,13 @@ public function setTheme(FormView $view, $themes) /** * {@inheritdoc} */ - public function renderCsrfToken($intention) + public function renderCsrfToken($tokenId) { - if (null === $this->csrfProvider) { - throw new BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.'); + if (null === $this->csrfTokenManager) { + throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct().'); } - return $this->csrfProvider->generateCsrfToken($intention); + return $this->csrfTokenManager->getToken($tokenId)->getValue(); } /** diff --git a/src/Symfony/Component/Form/FormRendererInterface.php b/src/Symfony/Component/Form/FormRendererInterface.php index 848e7125ca329..7cf24c0b00b86 100644 --- a/src/Symfony/Component/Form/FormRendererInterface.php +++ b/src/Symfony/Component/Form/FormRendererInterface.php @@ -73,20 +73,20 @@ public function searchAndRenderBlock(FormView $view, $blockNameSuffix, array $va * * * - * Check the token in your action using the same intention. + * Check the token in your action using the same token ID. * * - * $csrfProvider = $this->get('form.csrf_provider'); + * $csrfProvider = $this->get('security.csrf.token_generator'); * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) { * throw new \RuntimeException('CSRF attack detected.'); * } * * - * @param string $intention The intention of the protected action + * @param string $tokenId The ID of the CSRF token * * @return string A CSRF token */ - public function renderCsrfToken($intention); + public function renderCsrfToken($tokenId); /** * Makes a technical name human readable. diff --git a/src/Symfony/Component/Form/README.md b/src/Symfony/Component/Form/README.md index 4396ec0155e17..340bc6e63137f 100644 --- a/src/Symfony/Component/Form/README.md +++ b/src/Symfony/Component/Form/README.md @@ -14,7 +14,7 @@ https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/FormServiceProvid Documentation: -http://symfony.com/doc/2.3/book/forms.html +http://symfony.com/doc/2.5/book/forms.html Resources --------- diff --git a/src/Symfony/Component/Form/ResolvedFormType.php b/src/Symfony/Component/Form/ResolvedFormType.php index 0f676e53de062..f8d87d0f97ae5 100644 --- a/src/Symfony/Component/Form/ResolvedFormType.php +++ b/src/Symfony/Component/Form/ResolvedFormType.php @@ -114,8 +114,6 @@ public function createBuilder(FormFactoryInterface $factory, $name, array $optio $builder = $this->newBuilder($name, $dataClass, $factory, $options); $builder->setType($this); - $this->buildForm($builder, $options); - return $builder; } @@ -124,28 +122,12 @@ public function createBuilder(FormFactoryInterface $factory, $name, array $optio */ public function createView(FormInterface $form, FormView $parent = null) { - $options = $form->getConfig()->getOptions(); - - $view = $this->newView($parent); - - $this->buildView($view, $form, $options); - - foreach ($form as $name => $child) { - /* @var FormInterface $child */ - $view->children[$name] = $child->createView($view); - } - - $this->finishView($view, $form, $options); - - return $view; + return $this->newView($parent); } /** * Configures a form builder for the type hierarchy. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createBuilder()}. - * * @param FormBuilderInterface $builder The builder to configure. * @param array $options The options used for the configuration. */ @@ -166,10 +148,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) /** * Configures a form view for the type hierarchy. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createView()}. - * - * It is called before the children of the view are built. + * This method is called before the children of the view are built. * * @param FormView $view The form view to configure. * @param FormInterface $form The form corresponding to the view. @@ -192,10 +171,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) /** * Finishes a form view for the type hierarchy. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createView()}. - * - * It is called after the children of the view have been built. + * This method is called after the children of the view have been built. * * @param FormView $view The form view to configure. * @param FormInterface $form The form corresponding to the view. @@ -218,9 +194,6 @@ public function finishView(FormView $view, FormInterface $form, array $options) /** * Returns the configured options resolver used for this type. * - * This method is protected in order to allow implementing classes - * to change or call it in re-implementations of {@link createBuilder()}. - * * @return \Symfony\Component\OptionsResolver\OptionsResolverInterface The options resolver. */ public function getOptionsResolver() diff --git a/src/Symfony/Component/Form/Resources/translations/validators.hu.xlf b/src/Symfony/Component/Form/Resources/translations/validators.hu.xlf index d1491e7e2a6a7..374cfaaea34ac 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.hu.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.hu.xlf @@ -8,11 +8,11 @@ The uploaded file was too large. Please try to upload a smaller file. - A feltöltött fájl túl nagy. Kérem próbáljon egy kisebb fájlt feltölteni. + A feltöltött fájl túl nagy. Kérem, próbáljon egy kisebb fájlt feltölteni. The CSRF token is invalid. Please try to resubmit the form. - Érvénytelen CSRF token. Kérem próbálja újra elküldeni az űrlapot. + Érvénytelen CSRF token. Kérem, próbálja újra elküldeni az űrlapot. diff --git a/src/Symfony/Component/Form/Test/FormIntegrationTestCase.php b/src/Symfony/Component/Form/Test/FormIntegrationTestCase.php index 68e5f2445ec8c..82894d552270a 100644 --- a/src/Symfony/Component/Form/Test/FormIntegrationTestCase.php +++ b/src/Symfony/Component/Form/Test/FormIntegrationTestCase.php @@ -25,10 +25,6 @@ abstract class FormIntegrationTestCase extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->factory = Forms::createFormFactoryBuilder() ->addExtensions($this->getExtensions()) ->getFormFactory(); diff --git a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php index ee9ed8f2a6a77..2d32aa38a398b 100644 --- a/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractDivLayoutTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Form\FormError; use Symfony\Component\Form\Tests\Fixtures\AlternatingRowType; +use Symfony\Component\Security\Csrf\CsrfToken; abstract class AbstractDivLayoutTest extends AbstractLayoutTest { @@ -471,9 +472,9 @@ public function testNestedFormError() public function testCsrf() { - $this->csrfProvider->expects($this->any()) - ->method('generateCsrfToken') - ->will($this->returnValue('foo&bar')); + $this->csrfTokenManager->expects($this->any()) + ->method('getToken') + ->will($this->returnValue(new CsrfToken('token_id', 'foo&bar'))); $form = $this->factory->createNamedBuilder('name', 'form') ->add($this->factory @@ -729,4 +730,42 @@ public function testFormEndWithoutRest() $this->assertEquals('', $html); } + + public function testWidgetContainerAttributes() + { + $form = $this->factory->createNamed('form', 'form', null, array( + 'attr' => array('class' => 'foobar', 'data-foo' => 'bar'), + )); + + $form->add('text', 'text'); + + $html = $this->renderWidget($form->createView()); + + // compare plain HTML to check the whitespace + $this->assertContains('
    ', $html); + } + + public function testWidgetContainerAttributeNameRepeatedIfTrue() + { + $form = $this->factory->createNamed('form', 'form', null, array( + 'attr' => array('foo' => true), + )); + + $html = $this->renderWidget($form->createView()); + + // foo="foo" + $this->assertContains('
    ', $html); + } + + public function testWidgetContainerAttributeHiddenIfFalse() + { + $form = $this->factory->createNamed('form', 'form', null, array( + 'attr' => array('foo' => false), + )); + + $html = $this->renderWidget($form->createView()); + + // no foo + $this->assertContains('
    ', $html); + } } diff --git a/src/Symfony/Component/Form/Tests/AbstractFormTest.php b/src/Symfony/Component/Form/Tests/AbstractFormTest.php index 77b0188a03acd..b6cd54cd82930 100644 --- a/src/Symfony/Component/Form/Tests/AbstractFormTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractFormTest.php @@ -34,10 +34,6 @@ abstract class AbstractFormTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - // We need an actual dispatcher to use the deprecated // bindRequest() method $this->dispatcher = new EventDispatcher(); @@ -61,12 +57,13 @@ abstract protected function createForm(); * @param string $name * @param EventDispatcherInterface $dispatcher * @param string $dataClass + * @param array $options * * @return FormBuilder */ - protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null, $dataClass = null) + protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null, $dataClass = null, array $options = array()) { - return new FormBuilder($name, $dataClass, $dispatcher ?: $this->dispatcher, $this->factory); + return new FormBuilder($name, $dataClass, $dispatcher ?: $this->dispatcher, $this->factory, $options); } /** diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 10650e5998aae..28827e51b75c8 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -17,7 +17,7 @@ abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormIntegrationTestCase { - protected $csrfProvider; + protected $csrfTokenManager; protected function setUp() { @@ -27,7 +27,7 @@ protected function setUp() \Locale::setDefault('en'); - $this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'); + $this->csrfTokenManager = $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface'); parent::setUp(); } @@ -35,13 +35,13 @@ protected function setUp() protected function getExtensions() { return array( - new CsrfExtension($this->csrfProvider), + new CsrfExtension($this->csrfTokenManager), ); } protected function tearDown() { - $this->csrfProvider = null; + $this->csrfTokenManager = null; parent::tearDown(); } @@ -302,8 +302,9 @@ public function testErrors() ); } - public function testWidgetById() + public function testOverrideWidgetBlock() { + // see custom_widgets.html.twig $form = $this->factory->createNamed('text_id', 'text'); $html = $this->renderWidget($form->createView()); @@ -1828,7 +1829,7 @@ public function testStartTag() $html = $this->renderStart($form->createView()); - $this->assertSame('
    ', $html); + $this->assertSame('', $html); } public function testStartTagForPutRequest() @@ -1860,7 +1861,7 @@ public function testStartTagWithOverriddenVars() 'action' => 'http://foo.com/directory' )); - $this->assertSame('', $html); + $this->assertSame('', $html); } public function testStartTagForMultipartForm() @@ -1874,7 +1875,7 @@ public function testStartTagForMultipartForm() $html = $this->renderStart($form->createView()); - $this->assertSame('', $html); + $this->assertSame('', $html); } public function testStartTagWithExtraAttributes() @@ -1888,6 +1889,84 @@ public function testStartTagWithExtraAttributes() 'attr' => array('class' => 'foobar'), )); - $this->assertSame('', $html); + $this->assertSame('', $html); + } + + public function testWidgetAttributes() + { + $form = $this->factory->createNamed('text', 'text', 'value', array( + 'required' => true, + 'disabled' => true, + 'read_only' => true, + 'max_length' => 10, + 'pattern' => '\d+', + 'attr' => array('class' => 'foobar', 'data-foo' => 'bar'), + )); + + $html = $this->renderWidget($form->createView()); + + // compare plain HTML to check the whitespace + $this->assertSame('', $html); + } + + public function testWidgetAttributeNameRepeatedIfTrue() + { + $form = $this->factory->createNamed('text', 'text', 'value', array( + 'attr' => array('foo' => true), + )); + + $html = $this->renderWidget($form->createView()); + + // foo="foo" + $this->assertSame('', $html); + } + + public function testWidgetAttributeHiddenIfFalse() + { + $form = $this->factory->createNamed('text', 'text', 'value', array( + 'attr' => array('foo' => false), + )); + + $html = $this->renderWidget($form->createView()); + + // no foo + $this->assertSame('', $html); + } + + public function testButtonAttributes() + { + $form = $this->factory->createNamed('button', 'button', null, array( + 'disabled' => true, + 'attr' => array('class' => 'foobar', 'data-foo' => 'bar'), + )); + + $html = $this->renderWidget($form->createView()); + + // compare plain HTML to check the whitespace + $this->assertSame('', $html); + } + + public function testButtonAttributeNameRepeatedIfTrue() + { + $form = $this->factory->createNamed('button', 'button', null, array( + 'attr' => array('foo' => true), + )); + + $html = $this->renderWidget($form->createView()); + + // foo="foo" + $this->assertSame('', $html); + } + + public function testButtonAttributeHiddenIfFalse() + { + $form = $this->factory->createNamed('button', 'button', null, array( + 'attr' => array('foo' => false), + )); + + $html = $this->renderWidget($form->createView()); + + // no foo + $this->assertSame('', $html); } } diff --git a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php index 5c91195169951..5c8a9be2d5959 100644 --- a/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractTableLayoutTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Tests; use Symfony\Component\Form\FormError; +use Symfony\Component\Security\Csrf\CsrfToken; abstract class AbstractTableLayoutTest extends AbstractLayoutTest { @@ -336,9 +337,9 @@ public function testNestedFormError() public function testCsrf() { - $this->csrfProvider->expects($this->any()) - ->method('generateCsrfToken') - ->will($this->returnValue('foo&bar')); + $this->csrfTokenManager->expects($this->any()) + ->method('getToken') + ->will($this->returnValue(new CsrfToken('token_id', 'foo&bar'))); $form = $this->factory->createNamedBuilder('name', 'form') ->add($this->factory @@ -506,4 +507,42 @@ public function testFormEndWithoutRest() $this->assertEquals('', $html); } + + public function testWidgetContainerAttributes() + { + $form = $this->factory->createNamed('form', 'form', null, array( + 'attr' => array('class' => 'foobar', 'data-foo' => 'bar'), + )); + + $form->add('text', 'text'); + + $html = $this->renderWidget($form->createView()); + + // compare plain HTML to check the whitespace + $this->assertContains('
    ', $html); + } + + public function testWidgetContainerAttributeNameRepeatedIfTrue() + { + $form = $this->factory->createNamed('form', 'form', null, array( + 'attr' => array('foo' => true), + )); + + $html = $this->renderWidget($form->createView()); + + // foo="foo" + $this->assertContains('
    ', $html); + } + + public function testWidgetContainerAttributeHiddenIfFalse() + { + $form = $this->factory->createNamed('form', 'form', null, array( + 'attr' => array('foo' => false), + )); + + $html = $this->renderWidget($form->createView()); + + // no foo + $this->assertContains('
    ', $html); + } } diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index fe3755c1720aa..32a09719ae851 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Forms; +use Symfony\Component\Form\FormView; use Symfony\Component\Form\SubmitButtonBuilder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -82,6 +83,19 @@ public function testDisabledFormsValidEvenIfChildrenInvalid() $this->assertTrue($form->isValid()); } + public function testSubmitForwardsNullIfNotClearMissingButValueIsExplicitlyNull() + { + $child = $this->getMockForm('firstName'); + + $this->form->add($child); + + $child->expects($this->once()) + ->method('submit') + ->with($this->equalTo(null)); + + $this->form->submit(array('firstName' => null), false); + } + public function testSubmitForwardsNullIfValueIsMissing() { $child = $this->getMockForm('firstName'); @@ -583,10 +597,6 @@ public function requestMethodProvider() */ public function testSubmitPostOrPutRequest($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $path = tempnam(sys_get_temp_dir(), 'sf2'); touch($path); @@ -635,10 +645,6 @@ public function testSubmitPostOrPutRequest($method) */ public function testSubmitPostOrPutRequestWithEmptyRootFormName($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $path = tempnam(sys_get_temp_dir(), 'sf2'); touch($path); @@ -686,10 +692,6 @@ public function testSubmitPostOrPutRequestWithEmptyRootFormName($method) */ public function testSubmitPostOrPutRequestWithSingleChildForm($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $path = tempnam(sys_get_temp_dir(), 'sf2'); touch($path); @@ -726,10 +728,6 @@ public function testSubmitPostOrPutRequestWithSingleChildForm($method) */ public function testSubmitPostOrPutRequestWithSingleChildFormUploadedFile($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $path = tempnam(sys_get_temp_dir(), 'sf2'); touch($path); @@ -755,10 +753,6 @@ public function testSubmitPostOrPutRequestWithSingleChildFormUploadedFile($metho public function testSubmitGetRequest() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $values = array( 'author' => array( 'firstName' => 'Bernhard', @@ -787,10 +781,6 @@ public function testSubmitGetRequest() public function testSubmitGetRequestWithEmptyRootFormName() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $values = array( 'firstName' => 'Bernhard', 'lastName' => 'Schussek', @@ -832,6 +822,65 @@ public function testGetErrorsAsStringDeep() $this->assertEquals("name:\n ERROR: Error!\nfoo:\n No errors\n", $parent->getErrorsAsString()); } + // Basic cases are covered in SimpleFormTest + public function testCreateViewWithChildren() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $options = array('a' => 'Foo', 'b' => 'Bar'); + $field1 = $this->getMockForm('foo'); + $field2 = $this->getMockForm('bar'); + $view = new FormView(); + $field1View = new FormView(); + $field2View = new FormView(); + + $this->form = $this->getBuilder('form', null, null, $options) + ->setCompound(true) + ->setDataMapper($this->getDataMapper()) + ->setType($type) + ->getForm(); + $this->form->add($field1); + $this->form->add($field2); + + $test = $this; + + $assertChildViewsEqual = function (array $childViews) use ($test) { + return function (FormView $view) use ($test, $childViews) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertSame($childViews, $view->children); + }; + }; + + // First create the view + $type->expects($this->once()) + ->method('createView') + ->will($this->returnValue($view)); + + // Then build it for the form itself + $type->expects($this->once()) + ->method('buildView') + ->with($view, $this->form, $options) + ->will($this->returnCallback($assertChildViewsEqual(array()))); + + // Then add the first child form + $field1->expects($this->once()) + ->method('createView') + ->will($this->returnValue($field1View)); + + // Then the second child form + $field2->expects($this->once()) + ->method('createView') + ->will($this->returnValue($field2View)); + + // Again build the view for the form itself. This time the child views + // exist. + $type->expects($this->once()) + ->method('finishView') + ->with($view, $this->form, $options) + ->will($this->returnCallback($assertChildViewsEqual(array('foo' => $field1View, 'bar' => $field2View)))); + + $this->assertSame($view, $this->form->createView()); + } + public function testNoClickedButtonBeforeSubmission() { $this->assertNull($this->form->getClickedButton()); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php index faa10e2a7a3cf..a15d7e0d54570 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php @@ -34,14 +34,6 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\Event')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) { - $this->markTestSkipped('The "PropertyAccess" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->propertyAccessor = $this->getMock('Symfony\Component\PropertyAccess\PropertyAccessorInterface'); $this->mapper = new PropertyPathMapper($this->propertyAccessor); @@ -62,6 +54,7 @@ private function getPropertyPath($path) /** * @param FormConfigInterface $config * @param Boolean $synchronized + * @param Boolean $submitted * @return \PHPUnit_Framework_MockObject_MockObject */ private function getForm(FormConfigInterface $config, $synchronized = true, $submitted = true) diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformerTest.php index a90fa91bb05ff..840134da950df 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformerTest.php @@ -26,6 +26,67 @@ protected function setUp() \Locale::setDefault('de_AT'); } + public function transformWithRoundingProvider() + { + return array( + // towards positive infinity (1.6 -> 2, -1.6 -> -1) + array(1234.5, '1235', IntegerToLocalizedStringTransformer::ROUND_CEILING), + array(1234.4, '1235', IntegerToLocalizedStringTransformer::ROUND_CEILING), + array(-1234.5, '-1234', IntegerToLocalizedStringTransformer::ROUND_CEILING), + array(-1234.4, '-1234', IntegerToLocalizedStringTransformer::ROUND_CEILING), + // towards negative infinity (1.6 -> 1, -1.6 -> -2) + array(1234.5, '1234', IntegerToLocalizedStringTransformer::ROUND_FLOOR), + array(1234.4, '1234', IntegerToLocalizedStringTransformer::ROUND_FLOOR), + array(-1234.5, '-1235', IntegerToLocalizedStringTransformer::ROUND_FLOOR), + array(-1234.4, '-1235', IntegerToLocalizedStringTransformer::ROUND_FLOOR), + // away from zero (1.6 -> 2, -1.6 -> 2) + array(1234.5, '1235', IntegerToLocalizedStringTransformer::ROUND_UP), + array(1234.4, '1235', IntegerToLocalizedStringTransformer::ROUND_UP), + array(-1234.5, '-1235', IntegerToLocalizedStringTransformer::ROUND_UP), + array(-1234.4, '-1235', IntegerToLocalizedStringTransformer::ROUND_UP), + // towards zero (1.6 -> 1, -1.6 -> -1) + array(1234.5, '1234', IntegerToLocalizedStringTransformer::ROUND_DOWN), + array(1234.4, '1234', IntegerToLocalizedStringTransformer::ROUND_DOWN), + array(-1234.5, '-1234', IntegerToLocalizedStringTransformer::ROUND_DOWN), + array(-1234.4, '-1234', IntegerToLocalizedStringTransformer::ROUND_DOWN), + // round halves (.5) to the next even number + array(1234.6, '1235', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1234.5, '1234', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1234.4, '1234', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1233.5, '1234', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1232.5, '1232', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(-1234.6, '-1235', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(-1234.5, '-1234', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(-1234.4, '-1234', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(-1233.5, '-1234', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(-1232.5, '-1232', IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + // round halves (.5) away from zero + array(1234.6, '1235', IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array(1234.5, '1235', IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array(1234.4, '1234', IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array(-1234.6, '-1235', IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array(-1234.5, '-1235', IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array(-1234.4, '-1234', IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + // round halves (.5) towards zero + array(1234.6, '1235', IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1234.5, '1234', IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1234.4, '1234', IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(-1234.6, '-1235', IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(-1234.5, '-1234', IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(-1234.4, '-1234', IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + ); + } + + /** + * @dataProvider transformWithRoundingProvider + */ + public function testTransformWithRounding($input, $output, $roundingMode) + { + $transformer = new IntegerToLocalizedStringTransformer(null, null, $roundingMode); + + $this->assertEquals($output, $transformer->transform($input)); + } + public function testReverseTransform() { $transformer = new IntegerToLocalizedStringTransformer(); @@ -53,6 +114,67 @@ public function testReverseTransformWithGrouping() $this->assertEquals(12345, $transformer->reverseTransform('12345,912')); } + public function reverseTransformWithRoundingProvider() + { + return array( + // towards positive infinity (1.6 -> 2, -1.6 -> -1) + array('1234,5', 1235, IntegerToLocalizedStringTransformer::ROUND_CEILING), + array('1234,4', 1235, IntegerToLocalizedStringTransformer::ROUND_CEILING), + array('-1234,5', -1234, IntegerToLocalizedStringTransformer::ROUND_CEILING), + array('-1234,4', -1234, IntegerToLocalizedStringTransformer::ROUND_CEILING), + // towards negative infinity (1.6 -> 1, -1.6 -> -2) + array('1234,5', 1234, IntegerToLocalizedStringTransformer::ROUND_FLOOR), + array('1234,4', 1234, IntegerToLocalizedStringTransformer::ROUND_FLOOR), + array('-1234,5', -1235, IntegerToLocalizedStringTransformer::ROUND_FLOOR), + array('-1234,4', -1235, IntegerToLocalizedStringTransformer::ROUND_FLOOR), + // away from zero (1.6 -> 2, -1.6 -> 2) + array('1234,5', 1235, IntegerToLocalizedStringTransformer::ROUND_UP), + array('1234,4', 1235, IntegerToLocalizedStringTransformer::ROUND_UP), + array('-1234,5', -1235, IntegerToLocalizedStringTransformer::ROUND_UP), + array('-1234,4', -1235, IntegerToLocalizedStringTransformer::ROUND_UP), + // towards zero (1.6 -> 1, -1.6 -> -1) + array('1234,5', 1234, IntegerToLocalizedStringTransformer::ROUND_DOWN), + array('1234,4', 1234, IntegerToLocalizedStringTransformer::ROUND_DOWN), + array('-1234,5', -1234, IntegerToLocalizedStringTransformer::ROUND_DOWN), + array('-1234,4', -1234, IntegerToLocalizedStringTransformer::ROUND_DOWN), + // round halves (.5) to the next even number + array('1234,6', 1235, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('1234,5', 1234, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('1234,4', 1234, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('1233,5', 1234, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('1232,5', 1232, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('-1234,6', -1235, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('-1234,5', -1234, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('-1234,4', -1234, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('-1233,5', -1234, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + array('-1232,5', -1232, IntegerToLocalizedStringTransformer::ROUND_HALF_EVEN), + // round halves (.5) away from zero + array('1234,6', 1235, IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array('1234,5', 1235, IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array('1234,4', 1234, IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array('-1234,6', -1235, IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array('-1234,5', -1235, IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + array('-1234,4', -1234, IntegerToLocalizedStringTransformer::ROUND_HALF_UP), + // round halves (.5) towards zero + array('1234,6', 1235, IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array('1234,5', 1234, IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array('1234,4', 1234, IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array('-1234,6', -1235, IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array('-1234,5', -1234, IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + array('-1234,4', -1234, IntegerToLocalizedStringTransformer::ROUND_HALF_DOWN), + ); + } + + /** + * @dataProvider reverseTransformWithRoundingProvider + */ + public function testReverseTransformWithRounding($input, $output, $roundingMode) + { + $transformer = new IntegerToLocalizedStringTransformer(null, null, $roundingMode); + + $this->assertEquals($output, $transformer->reverseTransform($input)); + } + /** * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException */ diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php index c58e3f60e7475..183935e7aba79 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php @@ -82,14 +82,110 @@ public function testTransformWithPrecision() $this->assertEquals('678,92', $transformer->transform(678.916)); } - public function testTransformWithRoundingMode() + public function transformWithRoundingProvider() { - $transformer = new NumberToLocalizedStringTransformer(null, null, NumberToLocalizedStringTransformer::ROUND_DOWN); - $this->assertEquals('1234,547', $transformer->transform(1234.547), '->transform() only applies rounding mode if precision set'); + return array( + // towards positive infinity (1.6 -> 2, -1.6 -> -1) + array(0, 1234.5, '1235', NumberToLocalizedStringTransformer::ROUND_CEILING), + array(0, 1234.4, '1235', NumberToLocalizedStringTransformer::ROUND_CEILING), + array(0, -1234.5, '-1234', NumberToLocalizedStringTransformer::ROUND_CEILING), + array(0, -1234.4, '-1234', NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, 123.45, '123,5', NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, 123.44, '123,5', NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, -123.45, '-123,4', NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, -123.44, '-123,4', NumberToLocalizedStringTransformer::ROUND_CEILING), + // towards negative infinity (1.6 -> 1, -1.6 -> -2) + array(0, 1234.5, '1234', NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(0, 1234.4, '1234', NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(0, -1234.5, '-1235', NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(0, -1234.4, '-1235', NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, 123.45, '123,4', NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, 123.44, '123,4', NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, -123.45, '-123,5', NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, -123.44, '-123,5', NumberToLocalizedStringTransformer::ROUND_FLOOR), + // away from zero (1.6 -> 2, -1.6 -> 2) + array(0, 1234.5, '1235', NumberToLocalizedStringTransformer::ROUND_UP), + array(0, 1234.4, '1235', NumberToLocalizedStringTransformer::ROUND_UP), + array(0, -1234.5, '-1235', NumberToLocalizedStringTransformer::ROUND_UP), + array(0, -1234.4, '-1235', NumberToLocalizedStringTransformer::ROUND_UP), + array(1, 123.45, '123,5', NumberToLocalizedStringTransformer::ROUND_UP), + array(1, 123.44, '123,5', NumberToLocalizedStringTransformer::ROUND_UP), + array(1, -123.45, '-123,5', NumberToLocalizedStringTransformer::ROUND_UP), + array(1, -123.44, '-123,5', NumberToLocalizedStringTransformer::ROUND_UP), + // towards zero (1.6 -> 1, -1.6 -> -1) + array(0, 1234.5, '1234', NumberToLocalizedStringTransformer::ROUND_DOWN), + array(0, 1234.4, '1234', NumberToLocalizedStringTransformer::ROUND_DOWN), + array(0, -1234.5, '-1234', NumberToLocalizedStringTransformer::ROUND_DOWN), + array(0, -1234.4, '-1234', NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, 123.45, '123,4', NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, 123.44, '123,4', NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, -123.45, '-123,4', NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, -123.44, '-123,4', NumberToLocalizedStringTransformer::ROUND_DOWN), + // round halves (.5) to the next even number + array(0, 1234.6, '1235', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, 1234.5, '1234', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, 1234.4, '1234', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, 1233.5, '1234', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, 1232.5, '1232', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, -1234.6, '-1235', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, -1234.5, '-1234', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, -1234.4, '-1234', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, -1233.5, '-1234', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, -1232.5, '-1232', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, 123.46, '123,5', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, 123.45, '123,4', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, 123.44, '123,4', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, 123.35, '123,4', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, 123.25, '123,2', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, -123.46, '-123,5', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, -123.45, '-123,4', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, -123.44, '-123,4', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, -123.35, '-123,4', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, -123.25, '-123,2', NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + // round halves (.5) away from zero + array(0, 1234.6, '1235', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, 1234.5, '1235', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, 1234.4, '1234', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, -1234.6, '-1235', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, -1234.5, '-1235', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, -1234.4, '-1234', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, 123.46, '123,5', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, 123.45, '123,5', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, 123.44, '123,4', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, -123.46, '-123,5', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, -123.45, '-123,5', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, -123.44, '-123,4', NumberToLocalizedStringTransformer::ROUND_HALF_UP), + // round halves (.5) towards zero + array(0, 1234.6, '1235', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, 1234.5, '1234', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, 1234.4, '1234', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, -1234.6, '-1235', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, -1234.5, '-1234', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, -1234.4, '-1234', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, 123.46, '123,5', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, 123.45, '123,4', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, 123.44, '123,4', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, -123.46, '-123,5', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, -123.45, '-123,4', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, -123.44, '-123,4', NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + ); + } + + /** + * @dataProvider transformWithRoundingProvider + */ + public function testTransformWithRounding($precision, $input, $output, $roundingMode) + { + $transformer = new NumberToLocalizedStringTransformer($precision, null, $roundingMode); - $transformer = new NumberToLocalizedStringTransformer(2, null, NumberToLocalizedStringTransformer::ROUND_DOWN); - $this->assertEquals('1234,54', $transformer->transform(1234.547), '->transform() rounding-mode works'); + $this->assertEquals($output, $transformer->transform($input)); + } + public function testTransformDoesNotRoundIfNoPrecision() + { + $transformer = new NumberToLocalizedStringTransformer(null, null, NumberToLocalizedStringTransformer::ROUND_DOWN); + + $this->assertEquals('1234,547', $transformer->transform(1234.547)); } /** @@ -139,6 +235,112 @@ public function testReverseTransformWithGroupingButWithoutGroupSeparator() $this->assertEquals(12345.912, $transformer->reverseTransform('12345,912')); } + public function reverseTransformWithRoundingProvider() + { + return array( + // towards positive infinity (1.6 -> 2, -1.6 -> -1) + array(0, '1234,5', 1235, NumberToLocalizedStringTransformer::ROUND_CEILING), + array(0, '1234,4', 1235, NumberToLocalizedStringTransformer::ROUND_CEILING), + array(0, '-1234,5', -1234, NumberToLocalizedStringTransformer::ROUND_CEILING), + array(0, '-1234,4', -1234, NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, '123,45', 123.5, NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, '123,44', 123.5, NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, '-123,45', -123.4, NumberToLocalizedStringTransformer::ROUND_CEILING), + array(1, '-123,44', -123.4, NumberToLocalizedStringTransformer::ROUND_CEILING), + // towards negative infinity (1.6 -> 1, -1.6 -> -2) + array(0, '1234,5', 1234, NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(0, '1234,4', 1234, NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(0, '-1234,5', -1235, NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(0, '-1234,4', -1235, NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, '123,45', 123.4, NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, '123,44', 123.4, NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, '-123,45', -123.5, NumberToLocalizedStringTransformer::ROUND_FLOOR), + array(1, '-123,44', -123.5, NumberToLocalizedStringTransformer::ROUND_FLOOR), + // away from zero (1.6 -> 2, -1.6 -> 2) + array(0, '1234,5', 1235, NumberToLocalizedStringTransformer::ROUND_UP), + array(0, '1234,4', 1235, NumberToLocalizedStringTransformer::ROUND_UP), + array(0, '-1234,5', -1235, NumberToLocalizedStringTransformer::ROUND_UP), + array(0, '-1234,4', -1235, NumberToLocalizedStringTransformer::ROUND_UP), + array(1, '123,45', 123.5, NumberToLocalizedStringTransformer::ROUND_UP), + array(1, '123,44', 123.5, NumberToLocalizedStringTransformer::ROUND_UP), + array(1, '-123,45', -123.5, NumberToLocalizedStringTransformer::ROUND_UP), + array(1, '-123,44', -123.5, NumberToLocalizedStringTransformer::ROUND_UP), + // towards zero (1.6 -> 1, -1.6 -> -1) + array(0, '1234,5', 1234, NumberToLocalizedStringTransformer::ROUND_DOWN), + array(0, '1234,4', 1234, NumberToLocalizedStringTransformer::ROUND_DOWN), + array(0, '-1234,5', -1234, NumberToLocalizedStringTransformer::ROUND_DOWN), + array(0, '-1234,4', -1234, NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, '123,45', 123.4, NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, '123,44', 123.4, NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, '-123,45', -123.4, NumberToLocalizedStringTransformer::ROUND_DOWN), + array(1, '-123,44', -123.4, NumberToLocalizedStringTransformer::ROUND_DOWN), + // round halves (.5) to the next even number + array(0, '1234,6', 1235, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '1234,5', 1234, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '1234,4', 1234, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '1233,5', 1234, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '1232,5', 1232, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '-1234,6', -1235, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '-1234,5', -1234, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '-1234,4', -1234, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '-1233,5', -1234, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(0, '-1232,5', -1232, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '123,46', 123.5, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '123,45', 123.4, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '123,44', 123.4, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '123,35', 123.4, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '123,25', 123.2, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '-123,46', -123.5, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '-123,45', -123.4, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '-123,44', -123.4, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '-123,35', -123.4, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + array(1, '-123,25', -123.2, NumberToLocalizedStringTransformer::ROUND_HALF_EVEN), + // round halves (.5) away from zero + array(0, '1234,6', 1235, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, '1234,5', 1235, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, '1234,4', 1234, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, '-1234,6', -1235, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, '-1234,5', -1235, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(0, '-1234,4', -1234, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, '123,46', 123.5, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, '123,45', 123.5, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, '123,44', 123.4, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, '-123,46', -123.5, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, '-123,45', -123.5, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + array(1, '-123,44', -123.4, NumberToLocalizedStringTransformer::ROUND_HALF_UP), + // round halves (.5) towards zero + array(0, '1234,6', 1235, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, '1234,5', 1234, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, '1234,4', 1234, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, '-1234,6', -1235, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, '-1234,5', -1234, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(0, '-1234,4', -1234, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, '123,46', 123.5, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, '123,45', 123.4, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, '123,44', 123.4, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, '-123,46', -123.5, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, '-123,45', -123.4, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + array(1, '-123,44', -123.4, NumberToLocalizedStringTransformer::ROUND_HALF_DOWN), + ); + } + + /** + * @dataProvider reverseTransformWithRoundingProvider + */ + public function testReverseTransformWithRounding($precision, $input, $output, $roundingMode) + { + $transformer = new NumberToLocalizedStringTransformer($precision, null, $roundingMode); + + $this->assertEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformDoesNotRoundIfNoPrecision() + { + $transformer = new NumberToLocalizedStringTransformer(null, null, NumberToLocalizedStringTransformer::ROUND_DOWN); + + $this->assertEquals(1234.547, $transformer->reverseTransform('1234,547')); + } + public function testDecimalSeparatorMayBeDotIfGroupingSeparatorIsNotDot() { \Locale::setDefault('fr'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php index a5d5c78a81858..426293395c9f3 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixRadioInputListenerTest.php @@ -21,10 +21,6 @@ class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - parent::setUp(); $this->choiceList = new SimpleChoiceList(array('' => 'Empty', 0 => 'A', 1 => 'B')); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixUrlProtocolListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixUrlProtocolListenerTest.php index 2b84e4fd82f5f..475681a053613 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixUrlProtocolListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/FixUrlProtocolListenerTest.php @@ -16,13 +16,6 @@ class FixUrlProtocolListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - } - public function testFixHttpUrl() { $data = "www.symfony.com"; diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/MergeCollectionListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/MergeCollectionListenerTest.php index dbd28c6b55a7d..2d7ecfec75571 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/MergeCollectionListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/MergeCollectionListenerTest.php @@ -22,10 +22,6 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); $this->form = $this->getForm('axes'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php index 1367b3ef02f46..67a71e462a169 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/ResizeFormListenerTest.php @@ -23,10 +23,6 @@ class ResizeFormListenerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); $this->form = $this->getBuilder() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/TrimListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/TrimListenerTest.php index 4e36893380bbd..6e81ed404cd41 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/TrimListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/EventListener/TrimListenerTest.php @@ -16,13 +16,6 @@ class TrimListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - } - public function testTrim() { $data = " Foo! "; diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CollectionTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CollectionTypeTest.php index be3ad9db5ca84..58bbfad7829da 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/CollectionTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/CollectionTypeTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\Form; +use Symfony\Component\Form\Tests\Fixtures\Author; +use Symfony\Component\Form\Tests\Fixtures\AuthorType; class CollectionTypeTest extends \Symfony\Component\Form\Test\TypeTestCase { @@ -88,6 +90,78 @@ public function testResizedDownIfSubmittedWithMissingDataAndAllowDelete() $this->assertEquals(array('foo@foo.com'), $form->getData()); } + public function testResizedDownIfSubmittedWithEmptyDataAndDeleteEmpty() + { + $form = $this->factory->create('collection', null, array( + 'type' => 'text', + 'allow_delete' => true, + 'delete_empty' => true, + )); + + $form->setData(array('foo@foo.com', 'bar@bar.com')); + $form->submit(array('foo@foo.com', '')); + + $this->assertTrue($form->has('0')); + $this->assertFalse($form->has('1')); + $this->assertEquals('foo@foo.com', $form[0]->getData()); + $this->assertEquals(array('foo@foo.com'), $form->getData()); + } + + public function testDontAddEmptyDataIfDeleteEmpty() + { + $form = $this->factory->create('collection', null, array( + 'type' => 'text', + 'allow_add' => true, + 'delete_empty' => true, + )); + + $form->setData(array('foo@foo.com')); + $form->submit(array('foo@foo.com', '')); + + $this->assertTrue($form->has('0')); + $this->assertFalse($form->has('1')); + $this->assertEquals('foo@foo.com', $form[0]->getData()); + $this->assertEquals(array('foo@foo.com'), $form->getData()); + } + + public function testNoDeleteEmptyIfDeleteNotAllowed() + { + $form = $this->factory->create('collection', null, array( + 'type' => 'text', + 'allow_delete' => false, + 'delete_empty' => true, + )); + + $form->setData(array('foo@foo.com')); + $form->submit(array('')); + + $this->assertTrue($form->has('0')); + $this->assertEquals('', $form[0]->getData()); + } + + public function testResizedDownIfSubmittedWithCompoundEmptyDataAndDeleteEmpty() + { + $form = $this->factory->create('collection', null, array( + 'type' => new AuthorType(), + // If the field is not required, no new Author will be created if the + // form is completely empty + 'options' => array('required' => false), + 'allow_add' => true, + 'delete_empty' => true, + )); + + $form->setData(array(new Author('first', 'last'))); + $form->submit(array( + array('firstName' => 's_first', 'lastName' => 's_last'), + array('firstName' => '', 'lastName' => ''), + )); + + $this->assertTrue($form->has('0')); + $this->assertFalse($form->has('1')); + $this->assertEquals(new Author('s_first', 's_last'), $form[0]->getData()); + $this->assertEquals(array(new Author('s_first', 's_last')), $form->getData()); + } + public function testNotResizedIfSubmittedWithExtraData() { $form = $this->factory->create('collection', null, array( diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php index 63556eb5a63fd..0227af4f23081 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php @@ -44,6 +44,25 @@ public function testSubmitEmpty() $this->assertNull($form->getData()); } + public function testSubmitMultiple() + { + $form = $this->factory->createBuilder('file', null, array( + 'multiple' => true + ))->getForm(); + + $data = array( + $this->createUploadedFileMock('abcdef', 'first.jpg', true), + $this->createUploadedFileMock('zyxwvu', 'second.jpg', true), + ); + + $form->submit($data); + $this->assertSame($data, $form->getData()); + + $view = $form->createView(); + $this->assertSame('file[]', $view->vars['full_name']); + $this->assertArrayHasKey('multiple', $view->vars['attr']); + } + public function testDontPassValueToView() { $form = $this->factory->create('file'); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php index e670e57389f05..4568807c774be 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php @@ -534,6 +534,21 @@ public function testViewNotValidSubmitted() $this->assertFalse($view->vars['valid']); } + public function testViewSubmittedNotSubmitted() + { + $form = $this->factory->create('form'); + $view = $form->createView(); + $this->assertFalse($view->vars['submitted']); + } + + public function testViewSubmittedSubmitted() + { + $form = $this->factory->create('form'); + $form->submit(array()); + $view = $form->createView(); + $this->assertTrue($view->vars['submitted']); + } + public function testDataOptionSupersedesSetDataCalls() { $form = $this->factory->create('form', null, array( diff --git a/src/Symfony/Component/Form/Tests/Extension/Csrf/CsrfProvider/SessionCsrfProviderTest.php b/src/Symfony/Component/Form/Tests/Extension/Csrf/CsrfProvider/SessionCsrfProviderTest.php index 1dcc6b4c63b7b..99e87158b5b02 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Csrf/CsrfProvider/SessionCsrfProviderTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Csrf/CsrfProvider/SessionCsrfProviderTest.php @@ -20,10 +20,6 @@ class SessionCsrfProviderTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\HttpFoundation\Session\Session')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $this->session = $this->getMock( 'Symfony\Component\HttpFoundation\Session\Session', array(), diff --git a/src/Symfony/Component/Form/Tests/Extension/Csrf/EventListener/CsrfValidationListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Csrf/EventListener/CsrfValidationListenerTest.php index 0bcfe74e99055..ef013ca82be9c 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Csrf/EventListener/CsrfValidationListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Csrf/EventListener/CsrfValidationListenerTest.php @@ -19,17 +19,14 @@ class CsrfValidationListenerTest extends \PHPUnit_Framework_TestCase { protected $dispatcher; protected $factory; - protected $csrfProvider; + protected $tokenManager; + protected $form; protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); - $this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'); + $this->tokenManager = $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface'); $this->form = $this->getBuilder('post') ->setDataMapper($this->getDataMapper()) ->getForm(); @@ -39,7 +36,7 @@ protected function tearDown() { $this->dispatcher = null; $this->factory = null; - $this->csrfProvider = null; + $this->tokenManager = null; $this->form = null; } @@ -69,7 +66,7 @@ public function testStringFormData() $data = "XP4HUzmHPi"; $event = new FormEvent($this->form, $data); - $validation = new CsrfValidationListener('csrf', $this->csrfProvider, 'unknown', 'Invalid.'); + $validation = new CsrfValidationListener('csrf', $this->tokenManager, 'unknown', 'Invalid.'); $validation->preSubmit($event); // Validate accordingly diff --git a/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php index 91392bc441dd3..53e3d3a6b3b5e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Form\Extension\Csrf\CsrfExtension; +use Symfony\Component\Security\Csrf\CsrfToken; class FormTypeCsrfExtensionTest_ChildType extends AbstractType { @@ -37,7 +38,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $csrfProvider; + protected $tokenManager; /** * @var \PHPUnit_Framework_MockObject_MockObject @@ -46,7 +47,7 @@ class FormTypeCsrfExtensionTest extends TypeTestCase protected function setUp() { - $this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'); + $this->tokenManager = $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface'); $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); parent::setUp(); @@ -54,7 +55,7 @@ protected function setUp() protected function tearDown() { - $this->csrfProvider = null; + $this->tokenManager = null; $this->translator = null; parent::tearDown(); @@ -63,7 +64,7 @@ protected function tearDown() protected function getExtensions() { return array_merge(parent::getExtensions(), array( - new CsrfExtension($this->csrfProvider, $this->translator), + new CsrfExtension($this->tokenManager, $this->translator), )); } @@ -123,16 +124,16 @@ public function testCsrfProtectionCanBeDisabled() public function testGenerateCsrfToken() { - $this->csrfProvider->expects($this->once()) - ->method('generateCsrfToken') - ->with('%INTENTION%') - ->will($this->returnValue('token')); + $this->tokenManager->expects($this->once()) + ->method('getToken') + ->with('TOKEN_ID') + ->will($this->returnValue(new CsrfToken('TOKEN_ID', 'token'))); $view = $this->factory ->create('form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%', + 'csrf_token_manager' => $this->tokenManager, + 'csrf_token_id' => 'TOKEN_ID', 'compound' => true, )) ->createView(); @@ -142,15 +143,15 @@ public function testGenerateCsrfToken() public function testGenerateCsrfTokenUsesFormNameAsIntentionByDefault() { - $this->csrfProvider->expects($this->once()) - ->method('generateCsrfToken') + $this->tokenManager->expects($this->once()) + ->method('getToken') ->with('FORM_NAME') ->will($this->returnValue('token')); $view = $this->factory ->createNamed('FORM_NAME', 'form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, + 'csrf_token_manager' => $this->tokenManager, 'compound' => true, )) ->createView(); @@ -160,15 +161,15 @@ public function testGenerateCsrfTokenUsesFormNameAsIntentionByDefault() public function testGenerateCsrfTokenUsesTypeClassAsIntentionIfEmptyFormName() { - $this->csrfProvider->expects($this->once()) - ->method('generateCsrfToken') + $this->tokenManager->expects($this->once()) + ->method('getToken') ->with('Symfony\Component\Form\Extension\Core\Type\FormType') ->will($this->returnValue('token')); $view = $this->factory ->createNamed('', 'form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, + 'csrf_token_manager' => $this->tokenManager, 'compound' => true, )) ->createView(); @@ -189,16 +190,16 @@ public function provideBoolean() */ public function testValidateTokenOnSubmitIfRootAndCompound($valid) { - $this->csrfProvider->expects($this->once()) - ->method('isCsrfTokenValid') - ->with('%INTENTION%', 'token') + $this->tokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('TOKEN_ID', 'token')) ->will($this->returnValue($valid)); $form = $this->factory ->createBuilder('form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%', + 'csrf_token_manager' => $this->tokenManager, + 'csrf_token_id' => 'TOKEN_ID', 'compound' => true, )) ->add('child', 'text') @@ -221,15 +222,15 @@ public function testValidateTokenOnSubmitIfRootAndCompound($valid) */ public function testValidateTokenOnSubmitIfRootAndCompoundUsesFormNameAsIntentionByDefault($valid) { - $this->csrfProvider->expects($this->once()) - ->method('isCsrfTokenValid') - ->with('FORM_NAME', 'token') + $this->tokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('FORM_NAME', 'token')) ->will($this->returnValue($valid)); $form = $this->factory ->createNamedBuilder('FORM_NAME', 'form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, + 'csrf_token_manager' => $this->tokenManager, 'compound' => true, )) ->add('child', 'text') @@ -252,15 +253,15 @@ public function testValidateTokenOnSubmitIfRootAndCompoundUsesFormNameAsIntentio */ public function testValidateTokenOnSubmitIfRootAndCompoundUsesTypeClassAsIntentionIfEmptyFormName($valid) { - $this->csrfProvider->expects($this->once()) - ->method('isCsrfTokenValid') - ->with('Symfony\Component\Form\Extension\Core\Type\FormType', 'token') + $this->tokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('Symfony\Component\Form\Extension\Core\Type\FormType', 'token')) ->will($this->returnValue($valid)); $form = $this->factory ->createNamedBuilder('', 'form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, + 'csrf_token_manager' => $this->tokenManager, 'compound' => true, )) ->add('child', 'text') @@ -278,47 +279,16 @@ public function testValidateTokenOnSubmitIfRootAndCompoundUsesTypeClassAsIntenti $this->assertSame($valid, $form->isValid()); } - /** - * @dataProvider provideBoolean - */ - public function testValidateTokenOnBindIfRootAndCompoundUsesTypeClassAsIntentionIfEmptyFormName($valid) - { - $this->csrfProvider->expects($this->once()) - ->method('isCsrfTokenValid') - ->with('Symfony\Component\Form\Extension\Core\Type\FormType', 'token') - ->will($this->returnValue($valid)); - - $form = $this->factory - ->createNamedBuilder('', 'form', null, array( - 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, - 'compound' => true, - )) - ->add('child', 'text') - ->getForm(); - - $form->bind(array( - 'child' => 'foobar', - 'csrf' => 'token', - )); - - // Remove token from data - $this->assertSame(array('child' => 'foobar'), $form->getData()); - - // Validate accordingly - $this->assertSame($valid, $form->isValid()); - } - public function testFailIfRootAndCompoundAndTokenMissing() { - $this->csrfProvider->expects($this->never()) - ->method('isCsrfTokenValid'); + $this->tokenManager->expects($this->never()) + ->method('isTokenValid'); $form = $this->factory ->createBuilder('form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%', + 'csrf_token_manager' => $this->tokenManager, + 'csrf_token_id' => 'TOKEN_ID', 'compound' => true, )) ->add('child', 'text') @@ -338,16 +308,16 @@ public function testFailIfRootAndCompoundAndTokenMissing() public function testDontValidateTokenIfCompoundButNoRoot() { - $this->csrfProvider->expects($this->never()) - ->method('isCsrfTokenValid'); + $this->tokenManager->expects($this->never()) + ->method('isTokenValid'); $form = $this->factory ->createNamedBuilder('root', 'form') ->add($this->factory ->createNamedBuilder('form', 'form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%', + 'csrf_token_manager' => $this->tokenManager, + 'csrf_token_id' => 'TOKEN_ID', 'compound' => true, )) ) @@ -362,14 +332,14 @@ public function testDontValidateTokenIfCompoundButNoRoot() public function testDontValidateTokenIfRootButNotCompound() { - $this->csrfProvider->expects($this->never()) - ->method('isCsrfTokenValid'); + $this->tokenManager->expects($this->never()) + ->method('isTokenValid'); $form = $this->factory ->create('form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, - 'intention' => '%INTENTION%', + 'csrf_token_manager' => $this->tokenManager, + 'csrf_token_id' => 'TOKEN_ID', 'compound' => false, )); @@ -398,9 +368,9 @@ public function testNoCsrfProtectionOnPrototype() public function testsTranslateCustomErrorMessage() { - $this->csrfProvider->expects($this->once()) - ->method('isCsrfTokenValid') - ->with('%INTENTION%', 'token') + $this->tokenManager->expects($this->once()) + ->method('isTokenValid') + ->with(new CsrfToken('TOKEN_ID', 'token')) ->will($this->returnValue(false)); $this->translator->expects($this->once()) @@ -411,9 +381,9 @@ public function testsTranslateCustomErrorMessage() $form = $this->factory ->createBuilder('form', null, array( 'csrf_field_name' => 'csrf', - 'csrf_provider' => $this->csrfProvider, + 'csrf_token_manager' => $this->tokenManager, 'csrf_message' => 'Foobar', - 'intention' => '%INTENTION%', + 'csrf_token_id' => 'TOKEN_ID', 'compound' => true, )) ->getForm(); diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php new file mode 100644 index 0000000000000..9f5991c1052c4 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/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\DataCollector; + +use Symfony\Component\Form\Extension\DataCollector\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; + + public function setUp() + { + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\DataCollector\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\DataCollector\Type\DataCollectorTypeExtension', array_shift($typeExtensions)); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php new file mode 100644 index 0000000000000..885b4b68d1c43 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php @@ -0,0 +1,534 @@ + + * + * 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\DataCollector; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormView; + +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\DataCollector\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->collectConfiguration($this->form); + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->collectSubmittedData($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $childFormData = array( + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + 'children' => array(), + ); + + $formData = array( + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + 'children' => array( + 'child' => $childFormData, + ), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + spl_object_hash($this->childForm) => $childFormData, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + 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('extractConfiguration') + ->with($form2) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataCollector->collectConfiguration($form1); + $this->dataCollector->collectConfiguration($form2); + $this->dataCollector->buildPreliminaryFormTree($form1); + + $form1Data = array( + 'config' => 'foo', + 'children' => array(), + ); + + $this->assertSame(array( + 'forms' => array( + 'form1' => $form1Data, + ), + 'forms_by_hash' => array( + spl_object_hash($form1) => $form1Data, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->buildPreliminaryFormTree($form2); + + $form2Data = array( + 'config' => 'bar', + 'children' => array(), + ); + + $this->assertSame(array( + 'forms' => array( + 'form1' => $form1Data, + 'form2' => $form2Data, + ), + 'forms_by_hash' => array( + spl_object_hash($form1) => $form1Data, + spl_object_hash($form2) => $form2Data, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + 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->dataCollector->collectConfiguration($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $formData = array( + 'config' => 'foo', + 'children' => array(), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $formData = array( + 'config' => 'foo', + 'default_data' => 'foo', + 'children' => array(), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildPreliminaryFormTreeWithoutCollectingAnyData() + { + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $formData = array( + 'children' => array(), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + 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->collectConfiguration($this->form); + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->collectSubmittedData($this->form); + $this->dataCollector->collectViewVariables($this->view); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $childFormData = array( + 'view_vars' => 'bar', + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + 'children' => array(), + ); + + $formData = array( + 'view_vars' => 'foo', + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + 'children' => array( + 'child' => $childFormData, + ), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + spl_object_hash($this->childForm) => $childFormData, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testFinalFormReliesOnFormViewStructure() + { + $this->form->add($child1 = $this->createForm('first')); + $this->form->add($child2 = $this->createForm('second')); + + $this->view->children['second'] = $this->childView; + + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $child1Data = array( + 'children' => array(), + ); + + $child2Data = array( + 'children' => array(), + ); + + $formData = array( + 'children' => array( + 'first' => $child1Data, + 'second' => $child2Data, + ), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + spl_object_hash($child1) => $child1Data, + spl_object_hash($child2) => $child2Data, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $formData = array( + 'children' => array( + // "first" not present in FormView + 'second' => $child2Data, + ), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + spl_object_hash($child1) => $child1Data, + spl_object_hash($child2) => $child2Data, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + 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('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectConfiguration($this->childForm); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $childFormData = array( + // no "config" key + 'children' => array(), + ); + + $formData = array( + 'config' => 'foo', + 'children' => array( + 'child' => $childFormData, + ), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + // no child entry + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + 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('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectConfiguration($this->childForm); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $childFormData = array( + 'config' => 'bar', + 'children' => array(), + ); + + $formData = array( + 'config' => 'foo', + 'children' => array( + 'child' => $childFormData, + ), + ); + + $this->assertSame(array( + 'forms' => array( + 'name' => $formData, + ), + 'forms_by_hash' => array( + spl_object_hash($this->form) => $formData, + spl_object_hash($this->childForm) => $childFormData, + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testCollectSubmittedDataCountsErrors() + { + $form1 = $this->createForm('form1'); + $childForm1 = $this->createForm('child1'); + $form2 = $this->createForm('form2'); + + $form1->add($childForm1); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractSubmittedData') + ->with($form1) + ->will($this->returnValue(array('errors' => array('foo')))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractSubmittedData') + ->with($childForm1) + ->will($this->returnValue(array('errors' => array('bar', 'bam')))); + $this->dataExtractor->expects($this->at(2)) + ->method('extractSubmittedData') + ->with($form2) + ->will($this->returnValue(array('errors' => array('baz')))); + + $this->dataCollector->collectSubmittedData($form1); + + $data = $this->dataCollector->getData(); + $this->assertSame(3, $data['nb_errors']); + + $this->dataCollector->collectSubmittedData($form2); + + $data = $this->dataCollector->getData(); + $this->assertSame(4, $data['nb_errors']); + + } + + 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/DataCollector/FormDataExtractorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php new file mode 100644 index 0000000000000..37b5b7d86cfbb --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php @@ -0,0 +1,427 @@ + + * + * 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\DataCollector; + +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\DataCollector\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\HttpKernel\DataCollector\Util\ValueExporter; + +class FormDataExtractorTest_SimpleValueExporter extends ValueExporter +{ + /** + * {@inheritdoc} + */ + public function exportValue($value) + { + 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' => null, 'cause' => null), + ), + '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), 'cause' => null), + ), + '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' => null, 'cause' => 'object(Exception)'), + ), + '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/DataCollector/Type/DataCollectorTypeExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php new file mode 100644 index 0000000000000..cb77456984526 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector\Type; + +use Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension; + +class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var DataCollectorTypeExtension + */ + private $extension; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataCollector; + + public function setUp() + { + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\DataCollector\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\DataCollector\EventListener\DataCollectorListener')); + + $this->extension->buildForm($builder, array()); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php index 2ff072b2eb75b..a60145f2dbda7 100644 --- a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/EventListener/BindRequestListenerTest.php @@ -86,10 +86,6 @@ public function requestMethodProvider() */ public function testSubmitRequest($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $values = array('author' => $this->values); $files = array('author' => $this->filesNested); $request = new Request(array(), $values, array(), array(), $files, array( @@ -115,10 +111,6 @@ public function testSubmitRequest($method) */ public function testSubmitRequestWithEmptyName($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $request = new Request(array(), $this->values, array(), array(), $this->filesPlain, array( 'REQUEST_METHOD' => $method, )); @@ -142,10 +134,6 @@ public function testSubmitRequestWithEmptyName($method) */ public function testSubmitEmptyRequestToCompoundForm($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $request = new Request(array(), array(), array(), array(), array(), array( 'REQUEST_METHOD' => $method, )); @@ -169,10 +157,6 @@ public function testSubmitEmptyRequestToCompoundForm($method) */ public function testSubmitEmptyRequestToSimpleForm($method) { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $request = new Request(array(), array(), array(), array(), array(), array( 'REQUEST_METHOD' => $method, )); @@ -192,10 +176,6 @@ public function testSubmitEmptyRequestToSimpleForm($method) public function testSubmitGetRequest() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $values = array('author' => $this->values); $request = new Request($values, array(), array(), array(), array(), array( 'REQUEST_METHOD' => 'GET', @@ -217,10 +197,6 @@ public function testSubmitGetRequest() public function testSubmitGetRequestWithEmptyName() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $request = new Request($this->values, array(), array(), array(), array(), array( 'REQUEST_METHOD' => 'GET', )); @@ -241,10 +217,6 @@ public function testSubmitGetRequestWithEmptyName() public function testSubmitEmptyGetRequestToCompoundForm() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $request = new Request(array(), array(), array(), array(), array(), array( 'REQUEST_METHOD' => 'GET', )); @@ -264,10 +236,6 @@ public function testSubmitEmptyGetRequestToCompoundForm() public function testSubmitEmptyGetRequestToSimpleForm() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $request = new Request(array(), array(), array(), array(), array(), array( 'REQUEST_METHOD' => 'GET', )); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php index dc504172a0a61..4d9c26b81ce9f 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -49,10 +49,6 @@ class FormValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\Event')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); $this->serverParams = $this->getMock( diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php index 528f94633b28c..8d8f9235d1ce6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; class ValidationListenerTest extends \PHPUnit_Framework_TestCase { @@ -53,10 +54,6 @@ class ValidationListenerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\Event')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); $this->validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface'); @@ -142,4 +139,23 @@ public function testValidateIgnoresNonRoot() $this->listener->validateForm(new FormEvent($form, null)); } + + public function testValidateWithEmptyViolationList() + { + $form = $this->getMockForm(); + $form->expects($this->once()) + ->method('isRoot') + ->will($this->returnValue(true)); + + $this->validator + ->expects($this->once()) + ->method('validate') + ->will($this->returnValue(new ConstraintViolationList())); + + $this->violationMapper + ->expects($this->never()) + ->method('mapViolation'); + + $this->listener->validateForm(new FormEvent($form, null)); + } } diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php index 0eef62d29c0e1..298306b57d1ba 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/FormTypeValidatorExtensionTest.php @@ -11,19 +11,26 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\Type; +use Symfony\Component\Validator\ConstraintViolationList; + class FormTypeValidatorExtensionTest extends BaseValidatorExtensionTest { public function testSubmitValidatesData() { - $builder = $this->factory->createBuilder('form', null, array( - 'validation_groups' => 'group', - )); + $builder = $this->factory->createBuilder( + 'form', + null, + array( + 'validation_groups' => 'group', + ) + ); $builder->add('firstName', 'form'); $form = $builder->getForm(); $this->validator->expects($this->once()) ->method('validate') - ->with($this->equalTo($form)); + ->with($this->equalTo($form)) + ->will($this->returnValue(new ConstraintViolationList())); // specific data is irrelevant $form->submit(array()); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/TypeTestCase.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/TypeTestCase.php index d94d896a1c379..f197b19857315 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/TypeTestCase.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/TypeTestCase.php @@ -20,10 +20,6 @@ abstract class TypeTestCase extends BaseTypeTestCase protected function setUp() { - if (!class_exists('Symfony\Component\Validator\Constraint')) { - $this->markTestSkipped('The "Validator" component is not available'); - } - $this->validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface'); $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface'); $this->validator->expects($this->once())->method('getMetadataFactory')->will($this->returnValue($metadataFactory)); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php index c802ea7e8055d..3c9886d7cf233 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\FormError; use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationInterface; /** * @author Bernhard Schussek @@ -60,10 +61,6 @@ class ViolationMapperTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\Event')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->mapper = new ViolationMapper(); $this->message = 'Message'; @@ -113,9 +110,9 @@ protected function getConstraintViolation($propertyPath) /** * @return FormError */ - protected function getFormError() + protected function getFormError(ConstraintViolationInterface $violation) { - return new FormError($this->message, $this->messageTemplate, $this->params); + return new FormError($this->message, $this->messageTemplate, $this->params, null, $violation); } public function testMapToFormInheritingParentDataIfDataDoesNotMatch() @@ -131,7 +128,7 @@ public function testMapToFormInheritingParentDataIfDataDoesNotMatch() $this->mapper->mapViolation($violation, $parent); $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $child->getErrors(), $child->getName().' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $child->getName().' should have an error, but has none'); $this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one'); } @@ -158,7 +155,7 @@ public function testFollowDotRules() $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); $this->assertCount(0, $child->getErrors(), $child->getName().' should not have an error, but has one'); $this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $grandGrandChild->getErrors(), $grandGrandChild->getName().' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $grandGrandChild->getErrors(), $grandGrandChild->getName().' should have an error, but has none'); } public function testAbortMappingIfNotSynchronized() @@ -749,17 +746,17 @@ public function testDefaultErrorMapping($target, $childName, $childPath, $grandC $this->mapper->mapViolation($violation, $parent); if (self::LEVEL_0 === $target) { - $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } elseif (self::LEVEL_1 === $target) { $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } else { $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); } } @@ -1221,17 +1218,17 @@ public function testCustomDataErrorMapping($target, $mapFrom, $mapTo, $childName } if (self::LEVEL_0 === $target) { - $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } elseif (self::LEVEL_1 === $target) { $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } else { $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); } } @@ -1403,16 +1400,16 @@ public function testCustomFormErrorMapping($target, $mapFrom, $mapTo, $errorName if (self::LEVEL_0 === $target) { $this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } elseif (self::LEVEL_1 === $target) { $this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one'); $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } elseif (self::LEVEL_1B === $target) { - $this->assertEquals(array($this->getFormError()), $errorChild->getErrors(), $errorName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $errorChild->getErrors(), $errorName.' should have an error, but has none'); $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); @@ -1420,7 +1417,7 @@ public function testCustomFormErrorMapping($target, $mapFrom, $mapTo, $errorName $this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one'); $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); } } @@ -1465,17 +1462,17 @@ public function testErrorMappingForFormInheritingParentData($target, $childName, $this->mapper->mapViolation($violation, $parent); if (self::LEVEL_0 === $target) { - $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $parent->getErrors(), $parent->getName().' should have an error, but has none'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } elseif (self::LEVEL_1 === $target) { $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $child->getErrors(), $childName.' should have an error, but has none'); $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one'); } else { $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one'); $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one'); - $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); + $this->assertEquals(array($this->getFormError($violation)), $grandChild->getErrors(), $grandChildName.' should have an error, but has none'); } } } diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Author.php b/src/Symfony/Component/Form/Tests/Fixtures/Author.php index 1120489469e56..39765d9df3232 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Author.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/Author.php @@ -21,6 +21,12 @@ class Author private $privateProperty; + public function __construct($firstName = null, $lastName = null) + { + $this->firstName = $firstName; + $this->lastName = $lastName; + } + public function setLastName($lastName) { $this->lastName = $lastName; diff --git a/src/Symfony/Component/Form/Tests/FormBuilderTest.php b/src/Symfony/Component/Form/Tests/FormBuilderTest.php index e076c97e78691..be85ff322a583 100644 --- a/src/Symfony/Component/Form/Tests/FormBuilderTest.php +++ b/src/Symfony/Component/Form/Tests/FormBuilderTest.php @@ -23,10 +23,6 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); $this->builder = new FormBuilder('name', null, $this->dispatcher, $this->factory); diff --git a/src/Symfony/Component/Form/Tests/FormFactoryBuilderTest.php b/src/Symfony/Component/Form/Tests/FormFactoryBuilderTest.php index a1292dbe72287..2c3ab000ffe30 100644 --- a/src/Symfony/Component/Form/Tests/FormFactoryBuilderTest.php +++ b/src/Symfony/Component/Form/Tests/FormFactoryBuilderTest.php @@ -27,12 +27,12 @@ protected function setUp() $this->registry->setAccessible(true); $this->guesser = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); - $this->type = new FooType; + $this->type = new FooType(); } public function testAddType() { - $factoryBuilder = new FormFactoryBuilder; + $factoryBuilder = new FormFactoryBuilder(); $factoryBuilder->addType($this->type); $factory = $factoryBuilder->getFormFactory(); @@ -46,7 +46,7 @@ public function testAddType() public function testAddTypeGuesser() { - $factoryBuilder = new FormFactoryBuilder; + $factoryBuilder = new FormFactoryBuilder(); $factoryBuilder->addTypeGuesser($this->guesser); $factory = $factoryBuilder->getFormFactory(); diff --git a/src/Symfony/Component/Form/Tests/FormFactoryTest.php b/src/Symfony/Component/Form/Tests/FormFactoryTest.php index ea872b01edf28..cdd06e1594030 100644 --- a/src/Symfony/Component/Form/Tests/FormFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/FormFactoryTest.php @@ -46,6 +46,11 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase */ private $resolvedTypeFactory; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $builder; + /** * @var FormFactory */ @@ -53,14 +58,11 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->resolvedTypeFactory = $this->getMock('Symfony\Component\Form\ResolvedFormTypeFactoryInterface'); $this->guesser1 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); $this->guesser2 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); $this->registry = $this->getMock('Symfony\Component\Form\FormRegistryInterface'); + $this->builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); $this->factory = new FormFactory($this->registry, $this->resolvedTypeFactory); $this->registry->expects($this->any()) @@ -74,6 +76,7 @@ protected function setUp() public function testCreateNamedBuilderWithTypeName() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); $this->registry->expects($this->once()) @@ -84,14 +87,23 @@ public function testCreateNamedBuilderWithTypeName() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', null, $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', 'type', null, $options)); } public function testCreateNamedBuilderWithTypeInstance() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $type = new FooType(); $resolvedType = $this->getMockResolvedType(); @@ -103,14 +115,23 @@ public function testCreateNamedBuilderWithTypeInstance() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options)); + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $type, null, $options)); } public function testCreateNamedBuilderWithTypeInstanceWithParentType() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $type = new FooSubType(); $resolvedType = $this->getMockResolvedType(); $parentResolvedType = $this->getMockResolvedType(); @@ -128,14 +149,23 @@ public function testCreateNamedBuilderWithTypeInstanceWithParentType() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); + + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options)); + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $type, null, $options)); } public function testCreateNamedBuilderWithTypeInstanceWithParentTypeInstance() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $type = new FooSubTypeWithParentInstance(); $resolvedType = $this->getMockResolvedType(); $parentResolvedType = $this->getMockResolvedType(); @@ -153,28 +183,46 @@ public function testCreateNamedBuilderWithTypeInstanceWithParentTypeInstance() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); + + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options)); + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $type, null, $options)); } public function testCreateNamedBuilderWithResolvedTypeInstance() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); + + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $resolvedType, null, $options)); + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', $resolvedType, null, $options)); } public function testCreateNamedBuilderFillsDataOption() { $givenOptions = array('a' => '1', 'b' => '2'); $expectedOptions = array_merge($givenOptions, array('data' => 'DATA')); + $resolvedOptions = array('a' => '2', 'b' => '3', 'data' => 'DATA'); $resolvedType = $this->getMockResolvedType(); $this->registry->expects($this->once()) @@ -185,14 +233,23 @@ public function testCreateNamedBuilderFillsDataOption() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $expectedOptions) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); + + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $givenOptions)); + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', 'type', 'DATA', $givenOptions)); } public function testCreateNamedBuilderDoesNotOverrideExistingDataOption() { $options = array('a' => '1', 'b' => '2', 'data' => 'CUSTOM'); + $resolvedOptions = array('a' => '2', 'b' => '3', 'data' => 'CUSTOM'); $resolvedType = $this->getMockResolvedType(); $this->registry->expects($this->once()) @@ -203,9 +260,17 @@ public function testCreateNamedBuilderDoesNotOverrideExistingDataOption() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue('BUILDER')); + ->will($this->returnValue($this->builder)); + + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); - $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $options)); + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->assertSame($this->builder, $this->factory->createNamedBuilder('name', 'type', 'DATA', $options)); } /** @@ -220,8 +285,8 @@ public function testCreateNamedBuilderThrowsUnderstandableException() public function testCreateUsesTypeNameIfTypeGivenAsString() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); - $builder = $this->getMockFormBuilder(); $this->registry->expects($this->once()) ->method('getType') @@ -231,9 +296,17 @@ public function testCreateUsesTypeNameIfTypeGivenAsString() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'TYPE', $options) - ->will($this->returnValue($builder)); + ->will($this->returnValue($this->builder)); + + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); - $builder->expects($this->once()) + $this->builder->expects($this->once()) ->method('getForm') ->will($this->returnValue('FORM')); @@ -243,8 +316,8 @@ public function testCreateUsesTypeNameIfTypeGivenAsString() public function testCreateUsesTypeNameIfTypeGivenAsObject() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); - $builder = $this->getMockFormBuilder(); $resolvedType->expects($this->once()) ->method('getName') @@ -253,9 +326,17 @@ public function testCreateUsesTypeNameIfTypeGivenAsObject() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'TYPE', $options) - ->will($this->returnValue($builder)); + ->will($this->returnValue($this->builder)); - $builder->expects($this->once()) + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->builder->expects($this->once()) ->method('getForm') ->will($this->returnValue('FORM')); @@ -265,8 +346,8 @@ public function testCreateUsesTypeNameIfTypeGivenAsObject() public function testCreateNamed() { $options = array('a' => '1', 'b' => '2'); + $resolvedOptions = array('a' => '2', 'b' => '3'); $resolvedType = $this->getMockResolvedType(); - $builder = $this->getMockFormBuilder(); $this->registry->expects($this->once()) ->method('getType') @@ -276,9 +357,17 @@ public function testCreateNamed() $resolvedType->expects($this->once()) ->method('createBuilder') ->with($this->factory, 'name', $options) - ->will($this->returnValue($builder)); + ->will($this->returnValue($this->builder)); - $builder->expects($this->once()) + $this->builder->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($resolvedOptions)); + + $resolvedType->expects($this->once()) + ->method('buildForm') + ->with($this->builder, $resolvedOptions); + + $this->builder->expects($this->once()) ->method('getForm') ->will($this->returnValue('FORM')); @@ -298,9 +387,9 @@ public function testCreateBuilderForPropertyWithoutTypeGuesser() ->with('firstName', 'text', null, array()) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); + $this->builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderForPropertyCreatesFormWithHighestConfidence() @@ -330,9 +419,9 @@ public function testCreateBuilderForPropertyCreatesFormWithHighestConfidence() ->with('firstName', 'password', null, array('max_length' => 7)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); + $this->builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderCreatesTextFormIfNoGuess() @@ -349,9 +438,9 @@ public function testCreateBuilderCreatesTextFormIfNoGuess() ->with('firstName', 'text') ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); + $this->builder = $factory->createBuilderForProperty('Application\Author', 'firstName'); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testOptionsCanBeOverridden() @@ -372,14 +461,14 @@ public function testOptionsCanBeOverridden() ->with('firstName', 'text', null, array('max_length' => 11)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName', null, array('max_length' => 11) ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderUsesMaxLengthIfFound() @@ -407,12 +496,12 @@ public function testCreateBuilderUsesMaxLengthIfFound() ->with('firstName', 'text', null, array('max_length' => 20)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName' ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderUsesRequiredSettingWithHighestConfidence() @@ -440,12 +529,12 @@ public function testCreateBuilderUsesRequiredSettingWithHighestConfidence() ->with('firstName', 'text', null, array('required' => false)) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName' ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } public function testCreateBuilderUsesPatternIfFound() @@ -473,12 +562,12 @@ public function testCreateBuilderUsesPatternIfFound() ->with('firstName', 'text', null, array('pattern' => '[a-zA-Z]')) ->will($this->returnValue('builderInstance')); - $builder = $factory->createBuilderForProperty( + $this->builder = $factory->createBuilderForProperty( 'Application\Author', 'firstName' ); - $this->assertEquals('builderInstance', $builder); + $this->assertEquals('builderInstance', $this->builder); } private function getMockFactory(array $methods = array()) @@ -498,9 +587,4 @@ private function getMockType() { return $this->getMock('Symfony\Component\Form\FormTypeInterface'); } - - private function getMockFormBuilder() - { - return $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); - } } diff --git a/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php index bb32a24122e58..23c599e3f5a2b 100644 --- a/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php @@ -36,211 +36,291 @@ class ResolvedFormTypeTest extends \PHPUnit_Framework_TestCase * @var \PHPUnit_Framework_MockObject_MockObject */ private $dataMapper; + private $parentType; + private $type; + private $extension1; + private $extension2; + private $parentResolvedType; + private $resolvedType; protected function setUp() { - if (!class_exists('Symfony\Component\OptionsResolver\OptionsResolver')) { - $this->markTestSkipped('The "OptionsResolver" component is not available'); - } - - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $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->parentType = $this->getMockFormType(); + $this->type = $this->getMockFormType(); + $this->extension1 = $this->getMockFormTypeExtension(); + $this->extension2 = $this->getMockFormTypeExtension(); + $this->parentResolvedType = new ResolvedFormType($this->parentType); + $this->resolvedType = new ResolvedFormType($this->type, array($this->extension1, $this->extension2), $this->parentResolvedType); } - public function testCreateBuilder() + public function testGetOptionsResolver() { if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) { $this->markTestSkipped('This test requires PHPUnit 3.7.'); } - $parentType = $this->getMockFormType(); - $type = $this->getMockFormType(); - $extension1 = $this->getMockFormTypeExtension(); - $extension2 = $this->getMockFormTypeExtension(); - - $parentResolvedType = new ResolvedFormType($parentType); - $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType); - $test = $this; $i = 0; - $assertIndex = function ($index) use (&$i, $test) { - return function () use (&$i, $test, $index) { + $assertIndexAndAddOption = function ($index, $option, $default) use (&$i, $test) { + return function (OptionsResolverInterface $resolver) use (&$i, $test, $index, $option, $default) { /* @var \PHPUnit_Framework_TestCase $test */ $test->assertEquals($index, $i, 'Executed at index '.$index); ++$i; - }; - }; - - $assertIndexAndAddOption = function ($index, $option, $default) use ($assertIndex) { - $assertIndex = $assertIndex($index); - - return function (OptionsResolverInterface $resolver) use ($assertIndex, $index, $option, $default) { - $assertIndex(); $resolver->setDefaults(array($option => $default)); }; }; // First the default options are generated for the super type - $parentType->expects($this->once()) + $this->parentType->expects($this->once()) ->method('setDefaultOptions') ->will($this->returnCallback($assertIndexAndAddOption(0, 'a', 'a_default'))); // The form type itself - $type->expects($this->once()) + $this->type->expects($this->once()) ->method('setDefaultOptions') ->will($this->returnCallback($assertIndexAndAddOption(1, 'b', 'b_default'))); // And its extensions - $extension1->expects($this->once()) + $this->extension1->expects($this->once()) ->method('setDefaultOptions') ->will($this->returnCallback($assertIndexAndAddOption(2, 'c', 'c_default'))); - $extension2->expects($this->once()) + $this->extension2->expects($this->once()) ->method('setDefaultOptions') ->will($this->returnCallback($assertIndexAndAddOption(3, 'd', 'd_default'))); $givenOptions = array('a' => 'a_custom', 'c' => 'c_custom'); $resolvedOptions = array('a' => 'a_custom', 'b' => 'b_default', 'c' => 'c_custom', 'd' => 'd_default'); - // Then the form is built for the super type - $parentType->expects($this->once()) + $resolver = $this->resolvedType->getOptionsResolver(); + + $this->assertEquals($resolvedOptions, $resolver->resolve($givenOptions)); + } + + public function testCreateBuilder() + { + if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) { + $this->markTestSkipped('This test requires PHPUnit 3.7.'); + } + + $givenOptions = array('a' => 'a_custom', 'c' => 'c_custom'); + $resolvedOptions = array('a' => 'a_custom', 'b' => 'b_default', 'c' => 'c_custom', 'd' => 'd_default'); + $optionsResolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + + $this->resolvedType = $this->getMockBuilder('Symfony\Component\Form\ResolvedFormType') + ->setConstructorArgs(array($this->type, array($this->extension1, $this->extension2), $this->parentResolvedType)) + ->setMethods(array('getOptionsResolver')) + ->getMock(); + + $this->resolvedType->expects($this->once()) + ->method('getOptionsResolver') + ->will($this->returnValue($optionsResolver)); + + $optionsResolver->expects($this->once()) + ->method('resolve') + ->with($givenOptions) + ->will($this->returnValue($resolvedOptions)); + + $factory = $this->getMockFormFactory(); + $builder = $this->resolvedType->createBuilder($factory, 'name', $givenOptions); + + $this->assertSame($this->resolvedType, $builder->getType()); + $this->assertSame($resolvedOptions, $builder->getOptions()); + $this->assertNull($builder->getDataClass()); + } + + public function testCreateBuilderWithDataClassOption() + { + if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) { + $this->markTestSkipped('This test requires PHPUnit 3.7.'); + } + + $givenOptions = array('data_class' => 'Foo'); + $resolvedOptions = array('data_class' => '\stdClass'); + $optionsResolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + + $this->resolvedType = $this->getMockBuilder('Symfony\Component\Form\ResolvedFormType') + ->setConstructorArgs(array($this->type, array($this->extension1, $this->extension2), $this->parentResolvedType)) + ->setMethods(array('getOptionsResolver')) + ->getMock(); + + $this->resolvedType->expects($this->once()) + ->method('getOptionsResolver') + ->will($this->returnValue($optionsResolver)); + + $optionsResolver->expects($this->once()) + ->method('resolve') + ->with($givenOptions) + ->will($this->returnValue($resolvedOptions)); + + $factory = $this->getMockFormFactory(); + $builder = $this->resolvedType->createBuilder($factory, 'name', $givenOptions); + + $this->assertSame($this->resolvedType, $builder->getType()); + $this->assertSame($resolvedOptions, $builder->getOptions()); + $this->assertSame('\stdClass', $builder->getDataClass()); + } + + public function testBuildForm() + { + if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) { + $this->markTestSkipped('This test requires PHPUnit 3.7.'); + } + + $test = $this; + $i = 0; + + $assertIndex = function ($index) use (&$i, $test) { + return function () use (&$i, $test, $index) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals($index, $i, 'Executed at index '.$index); + + ++$i; + }; + }; + + $options = array('a' => 'Foo', 'b' => 'Bar'); + $builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); + + // First the form is built for the super type + $this->parentType->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(4))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(0))); // Then the type itself - $type->expects($this->once()) + $this->type->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(5))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(1))); // Then its extensions - $extension1->expects($this->once()) + $this->extension1->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(6))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(2))); - $extension2->expects($this->once()) + $this->extension2->expects($this->once()) ->method('buildForm') - ->with($this->anything(), $resolvedOptions) - ->will($this->returnCallback($assertIndex(7))); + ->with($builder, $options) + ->will($this->returnCallback($assertIndex(3))); - $factory = $this->getMockFormFactory(); - $builder = $resolvedType->createBuilder($factory, 'name', $givenOptions); - - $this->assertSame($resolvedType, $builder->getType()); + $this->resolvedType->buildForm($builder, $options); } public function testCreateView() { - $parentType = $this->getMockFormType(); - $type = $this->getMockFormType(); - $field1Type = $this->getMockFormType(); - $field2Type = $this->getMockFormType(); - $extension1 = $this->getMockFormTypeExtension(); - $extension2 = $this->getMockFormTypeExtension(); - - $parentResolvedType = new ResolvedFormType($parentType); - $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType); - $field1ResolvedType = new ResolvedFormType($field1Type); - $field2ResolvedType = new ResolvedFormType($field2Type); + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + + $view = $this->resolvedType->createView($form); + + $this->assertInstanceOf('Symfony\Component\Form\FormView', $view); + $this->assertNull($view->parent); + } + + public function testCreateViewWithParent() + { + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $parentView = $this->getMock('Symfony\Component\Form\FormView'); + + $view = $this->resolvedType->createView($form, $parentView); + + $this->assertInstanceOf('Symfony\Component\Form\FormView', $view); + $this->assertSame($parentView, $view->parent); + } + public function testBuildView() + { $options = array('a' => '1', 'b' => '2'); - $form = $this->getBuilder('name', $options) - ->setCompound(true) - ->setDataMapper($this->dataMapper) - ->setType($resolvedType) - ->add($this->getBuilder('foo')->setType($field1ResolvedType)) - ->add($this->getBuilder('bar')->setType($field2ResolvedType)) - ->getForm(); + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $view = $this->getMock('Symfony\Component\Form\FormView'); $test = $this; $i = 0; - $assertIndexAndNbOfChildViews = function ($index, $nbOfChildViews) use (&$i, $test) { - return function (FormView $view) use (&$i, $test, $index, $nbOfChildViews) { + $assertIndex = function ($index) use (&$i, $test) { + return function () use (&$i, $test, $index) { /* @var \PHPUnit_Framework_TestCase $test */ $test->assertEquals($index, $i, 'Executed at index '.$index); - $test->assertCount($nbOfChildViews, $view); ++$i; }; }; // First the super type - $parentType->expects($this->once()) + $this->parentType->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(0, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(0))); // Then the type itself - $type->expects($this->once()) + $this->type->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(1, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(1))); // Then its extensions - $extension1->expects($this->once()) + $this->extension1->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(2, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(2))); - $extension2->expects($this->once()) + $this->extension2->expects($this->once()) ->method('buildView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(3, 0))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(3))); - // Now the first child form - $field1Type->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(4, 0))); - $field1Type->expects($this->once()) - ->method('finishView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(5, 0))); + $this->resolvedType->buildView($view, $form, $options); + } - // And the second child form - $field2Type->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(6, 0))); - $field2Type->expects($this->once()) - ->method('finishView') - ->will($this->returnCallback($assertIndexAndNbOfChildViews(7, 0))); + public function testFinishView() + { + $options = array('a' => '1', 'b' => '2'); + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $view = $this->getMock('Symfony\Component\Form\FormView'); + + $test = $this; + $i = 0; - // Again first the parent - $parentType->expects($this->once()) + $assertIndex = function ($index) use (&$i, $test) { + return function () use (&$i, $test, $index) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals($index, $i, 'Executed at index '.$index); + + ++$i; + }; + }; + + // First the super type + $this->parentType->expects($this->once()) ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(8, 2))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(0))); // Then the type itself - $type->expects($this->once()) + $this->type->expects($this->once()) ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(9, 2))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(1))); // Then its extensions - $extension1->expects($this->once()) + $this->extension1->expects($this->once()) ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(10, 2))); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(2))); - $extension2->expects($this->once()) + $this->extension2->expects($this->once()) ->method('finishView') - ->with($this->anything(), $form, $options) - ->will($this->returnCallback($assertIndexAndNbOfChildViews(11, 2))); - - $parentView = new FormView(); - $view = $resolvedType->createView($form, $parentView); + ->with($view, $form, $options) + ->will($this->returnCallback($assertIndex(3))); - $this->assertSame($parentView, $view->parent); + $this->resolvedType->finishView($view, $form, $options); } /** diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 14f9dadce40a2..3de847e97fb04 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -24,11 +24,14 @@ }, "require-dev": { "symfony/validator": "~2.2", - "symfony/http-foundation": "~2.2" + "symfony/http-foundation": "~2.2", + "symfony/security-csrf": "~2.4" }, "suggest": { - "symfony/validator": "", - "symfony/http-foundation": "" + "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." }, "autoload": { "psr-0": { "Symfony\\Component\\Form\\": "" } @@ -37,7 +40,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index ddcb657ee2266..4d27435807279 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -183,7 +183,7 @@ public function prepare(Request $request) $path = $this->file->getRealPath(); if (strtolower($type) == 'x-accel-redirect') { // Do X-Accel-Mapping substitutions. - foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) { + foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) { $mapping = explode('=', $mapping, 2); if (2 == count($mapping)) { diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 41e8eb2ec32a7..0da734bce2f1c 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +2.5.0 +----- + + * added `JsonResponse::setEncodingOptions()` & `JsonResponse::getEncodingOptions()` for easier manipulation + of the options used while encoding data to JSON format. + +2.4.0 +----- + + * added RequestStack + * added Request::getEncodings() + * added accessors methods to session handlers + 2.3.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/ExpressionRequestMatcher.php b/src/Symfony/Component/HttpFoundation/ExpressionRequestMatcher.php new file mode 100644 index 0000000000000..e9c8441ce314b --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/ExpressionRequestMatcher.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + +/** + * ExpressionRequestMatcher uses an expression to match a Request. + * + * @author Fabien Potencier + */ +class ExpressionRequestMatcher extends RequestMatcher +{ + private $language; + private $expression; + + public function setExpression(ExpressionLanguage $language, $expression) + { + $this->language = $language; + $this->expression = $expression; + } + + public function matches(Request $request) + { + if (!$this->language) { + throw new \LogicException('Unable to match the request as the expression language is not available.'); + } + + return $this->language->evaluate($this->expression, array( + 'request' => $request, + 'method' => $request->getMethod(), + 'path' => rawurldecode($request->getPathInfo()), + 'host' => $request->getHost(), + 'ip' => $request->getClientIp(), + 'attributes' => $request->attributes->all(), + )) && parent::matches($request); + } +} diff --git a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php index f7e39f9a1fbab..16c4cdb65eda9 100644 --- a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php +++ b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php @@ -252,7 +252,7 @@ public function move($directory, $name = null) return $target; } - throw new FileException($this->getErrorMessage($this->getError())); + throw new FileException($this->getErrorMessage()); } /** @@ -290,11 +290,9 @@ public static function getMaxFilesize() /** * Returns an informative upload error message. * - * @param int $code The error code returned by an upload attempt - * * @return string The error message regarding the specified error code */ - private function getErrorMessage($errorCode) + public function getErrorMessage() { static $errors = array( UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d kb).', @@ -306,9 +304,10 @@ private function getErrorMessage($errorCode) UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', ); + $errorCode = $this->error; $maxFilesize = $errorCode === UPLOAD_ERR_INI_SIZE ? self::getMaxFilesize() / 1024 : 0; $message = isset($errors[$errorCode]) ? $errors[$errorCode] : 'The file "%s" was not uploaded due to an unknown error.'; - return sprintf($message, $this->getClientOriginalName(), $maxFilesize); + return sprintf($message, $this->getClientOriginalName(), $maxFilesize); } } diff --git a/src/Symfony/Component/HttpFoundation/HeaderBag.php b/src/Symfony/Component/HttpFoundation/HeaderBag.php index b579eb991a146..2b9ef0e443e15 100644 --- a/src/Symfony/Component/HttpFoundation/HeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/HeaderBag.php @@ -20,8 +20,8 @@ */ class HeaderBag implements \IteratorAggregate, \Countable { - protected $headers; - protected $cacheControl; + protected $headers = array(); + protected $cacheControl = array(); /** * Constructor. @@ -32,8 +32,6 @@ class HeaderBag implements \IteratorAggregate, \Countable */ public function __construct(array $headers = array()) { - $this->cacheControl = array(); - $this->headers = array(); foreach ($headers as $key => $values) { $this->set($key, $values); } diff --git a/src/Symfony/Component/HttpFoundation/JsonResponse.php b/src/Symfony/Component/HttpFoundation/JsonResponse.php index 4a96d082d55cb..004324bfedea7 100644 --- a/src/Symfony/Component/HttpFoundation/JsonResponse.php +++ b/src/Symfony/Component/HttpFoundation/JsonResponse.php @@ -26,6 +26,7 @@ class JsonResponse extends Response { protected $data; protected $callback; + protected $encodingOptions; /** * Constructor. @@ -41,6 +42,10 @@ public function __construct($data = null, $status = 200, $headers = array()) if (null === $data) { $data = new \ArrayObject(); } + + // Encode <, >, ', &, and " for RFC4627-compliant JSON, which may also be embedded into HTML. + $this->encodingOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; + $this->setData($data); } @@ -55,11 +60,11 @@ public static function create($data = null, $status = 200, $headers = array()) /** * Sets the JSONP callback. * - * @param string $callback + * @param string|null $callback The JSONP callback or null to use none * * @return JsonResponse * - * @throws \InvalidArgumentException + * @throws \InvalidArgumentException When the callback name is not valid */ public function setCallback($callback = null) { @@ -80,7 +85,7 @@ public function setCallback($callback = null) } /** - * Sets the data to be sent as json. + * Sets the data to be sent as JSON. * * @param mixed $data * @@ -90,8 +95,7 @@ public function setCallback($callback = null) */ public function setData($data = array()) { - // Encode <, >, ', &, and " for RFC4627-compliant JSON, which may also be embedded into HTML. - $this->data = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); + $this->data = @json_encode($data, $this->encodingOptions); if (JSON_ERROR_NONE !== json_last_error()) { throw new \InvalidArgumentException($this->transformJsonError()); @@ -101,7 +105,31 @@ public function setData($data = array()) } /** - * Updates the content and headers according to the json data and callback. + * Returns options used while encoding data to JSON. + * + * @return integer + */ + public function getEncodingOptions() + { + return $this->encodingOptions; + } + + /** + * Sets options used while encoding data to JSON. + * + * @param integer $encodingOptions + * + * @return JsonResponse + */ + public function setEncodingOptions($encodingOptions) + { + $this->encodingOptions = (integer) $encodingOptions; + + return $this->setData(json_decode($this->data)); + } + + /** + * Updates the content and headers according to the JSON data and callback. * * @return JsonResponse */ diff --git a/src/Symfony/Component/HttpFoundation/ParameterBag.php b/src/Symfony/Component/HttpFoundation/ParameterBag.php index c8720cdc12e2b..a4ac98cffebf9 100644 --- a/src/Symfony/Component/HttpFoundation/ParameterBag.php +++ b/src/Symfony/Component/HttpFoundation/ParameterBag.php @@ -266,7 +266,7 @@ public function getInt($key, $default = 0, $deep = false) * * @return mixed */ - public function filter($key, $default = null, $deep = false, $filter=FILTER_DEFAULT, $options=array()) + public function filter($key, $default = null, $deep = false, $filter = FILTER_DEFAULT, $options = array()) { $value = $this->get($key, $default, $deep); diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 685772b250179..88a61b642dd88 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -141,6 +141,11 @@ class Request */ protected $charsets; + /** + * @var array + */ + protected $encodings; + /** * @var array */ @@ -196,6 +201,8 @@ class Request */ protected static $formats; + protected static $requestFactory; + /** * Constructor. * @@ -242,6 +249,7 @@ public function initialize(array $query = array(), array $request = array(), arr $this->content = $content; $this->languages = null; $this->charsets = null; + $this->encodings = null; $this->acceptableContentTypes = null; $this->pathInfo = null; $this->requestUri = null; @@ -260,7 +268,7 @@ public function initialize(array $query = array(), array $request = array(), arr */ public static function createFromGlobals() { - $request = new static($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); + $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER); if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH')) @@ -378,7 +386,21 @@ public static function create($uri, $method = 'GET', $parameters = array(), $coo $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : ''); $server['QUERY_STRING'] = $queryString; - return new static($query, $request, array(), $cookies, $files, $server, $content); + return self::createRequestFromFactory($query, $request, array(), $cookies, $files, $server, $content); + } + + /** + * Sets a callable able to create a Request instance. + * + * This is mainly useful when you need to override the Request class + * to keep BC with an existing system. It should not be used for any + * other purpose. + * + * @param callable|null $callable A PHP callable + */ + public static function setFactory($callable) + { + self::$requestFactory = $callable; } /** @@ -419,6 +441,7 @@ public function duplicate(array $query = null, array $request = null, array $att } $dup->languages = null; $dup->charsets = null; + $dup->encodings = null; $dup->acceptableContentTypes = null; $dup->pathInfo = null; $dup->requestUri = null; @@ -973,7 +996,7 @@ public function getUserInfo() $pass = $this->getPassword(); if ('' != $pass) { - $userinfo .= ":$pass"; + $userinfo .= ":$pass"; } return $userinfo; @@ -1139,7 +1162,7 @@ public function getHost() // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) // check that it does not contain forbidden characters (see RFC 952 and RFC 2181) if ($host && !preg_match('/^\[?(?:[a-zA-Z0-9-:\]_]+\.?)+$/', $host)) { - throw new \UnexpectedValueException('Invalid Host "'.$host.'"'); + throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host)); } if (count(self::$trustedHostPatterns) > 0) { @@ -1157,7 +1180,7 @@ public function getHost() } } - throw new \UnexpectedValueException('Untrusted Host "'.$host.'"'); + throw new \UnexpectedValueException(sprintf('Untrusted Host "%s"', $host)); } return $host; @@ -1536,6 +1559,20 @@ public function getCharsets() return $this->charsets = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all()); } + /** + * Gets a list of encodings acceptable by the client browser. + * + * @return array List of encodings in preferable order + */ + public function getEncodings() + { + if (null !== $this->encodings) { + return $this->encodings; + } + + return $this->encodings = array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all()); + } + /** * Gets a list of content types acceptable by the client browser * @@ -1799,4 +1836,19 @@ private function getUrlencodedPrefix($string, $prefix) return false; } + + private static function createRequestFromFactory(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) + { + if (self::$requestFactory) { + $request = call_user_func(self::$requestFactory, $query, $request, $attributes, $cookies, $files, $server, $content); + + if (!$request instanceof Request) { + throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.'); + } + + return $request; + } + + return new static($query, $request, $attributes, $cookies, $files, $server, $content); + } } diff --git a/src/Symfony/Component/HttpFoundation/RequestStack.php b/src/Symfony/Component/HttpFoundation/RequestStack.php new file mode 100644 index 0000000000000..4b0ef28deefd3 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/RequestStack.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Request stack that controls the lifecycle of requests. + * + * @author Benjamin Eberlei + */ +class RequestStack +{ + /** + * @var Request[] + */ + private $requests = array(); + + /** + * Pushes a Request on the stack. + * + * This method should generally not be called directly as the stack + * management should be taken care of by the application itself. + */ + public function push(Request $request) + { + $this->requests[] = $request; + } + + /** + * Pops the current request from the stack. + * + * This operation lets the current request go out of scope. + * + * This method should generally not be called directly as the stack + * management should be taken care of by the application itself. + * + * @return Request|null + */ + public function pop() + { + if (!$this->requests) { + return null; + } + + return array_pop($this->requests); + } + + /** + * @return Request|null + */ + public function getCurrentRequest() + { + return end($this->requests) ?: null; + } + + /** + * Gets the master Request. + * + * Be warned that making your code aware of the master request + * might make it un-compatible with other features of your framework + * like ESI support. + * + * @return Request|null + */ + public function getMasterRequest() + { + if (!$this->requests) { + return null; + } + + return $this->requests[0]; + } + + /** + * Returns the parent request of the current. + * + * Be warned that making your code aware of the parent request + * might make it un-compatible with other features of your framework + * like ESI support. + * + * If current Request is the master request, it returns null. + * + * @return Request|null + */ + public function getParentRequest() + { + $pos = count($this->requests) - 2; + + if (!isset($this->requests[$pos])) { + return null; + } + + return $this->requests[$pos]; + } +} diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index 828c868c3ccf0..bc9a193c7dc2c 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -20,6 +20,67 @@ */ class Response { + const HTTP_CONTINUE = 100; + const HTTP_SWITCHING_PROTOCOLS = 101; + const HTTP_PROCESSING = 102; // RFC2518 + const HTTP_OK = 200; + const HTTP_CREATED = 201; + const HTTP_ACCEPTED = 202; + const HTTP_NON_AUTHORITATIVE_INFORMATION = 203; + const HTTP_NO_CONTENT = 204; + const HTTP_RESET_CONTENT = 205; + const HTTP_PARTIAL_CONTENT = 206; + const HTTP_MULTI_STATUS = 207; // RFC4918 + const HTTP_ALREADY_REPORTED = 208; // RFC5842 + const HTTP_IM_USED = 226; // RFC3229 + const HTTP_MULTIPLE_CHOICES = 300; + const HTTP_MOVED_PERMANENTLY = 301; + const HTTP_FOUND = 302; + const HTTP_SEE_OTHER = 303; + const HTTP_NOT_MODIFIED = 304; + const HTTP_USE_PROXY = 305; + const HTTP_RESERVED = 306; + const HTTP_TEMPORARY_REDIRECT = 307; + const HTTP_PERMANENTLY_REDIRECT = 308; // RFC-reschke-http-status-308-07 + const HTTP_BAD_REQUEST = 400; + const HTTP_UNAUTHORIZED = 401; + const HTTP_PAYMENT_REQUIRED = 402; + const HTTP_FORBIDDEN = 403; + const HTTP_NOT_FOUND = 404; + const HTTP_METHOD_NOT_ALLOWED = 405; + const HTTP_NOT_ACCEPTABLE = 406; + const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407; + const HTTP_REQUEST_TIMEOUT = 408; + const HTTP_CONFLICT = 409; + const HTTP_GONE = 410; + const HTTP_LENGTH_REQUIRED = 411; + const HTTP_PRECONDITION_FAILED = 412; + const HTTP_REQUEST_ENTITY_TOO_LARGE = 413; + const HTTP_REQUEST_URI_TOO_LONG = 414; + const HTTP_UNSUPPORTED_MEDIA_TYPE = 415; + const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + const HTTP_EXPECTATION_FAILED = 417; + const HTTP_I_AM_A_TEAPOT = 418; // RFC2324 + const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918 + const HTTP_LOCKED = 423; // RFC4918 + const HTTP_FAILED_DEPENDENCY = 424; // RFC4918 + const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425; // RFC2817 + const HTTP_UPGRADE_REQUIRED = 426; // RFC2817 + const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585 + const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585 + const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585 + const HTTP_INTERNAL_SERVER_ERROR = 500; + const HTTP_NOT_IMPLEMENTED = 501; + const HTTP_BAD_GATEWAY = 502; + const HTTP_SERVICE_UNAVAILABLE = 503; + const HTTP_GATEWAY_TIMEOUT = 504; + const HTTP_VERSION_NOT_SUPPORTED = 505; + const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295 + const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918 + const HTTP_LOOP_DETECTED = 508; // RFC5842 + const HTTP_NOT_EXTENDED = 510; // RFC2774 + const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585 + /** * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag */ @@ -127,7 +188,7 @@ class Response /** * Constructor. * - * @param string $content The response content + * @param mixed $content The response content, see setContent() * @param integer $status The response status code * @param array $headers An array of response headers * @@ -154,7 +215,7 @@ public function __construct($content = '', $status = 200, $headers = array()) * return Response::create($body, 200) * ->setSharedMaxAge(300); * - * @param string $content The response content + * @param mixed $content The response content, see setContent() * @param integer $status The response status code * @param array $headers An array of response headers * @@ -271,12 +332,12 @@ public function sendHeaders() } // status - header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)); + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode); // headers foreach ($this->headers->allPreserveCase() as $name => $values) { foreach ($values as $value) { - header($name.': '.$value, false); + header($name.': '.$value, false, $this->statusCode); } } @@ -342,9 +403,9 @@ public function send() /** * Sets the response content. * - * Valid types are strings, numbers, and objects that implement a __toString() method. + * Valid types are strings, numbers, null, and objects that implement a __toString() method. * - * @param mixed $content + * @param mixed $content Content that can be cast to string * * @return Response * diff --git a/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php b/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php index e9d0257152ec9..af416d6d3a4c4 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php @@ -31,7 +31,7 @@ class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Counta /** * Constructor. * - * @param string $storageKey The key used to store attributes in the session. + * @param string $storageKey The key used to store attributes in the session */ public function __construct($storageKey = '_sf2_attributes') { @@ -148,7 +148,7 @@ public function getIterator() /** * Returns the number of attributes. * - * @return int The number of attributes + * @return integer The number of attributes */ public function count() { diff --git a/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBagInterface.php b/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBagInterface.php index 5f1f37be25c31..6356056edddee 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBagInterface.php +++ b/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBagInterface.php @@ -33,7 +33,7 @@ public function has($name); * Returns an attribute. * * @param string $name The attribute name - * @param mixed $default The default value if not found. + * @param mixed $default The default value if not found * * @return mixed */ @@ -66,7 +66,7 @@ public function replace(array $attributes); * * @param string $name * - * @return mixed The removed value + * @return mixed The removed value or null when it does not exist */ public function remove($name); } diff --git a/src/Symfony/Component/HttpFoundation/Session/Attribute/NamespacedAttributeBag.php b/src/Symfony/Component/HttpFoundation/Session/Attribute/NamespacedAttributeBag.php index c0dd358e85117..7cafb2241c838 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Attribute/NamespacedAttributeBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Attribute/NamespacedAttributeBag.php @@ -149,8 +149,8 @@ protected function &resolveAttributePath($name, $writeContext = false) */ protected function resolveKey($name) { - if (strpos($name, $this->namespaceCharacter) !== false) { - $name = substr($name, strrpos($name, $this->namespaceCharacter)+1, strlen($name)); + if (false !== $pos = strrpos($name, $this->namespaceCharacter)) { + $name = substr($name, $pos+1); } return $name; diff --git a/src/Symfony/Component/HttpFoundation/Session/Flash/AutoExpireFlashBag.php b/src/Symfony/Component/HttpFoundation/Session/Flash/AutoExpireFlashBag.php index c6e41de62e287..25a62604d2607 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Flash/AutoExpireFlashBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Flash/AutoExpireFlashBag.php @@ -25,7 +25,7 @@ class AutoExpireFlashBag implements FlashBagInterface * * @var array */ - private $flashes = array(); + private $flashes = array('display' => array(), 'new' => array()); /** * The storage key for flashes in the session @@ -42,7 +42,6 @@ class AutoExpireFlashBag implements FlashBagInterface public function __construct($storageKey = '_sf2_flashes') { $this->storageKey = $storageKey; - $this->flashes = array('display' => array(), 'new' => array()); } /** diff --git a/src/Symfony/Component/HttpFoundation/Session/Flash/FlashBag.php b/src/Symfony/Component/HttpFoundation/Session/Flash/FlashBag.php index d62e3835abe08..2bd7786d7a918 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Flash/FlashBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Flash/FlashBag.php @@ -14,6 +14,8 @@ /** * FlashBag flash message container. * + * \IteratorAggregate implementation is deprecated and will be removed in 3.0. + * * @author Drak */ class FlashBag implements FlashBagInterface, \IteratorAggregate @@ -76,7 +78,7 @@ public function add($type, $message) /** * {@inheritdoc} */ - public function peek($type, array $default =array()) + public function peek($type, array $default = array()) { return $this->has($type) ? $this->flashes[$type] : $default; } @@ -167,6 +169,8 @@ public function clear() /** * Returns an iterator for flashes. * + * @deprecated Will be removed in 3.0. + * * @return \ArrayIterator An \ArrayIterator instance */ public function getIterator() diff --git a/src/Symfony/Component/HttpFoundation/Session/SessionInterface.php b/src/Symfony/Component/HttpFoundation/Session/SessionInterface.php index a94fad00d6f0f..dc2f7bc4f339c 100644 --- a/src/Symfony/Component/HttpFoundation/Session/SessionInterface.php +++ b/src/Symfony/Component/HttpFoundation/Session/SessionInterface.php @@ -163,7 +163,7 @@ public function replace(array $attributes); * * @param string $name * - * @return mixed The removed value + * @return mixed The removed value or null when it does not exist * * @api */ diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php index 4a5e63989a463..b2384505ae60d 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php @@ -106,4 +106,14 @@ public function gc($lifetime) // not required here because memcache will auto expire the records anyhow. return true; } + + /** + * Return a Memcache instance + * + * @return \Memcache + */ + protected function getMemcache() + { + return $this->memcache; + } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php index b3ca0bd3cdcf9..ed7d6edf4b824 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -112,4 +112,14 @@ public function gc($lifetime) // not required here because memcached will auto expire the records anyhow. return true; } + + /** + * Return a Memcached instance + * + * @return \Memcached + */ + protected function getMemcached() + { + return $this->memcached; + } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php index 69ebae9542cc3..4d819fe3f8cc9 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php @@ -160,4 +160,14 @@ private function getCollection() return $this->collection; } + + /** + * Return a Mongo instance + * + * @return \Mongo + */ + protected function getMongo() + { + return $this->mongo; + } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 347cbee706a08..baf8eea8f610e 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -240,4 +240,14 @@ private function createNewSession($id, $data = '') return true; } + + /** + * Return a PDO instance + * + * @return \PDO + */ + protected function getConnection() + { + return $this->pdo; + } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php new file mode 100644 index 0000000000000..c43c9d05feb0b --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Wraps another SessionHandlerInterface to only write the session when it has been modified. + * + * @author Adrien Brault + */ +class WriteCheckSessionHandler implements \SessionHandlerInterface +{ + /** + * @var \SessionHandlerInterface + */ + private $wrappedSessionHandler; + + /** + * @var array sessionId => session + */ + private $readSessions; + + public function __construct(\SessionHandlerInterface $wrappedSessionHandler) + { + $this->wrappedSessionHandler = $wrappedSessionHandler; + } + + /** + * {@inheritdoc} + */ + public function close() + { + return $this->wrappedSessionHandler->close(); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + return $this->wrappedSessionHandler->destroy($sessionId); + } + + /** + * {@inheritdoc} + */ + public function gc($maxLifetime) + { + return $this->wrappedSessionHandler->gc($maxLifetime); + } + + /** + * {@inheritdoc} + */ + public function open($savePath, $sessionId) + { + return $this->wrappedSessionHandler->open($savePath, $sessionId); + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + $session = $this->wrappedSessionHandler->read($sessionId); + + $this->readSessions[$sessionId] = $session; + + return $session; + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $sessionData) + { + if (isset($this->readSessions[$sessionId]) && $sessionData === $this->readSessions[$sessionId]) { + return true; + } + + return $this->wrappedSessionHandler->write($sessionId, $sessionData); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php b/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php index 892d004b5d611..44212979c307d 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php @@ -39,7 +39,7 @@ class MetadataBag implements SessionBagInterface /** * @var array */ - protected $meta = array(); + protected $meta = array(self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0); /** * Unix timestamp. @@ -48,15 +48,21 @@ class MetadataBag implements SessionBagInterface */ private $lastUsed; + /** + * @var integer + */ + private $updateThreshold; + /** * Constructor. * - * @param string $storageKey The key used to store bag in the session. + * @param string $storageKey The key used to store bag in the session. + * @param integer $updateThreshold The time to wait between two UPDATED updates */ - public function __construct($storageKey = '_sf2_meta') + public function __construct($storageKey = '_sf2_meta', $updateThreshold = 0) { $this->storageKey = $storageKey; - $this->meta = array(self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0); + $this->updateThreshold = $updateThreshold; } /** @@ -68,7 +74,11 @@ public function initialize(array &$array) if (isset($array[self::CREATED])) { $this->lastUsed = $this->meta[self::UPDATED]; - $this->meta[self::UPDATED] = time(); + + $timeStamp = time(); + if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) { + $this->meta[self::UPDATED] = $timeStamp; + } } else { $this->stampCreated(); } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php b/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php index 9d7fb7eaf6965..8b1c2137f6a8a 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/MockArraySessionStorage.php @@ -249,7 +249,7 @@ public function getMetadataBag() */ protected function generateId() { - return sha1(uniqid(mt_rand())); + return hash('sha256', uniqid(mt_rand())); } protected function loadSession() diff --git a/src/Symfony/Component/HttpFoundation/StreamedResponse.php b/src/Symfony/Component/HttpFoundation/StreamedResponse.php index ae579beaa954b..d9fece658f49a 100644 --- a/src/Symfony/Component/HttpFoundation/StreamedResponse.php +++ b/src/Symfony/Component/HttpFoundation/StreamedResponse.php @@ -34,9 +34,9 @@ class StreamedResponse extends Response /** * Constructor. * - * @param mixed $callback A valid PHP callback - * @param integer $status The response status code - * @param array $headers An array of response headers + * @param callable|null $callback A valid PHP callback or null to set it later + * @param integer $status The response status code + * @param array $headers An array of response headers * * @api */ @@ -51,7 +51,13 @@ public function __construct($callback = null, $status = 200, $headers = array()) } /** - * {@inheritDoc} + * Factory method for chainability + * + * @param callable|null $callback A valid PHP callback or null to set it later + * @param integer $status The response status code + * @param array $headers An array of response headers + * + * @return StreamedResponse */ public static function create($callback = null, $status = 200, $headers = array()) { @@ -61,7 +67,7 @@ public static function create($callback = null, $status = 200, $headers = array( /** * Sets the PHP callback associated with this Response. * - * @param mixed $callback A valid PHP callback + * @param callable $callback A valid PHP callback * * @throws \LogicException */ diff --git a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php index e6c4c35016443..def1c7a378f21 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php @@ -51,7 +51,7 @@ public function testInstantiationThrowsExceptionIfCookieNameContainsInvalidChara */ public function testInvalidExpiration() { - $cookie = new Cookie('MyCookie', 'foo','bar'); + $cookie = new Cookie('MyCookie', 'foo', 'bar'); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php b/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php index f6ea340091ba5..5b48970d5e158 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php @@ -224,7 +224,7 @@ public function testIsValid() null, filesize(__DIR__.'/Fixtures/test.gif'), UPLOAD_ERR_OK, - true + true ); $this->assertTrue($file->isValid()); diff --git a/src/Symfony/Component/HttpFoundation/Tests/JsonResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/JsonResponseTest.php index ef392ca59dbe6..c7ea72676de0f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/JsonResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/JsonResponseTest.php @@ -166,6 +166,25 @@ public function testJsonEncodeFlags() $this->assertEquals('"\u003C\u003E\u0027\u0026\u0022"', $response->getContent()); } + public function testGetEncodingOptions() + { + $response = new JsonResponse(); + + $this->assertEquals(JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT, $response->getEncodingOptions()); + } + + public function testSetEncodingOptions() + { + $response = new JsonResponse(); + $response->setData(array(array(1, 2, 3))); + + $this->assertEquals('[[1,2,3]]', $response->getContent()); + + $response->setEncodingOptions(JSON_FORCE_OBJECT); + + $this->assertEquals('{"0":{"0":1,"1":2,"2":3}}', $response->getContent()); + } + /** * @expectedException \InvalidArgumentException */ diff --git a/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php index 330d9fee51fbf..2a097d6fd422a 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RedirectResponseTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\HttpFoundation\Tests; -use \Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; class RedirectResponseTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 13efc4300b0ae..db331f283d05b 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -881,14 +881,14 @@ public function testGetClientIpsProvider() public function testGetContentWorksTwiceInDefaultMode() { - $req = new Request; + $req = new Request(); $this->assertEquals('', $req->getContent()); $this->assertEquals('', $req->getContent()); } public function testGetContentReturnsResource() { - $req = new Request; + $req = new Request(); $retval = $req->getContent(true); $this->assertInternalType('resource', $retval); $this->assertEquals("", fread($retval, 1)); @@ -901,7 +901,7 @@ public function testGetContentReturnsResource() */ public function testGetContentCantBeCalledTwiceWithResources($first, $second) { - $req = new Request; + $req = new Request(); $req->getContent($first); $req->getContent($second); } @@ -1001,6 +1001,12 @@ public function testOverrideGlobals() $this->assertArrayHasKey('HTTP_X_FORWARDED_PROTO', $_SERVER); + $request->headers->set('CONTENT_TYPE', 'multipart/form-data'); + $request->headers->set('CONTENT_LENGTH', 12345); + $request->overrideGlobals(); + $this->assertArrayHasKey('CONTENT_TYPE', $_SERVER); + $this->assertArrayHasKey('CONTENT_LENGTH', $_SERVER); + // restore initial $_SERVER array $_SERVER = $server; } @@ -1157,6 +1163,22 @@ public function testGetCharsets() $this->assertEquals(array('ISO-8859-1', 'utf-8', '*'), $request->getCharsets()); } + public function testGetEncodings() + { + $request = new Request(); + $this->assertEquals(array(), $request->getEncodings()); + $request->headers->set('Accept-Encoding', 'gzip,deflate,sdch'); + $this->assertEquals(array(), $request->getEncodings()); // testing caching + + $request = new Request(); + $request->headers->set('Accept-Encoding', 'gzip,deflate,sdch'); + $this->assertEquals(array('gzip', 'deflate', 'sdch'), $request->getEncodings()); + + $request = new Request(); + $request->headers->set('Accept-Encoding', 'gzip;q=0.4,deflate;q=0.9,compress;q=0.7'); + $this->assertEquals(array('deflate', 'compress', 'gzip'), $request->getEncodings()); + } + public function testGetAcceptableContentTypes() { $request = new Request(); @@ -1339,7 +1361,7 @@ public function getBaseUrlData() */ public function testUrlencodedStringPrefix($string, $prefix, $expect) { - $request = new Request; + $request = new Request(); $me = new \ReflectionMethod($request, 'getUrlencodedPrefix'); $me->setAccessible(true); @@ -1455,6 +1477,24 @@ public function testTrustedProxies() Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X_FORWARDED_PROTO'); } + /** + * @expectedException \InvalidArgumentException + */ + public function testSetTrustedProxiesInvalidHeaderName() + { + Request::create('http://example.com/'); + Request::setTrustedHeaderName('bogus name', 'X_MY_FOR'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetTrustedProxiesInvalidHeaderName() + { + Request::create('http://example.com/'); + Request::getTrustedHeaderName('bogus name'); + } + /** * @dataProvider iisRequestUriProvider */ @@ -1467,7 +1507,7 @@ public function testIISRequestUri($headers, $server, $expectedRequestUri) $this->assertEquals($expectedRequestUri, $request->getRequestUri(), '->getRequestUri() is correct'); $subRequestUri = '/bar/foo'; - $subRequest = $request::create($subRequestUri, 'get', array(), array(), array(), $request->server->all()); + $subRequest = Request::create($subRequestUri, 'get', array(), array(), array(), $request->server->all()); $this->assertEquals($subRequestUri, $subRequest->getRequestUri(), '->getRequestUri() is correct in sub request'); } @@ -1586,6 +1626,17 @@ public function testTrustedHosts() // reset request for following tests Request::setTrustedHosts(array()); } + + public function testFactory() + { + Request::setFactory(function (array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null) { + return new NewRequest(); + }); + + $this->assertEquals('foo', Request::create('/')->getFoo()); + + Request::setFactory(null); + } } class RequestContentProxy extends Request @@ -1595,3 +1646,11 @@ public function getContent($asResource = false) return http_build_query(array('_method' => 'PUT', 'content' => 'mycontent')); } } + +class NewRequest extends Request +{ + public function getFoo() + { + return 'foo'; + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index 2f48ed85c3165..11eb38c72372f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -79,6 +79,19 @@ public function testIsCacheable() $this->assertFalse($response->isCacheable()); } + public function testIsCacheableWithErrorCode() + { + $response = new Response('', 500); + $this->assertFalse($response->isCacheable()); + } + + public function testIsCacheableWithNoStoreDirective() + { + $response = new Response(); + $response->headers->set('cache-control', 'private'); + $this->assertFalse($response->isCacheable()); + } + public function testIsCacheableWithSetTtl() { $response = new Response(); @@ -118,6 +131,50 @@ public function testIsNotModified() $this->assertFalse($modified); } + public function testIsNotModifiedNotSafe() + { + $request = Request::create('/homepage', 'POST'); + + $response = new Response(); + $this->assertFalse($response->isNotModified($request)); + } + + public function testIsNotModifiedLastModified() + { + $modified = 'Sun, 25 Aug 2013 18:33:31 GMT'; + + $request = new Request(); + $request->headers->set('If-Modified-Since', $modified); + + $response = new Response(); + $response->headers->set('Last-Modified', $modified); + + $this->assertTrue($response->isNotModified($request)); + + $response->headers->set('Last-Modified', ''); + $this->assertFalse($response->isNotModified($request)); + } + + public function testIsNotModifiedEtag() + { + $etagOne = 'randomly_generated_etag'; + $etagTwo = 'randomly_generated_etag_2'; + + $request = new Request(); + $request->headers->set('if_none_match', sprintf('%s, %s, %s', $etagOne, $etagTwo, 'etagThree')); + + $response = new Response(); + + $response->headers->set('ETag', $etagOne); + $this->assertTrue($response->isNotModified($request)); + + $response->headers->set('ETag', $etagTwo); + $this->assertTrue($response->isNotModified($request)); + + $response->headers->set('ETag', ''); + $this->assertFalse($response->isNotModified($request)); + } + public function testIsValidateable() { $response = new Response('', 200, array('Last-Modified' => $this->createDateTimeOneHourAgo()->format(DATE_RFC2822))); @@ -361,11 +418,44 @@ public function testPrepareRemovesContentForHeadRequests() $response = new Response('foo'); $request = Request::create('/', 'HEAD'); + $length = 12345; + $response->headers->set('Content-Length', $length); + $response->prepare($request); + + $this->assertEquals('', $response->getContent()); + $this->assertEquals($length, $response->headers->get('Content-Length'), 'Content-Length should be as if it was GET; see RFC2616 14.13'); + } + + public function testPrepareRemovesContentForInformationalResponse() + { + $response = new Response('foo'); + $request = Request::create('/'); + + $response->setContent('content'); + $response->setStatusCode(101); $response->prepare($request); + $this->assertEquals('', $response->getContent()); + $response->setContent('content'); + $response->setStatusCode(304); + $response->prepare($request); $this->assertEquals('', $response->getContent()); } + public function testPrepareRemovesContentLength() + { + $response = new Response('foo'); + $request = Request::create('/'); + + $response->headers->set('Content-Length', 12345); + $response->prepare($request); + $this->assertEquals(12345, $response->headers->get('Content-Length')); + + $response->headers->set('Transfer-Encoding', 'chunked'); + $response->prepare($request); + $this->assertFalse($response->headers->has('Content-Length')); + } + public function testPrepareSetsPragmaOnHttp10Only() { $request = Request::create('/', 'GET'); @@ -668,7 +758,7 @@ public function testSettersAreChainable() public function validContentProvider() { return array( - 'obj' => array(new StringableObject), + 'obj' => array(new StringableObject()), 'string' => array('Foo'), 'int' => array(2), ); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcacheSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcacheSessionHandlerTest.php index da0440d4289e8..8d38ab3de991e 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcacheSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcacheSessionHandlerTest.php @@ -121,4 +121,12 @@ public function getOptionFixtures() array(array('expiretime' => 100, 'foo' => 'bar'), false), ); } + + public function testGetConnection() + { + $method = new \ReflectionMethod($this->storage, 'getMemcache'); + $method->setAccessible(true); + + $this->assertInstanceOf('\Memcache', $method->invoke($this->storage)); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 985fae4888771..72be3272526f6 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -116,4 +116,12 @@ public function getOptionFixtures() array(array('expiretime' => 100, 'foo' => 'bar'), false), ); } + + public function testGetConnection() + { + $method = new \ReflectionMethod($this->storage, 'getMemcached'); + $method->setAccessible(true); + + $this->assertInstanceOf('\Memcached', $method->invoke($this->storage)); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php index d168ce6379c8a..d907ce4a9031d 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MongoDbSessionHandlerTest.php @@ -31,7 +31,7 @@ protected function setUp() $this->markTestSkipped('MongoDbSessionHandler requires the PHP "mongo" extension.'); } - $mongoClass = (version_compare(phpversion('mongo'), '1.3.0', '<')) ? 'Mongo' : 'MongoClient'; + $mongoClass = version_compare(phpversion('mongo'), '1.3.0', '<') ? 'Mongo' : 'MongoClient'; $this->mongo = $this->getMockBuilder($mongoClass) ->disableOriginalConstructor() @@ -168,4 +168,14 @@ public function testGc() $this->assertTrue($this->storage->gc(-1)); } + + public function testGetConnection() + { + $method = new \ReflectionMethod($this->storage, 'getMongo'); + $method->setAccessible(true); + + $mongoClass = (version_compare(phpversion('mongo'), '1.3.0', '<')) ? '\Mongo' : '\MongoClient'; + + $this->assertInstanceOf($mongoClass, $method->invoke($this->storage)); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 1abf3844c6fdf..06da0096f3c47 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -98,4 +98,14 @@ public function testSessionGC() $storage->gc(-1); $this->assertEquals(0, count($this->pdo->query('SELECT * FROM sessions')->fetchAll())); } + + public function testGetConnection() + { + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions'), array()); + + $method = new \ReflectionMethod($storage, 'getConnection'); + $method->setAccessible(true); + + $this->assertInstanceOf('\PDO', $method->invoke($storage)); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php new file mode 100644 index 0000000000000..dcfd94bd1ecfb --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler; + +/** + * @author Adrien Brault + */ +class WriteCheckSessionHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function test() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('close') + ->with() + ->will($this->returnValue(true)) + ; + + $this->assertEquals(true, $writeCheckSessionHandler->close()); + } + + public function testWrite() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('write') + ->with('foo', 'bar') + ->will($this->returnValue(true)) + ; + + $this->assertEquals(true, $writeCheckSessionHandler->write('foo', 'bar')); + } + + public function testSkippedWrite() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('read') + ->with('foo') + ->will($this->returnValue('bar')) + ; + + $wrappedSessionHandlerMock + ->expects($this->never()) + ->method('write') + ; + + $this->assertEquals('bar', $writeCheckSessionHandler->read('foo')); + $this->assertEquals(true, $writeCheckSessionHandler->write('foo', 'bar')); + } + + public function testNonSkippedWrite() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('read') + ->with('foo') + ->will($this->returnValue('bar')) + ; + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('write') + ->with('foo', 'baZZZ') + ->will($this->returnValue(true)) + ; + + $this->assertEquals('bar', $writeCheckSessionHandler->read('foo')); + $this->assertEquals(true, $writeCheckSessionHandler->write('foo', 'baZZZ')); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php index ef70281ce08d3..c502c38ae613e 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php @@ -43,27 +43,23 @@ protected function tearDown() public function testInitialize() { - $p = new \ReflectionProperty('Symfony\Component\HttpFoundation\Session\Storage\MetadataBag', 'meta'); - $p->setAccessible(true); + $sessionMetadata = array(); $bag1 = new MetadataBag(); - $array = array(); - $bag1->initialize($array); + $bag1->initialize($sessionMetadata); $this->assertGreaterThanOrEqual(time(), $bag1->getCreated()); $this->assertEquals($bag1->getCreated(), $bag1->getLastUsed()); sleep(1); $bag2 = new MetadataBag(); - $array2 = $p->getValue($bag1); - $bag2->initialize($array2); + $bag2->initialize($sessionMetadata); $this->assertEquals($bag1->getCreated(), $bag2->getCreated()); $this->assertEquals($bag1->getLastUsed(), $bag2->getLastUsed()); $this->assertEquals($bag2->getCreated(), $bag2->getLastUsed()); sleep(1); $bag3 = new MetadataBag(); - $array3 = $p->getValue($bag2); - $bag3->initialize($array3); + $bag3->initialize($sessionMetadata); $this->assertEquals($bag1->getCreated(), $bag3->getCreated()); $this->assertGreaterThan($bag2->getLastUsed(), $bag3->getLastUsed()); $this->assertNotEquals($bag3->getCreated(), $bag3->getLastUsed()); @@ -104,4 +100,36 @@ public function testClear() { $this->bag->clear(); } + + public function testSkipLastUsedUpdate() + { + $bag = new MetadataBag('', 30); + $timeStamp = time(); + + $created = $timeStamp - 15; + $sessionMetadata = array( + MetadataBag::CREATED => $created, + MetadataBag::UPDATED => $created, + MetadataBag::LIFETIME => 1000 + ); + $bag->initialize($sessionMetadata); + + $this->assertEquals($created, $sessionMetadata[MetadataBag::UPDATED]); + } + + public function testDoesNotSkipLastUsedUpdate() + { + $bag = new MetadataBag('', 30); + $timeStamp = time(); + + $created = $timeStamp - 45; + $sessionMetadata = array( + MetadataBag::CREATED => $created, + MetadataBag::UPDATED => $created, + MetadataBag::LIFETIME => 1000 + ); + $bag->initialize($sessionMetadata); + + $this->assertEquals($timeStamp, $sessionMetadata[MetadataBag::UPDATED]); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php index c7f5ab8fa5a86..691ee1459347a 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/NativeSessionStorageTest.php @@ -59,7 +59,7 @@ protected function tearDown() protected function getStorage(array $options = array()) { $storage = new NativeSessionStorage($options); - $storage->registerBag(new AttributeBag); + $storage->registerBag(new AttributeBag()); return $storage; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php index cad917e62ad5d..0510f3fe9a5a8 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/PhpBridgeSessionStorageTest.php @@ -53,7 +53,7 @@ protected function tearDown() protected function getStorage() { $storage = new PhpBridgeSessionStorage(); - $storage->registerBag(new AttributeBag); + $storage->registerBag(new AttributeBag()); return $storage; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/AbstractProxyTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/AbstractProxyTest.php index 6b8bba0d5b0d3..58b632aeb0e50 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/AbstractProxyTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/AbstractProxyTest.php @@ -22,7 +22,7 @@ class ConcreteProxy extends AbstractProxy class ConcreteSessionHandlerInterfaceProxy extends AbstractProxy implements \SessionHandlerInterface { - public function open($savePath, $sessionName) + public function open($savePath, $sessionName) { } diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index f77e08ebd6ace..805268fb04176 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -26,7 +26,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/HttpKernel/Bundle/Bundle.php b/src/Symfony/Component/HttpKernel/Bundle/Bundle.php index 6c96c786bed23..2fdbe1ccd0faa 100644 --- a/src/Symfony/Component/HttpKernel/Bundle/Bundle.php +++ b/src/Symfony/Component/HttpKernel/Bundle/Bundle.php @@ -29,8 +29,8 @@ abstract class Bundle extends ContainerAware implements BundleInterface { protected $name; - protected $reflected; protected $extension; + protected $path; /** * Boots the Bundle. @@ -109,11 +109,9 @@ public function getContainerExtension() */ public function getNamespace() { - if (null === $this->reflected) { - $this->reflected = new \ReflectionObject($this); - } + $class = get_class($this); - return $this->reflected->getNamespaceName(); + return substr($class, 0, strrpos($class, '\\')); } /** @@ -125,11 +123,12 @@ public function getNamespace() */ public function getPath() { - if (null === $this->reflected) { - $this->reflected = new \ReflectionObject($this); + if (null === $this->path) { + $reflected = new \ReflectionObject($this); + $this->path = dirname($reflected->getFileName()); } - return dirname($this->reflected->getFileName()); + return $this->path; } /** @@ -188,7 +187,14 @@ public function registerCommands(Application $application) if ($relativePath = $file->getRelativePath()) { $ns .= '\\'.strtr($relativePath, '/', '\\'); } - $r = new \ReflectionClass($ns.'\\'.$file->getBasename('.php')); + $class = $ns.'\\'.$file->getBasename('.php'); + if ($this->container) { + $alias = 'console.command.'.strtolower(str_replace('\\', '_', $class)); + if ($this->container->has($alias)) { + continue; + } + } + $r = new \ReflectionClass($class); if ($r->isSubclassOf('Symfony\\Component\\Console\\Command\\Command') && !$r->isAbstract() && !$r->getConstructor()->getNumberOfRequiredParameters()) { $application->add($r->newInstance()); } diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index c06dd3fa57e07..2b2e827d979ea 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +2.5.0 +----- + + * deprecated `Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass`, use `Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass` instead + +2.4.0 +----- + + * added event listeners for the session + * added the KernelEvents::FINISH_REQUEST event + 2.3.0 ----- diff --git a/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php b/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php index eb26ac59e62dc..bd96057ded64c 100644 --- a/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php +++ b/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php @@ -18,13 +18,14 @@ */ class CacheWarmerAggregate implements CacheWarmerInterface { - protected $warmers; - protected $optionalsEnabled; + protected $warmers = array(); + protected $optionalsEnabled = false; public function __construct(array $warmers = array()) { - $this->setWarmers($warmers); - $this->optionalsEnabled = false; + foreach ($warmers as $warmer) { + $this->add($warmer); + } } public function enableOptionalWarmers() diff --git a/src/Symfony/Component/HttpKernel/Client.php b/src/Symfony/Component/HttpKernel/Client.php index a663350bf1e6c..b40d7006d696c 100644 --- a/src/Symfony/Component/HttpKernel/Client.php +++ b/src/Symfony/Component/HttpKernel/Client.php @@ -42,11 +42,11 @@ class Client extends BaseClient */ public function __construct(HttpKernelInterface $kernel, array $server = array(), History $history = null, CookieJar $cookieJar = null) { + // These class properties must be set before calling the parent constructor, as it may depend on it. $this->kernel = $kernel; + $this->followRedirects = false; parent::__construct($server, $history, $cookieJar); - - $this->followRedirects = false; } /** diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php index 047ade1062cad..46484498474c1 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php @@ -40,18 +40,11 @@ public function __construct(LoggerInterface $logger = null) } /** - * Returns the Controller instance associated with a Request. + * {@inheritdoc} * * This method looks for a '_controller' request attribute that represents * the controller name (a string like ClassName::MethodName). * - * @param Request $request A Request instance - * - * @return mixed|Boolean A PHP callable representing the Controller, - * or false if this resolver is not able to determine the controller - * - * @throws \InvalidArgumentException|\LogicException If the controller can't be found - * * @api */ public function getController(Request $request) @@ -86,14 +79,7 @@ public function getController(Request $request) } /** - * Returns the arguments to pass to the controller. - * - * @param Request $request A Request instance - * @param mixed $controller A PHP callable - * - * @return array - * - * @throws \RuntimeException When value for argument given is not provided + * {@inheritdoc} * * @api */ diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php index f58f50db53292..6f805ed2dab77 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php @@ -38,10 +38,10 @@ interface ControllerResolverInterface * * @param Request $request A Request instance * - * @return mixed|Boolean A PHP callable representing the Controller, - * or false if this resolver is not able to determine the controller + * @return callable|false A PHP callable representing the Controller, + * or false if this resolver is not able to determine the controller * - * @throws \InvalidArgumentException|\LogicException If the controller can't be found + * @throws \LogicException If the controller can't be found * * @api */ @@ -50,8 +50,8 @@ public function getController(Request $request); /** * Returns the arguments to pass to the controller. * - * @param Request $request A Request instance - * @param mixed $controller A PHP callable + * @param Request $request A Request instance + * @param callable $controller A PHP callable * * @return array An array of arguments to pass to the controller * diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php index 7d9c28943b72f..1835938a07263 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php @@ -11,17 +11,25 @@ namespace Symfony\Component\HttpKernel\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; + /** * DataCollector. * * Children of this class must store the collected data in the data property. * * @author Fabien Potencier + * @author Bernhard Schussek */ abstract class DataCollector implements DataCollectorInterface, \Serializable { protected $data; + /** + * @var ValueExporter + */ + private $valueExporter; + public function serialize() { return serialize($this->data); @@ -41,35 +49,10 @@ public function unserialize($data) */ protected function varToString($var) { - if (is_object($var)) { - return sprintf('Object(%s)', get_class($var)); - } - - if (is_array($var)) { - $a = array(); - foreach ($var as $k => $v) { - $a[] = sprintf('%s => %s', $k, $this->varToString($v)); - } - - return sprintf("Array(%s)", implode(', ', $a)); - } - - if (is_resource($var)) { - return sprintf('Resource(%s)', get_resource_type($var)); - } - - if (null === $var) { - return 'null'; - } - - if (false === $var) { - return 'false'; - } - - if (true === $var) { - return 'true'; + if (null === $this->valueExporter) { + $this->valueExporter = new ValueExporter(); } - return (string) $var; + return $this->valueExporter->exportValue($var); } } diff --git a/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php index cd7f7870177c2..418ed686c64d8 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface; /** @@ -20,8 +21,15 @@ * * @author Fabien Potencier */ -class EventDataCollector extends DataCollector +class EventDataCollector extends DataCollector implements LateDataCollectorInterface { + protected $dispatcher; + + public function __construct(EventDispatcherInterface $dispatcher = null) + { + $this->dispatcher = $dispatcher; + } + /** * {@inheritdoc} */ @@ -33,6 +41,14 @@ public function collect(Request $request, Response $response, \Exception $except ); } + public function lateCollect() + { + if ($this->dispatcher instanceof TraceableEventDispatcherInterface) { + $this->setCalledListeners($this->dispatcher->getCalledListeners()); + $this->setNotCalledListeners($this->dispatcher->getNotCalledListeners()); + } + } + /** * Sets the called listeners. * diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LateDataCollectorInterface.php b/src/Symfony/Component/HttpKernel/DataCollector/LateDataCollectorInterface.php new file mode 100644 index 0000000000000..012332de479f7 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/DataCollector/LateDataCollectorInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\DataCollector; + +/** + * LateDataCollectorInterface. + * + * @author Fabien Potencier + */ +interface LateDataCollectorInterface +{ + /** + * Collects data as late as possible. + */ + public function lateCollect(); +} diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index f08720e8073bb..ba2ea44c5ed32 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -21,7 +21,7 @@ * * @author Fabien Potencier */ -class LoggerDataCollector extends DataCollector +class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface { private $logger; @@ -36,6 +36,14 @@ public function __construct($logger = null) * {@inheritdoc} */ public function collect(Request $request, Response $response, \Exception $exception = null) + { + // everything is done as late as possible + } + + /** + * {@inheritdoc} + */ + public function lateCollect() { if (null !== $this->logger) { $this->data = array( diff --git a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php index 7c84bb93c34f9..e36f1f457e7b1 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php @@ -19,7 +19,7 @@ * * @author Fabien Potencier */ -class MemoryDataCollector extends DataCollector +class MemoryDataCollector extends DataCollector implements LateDataCollectorInterface { public function __construct() { @@ -37,6 +37,14 @@ public function collect(Request $request, Response $response, \Exception $except $this->updateMemoryUsage(); } + /** + * {@inheritdoc} + */ + public function lateCollect() + { + $this->updateMemoryUsage(); + } + /** * Gets the memory. * diff --git a/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php index 0d6fe02b98d55..4b5b00f0bf369 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/TimeDataCollector.php @@ -11,22 +11,24 @@ namespace Symfony\Component\HttpKernel\DataCollector; -use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\KernelInterface; /** * TimeDataCollector. * * @author Fabien Potencier */ -class TimeDataCollector extends DataCollector +class TimeDataCollector extends DataCollector implements LateDataCollectorInterface { protected $kernel; + protected $stopwatch; - public function __construct(KernelInterface $kernel = null) + public function __construct(KernelInterface $kernel = null, $stopwatch = null) { $this->kernel = $kernel; + $this->stopwatch = $stopwatch; } /** @@ -41,11 +43,23 @@ public function collect(Request $request, Response $response, \Exception $except } $this->data = array( + 'token' => $response->headers->get('X-Debug-Token'), 'start_time' => $startTime * 1000, 'events' => array(), ); } + /** + * {@inheritdoc} + */ + public function lateCollect() + { + if (null !== $this->stopwatch && isset($this->data['token'])) { + $this->setEvents($this->stopwatch->getSectionEvents($this->data['token'])); + } + unset($this->data['token']); + } + /** * Sets the request events. * diff --git a/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php new file mode 100644 index 0000000000000..f3aeb80cb227c --- /dev/null +++ b/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.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\DataCollector\Util; + +/** + * @author Bernhard Schussek + */ +class ValueExporter +{ + /** + * Converts a PHP value to a string. + * + * @param mixed $value The PHP value + * + * @return string The string representation of the given value + */ + public function exportValue($value) + { + if (is_object($value)) { + return sprintf('Object(%s)', get_class($value)); + } + + if (is_array($value)) { + $a = array(); + foreach ($value as $k => $v) { + $a[] = sprintf('%s => %s', $k, $this->exportValue($v)); + } + + return sprintf("Array(%s)", implode(', ', $a)); + } + + if (is_resource($value)) { + return sprintf('Resource(%s)', get_resource_type($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/HttpKernel/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php index 3c15abf626ff4..ed2417be721b0 100644 --- a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php @@ -11,16 +11,10 @@ namespace Symfony\Component\HttpKernel\Debug; -use Symfony\Component\Stopwatch\Stopwatch; -use Symfony\Component\HttpKernel\KernelEvents; -use Psr\Log\LoggerInterface; -use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher as BaseTraceableEventDispatcher; use Symfony\Component\HttpKernel\Profiler\Profiler; -use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface; /** * Collects some data about event listeners. @@ -29,356 +23,28 @@ * * @author Fabien Potencier */ -class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEventDispatcherInterface +class TraceableEventDispatcher extends BaseTraceableEventDispatcher { - private $logger; - private $called; - private $stopwatch; - private $profiler; - private $dispatcher; - private $wrappedListeners; - private $firstCalledEvent; - private $id; - private $lastEventId = 0; - - /** - * Constructor. - * - * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance - * @param Stopwatch $stopwatch A Stopwatch instance - * @param LoggerInterface $logger A LoggerInterface instance - */ - public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null) - { - $this->dispatcher = $dispatcher; - $this->stopwatch = $stopwatch; - $this->logger = $logger; - $this->called = array(); - $this->wrappedListeners = array(); - $this->firstCalledEvent = array(); - } - /** * Sets the profiler. * + * The traceable event dispatcher does not use the profiler anymore. + * The job is now done directly by the Profiler listener and the + * data collectors themselves. + * * @param Profiler|null $profiler A Profiler instance + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function setProfiler(Profiler $profiler = null) { - $this->profiler = $profiler; - } - - /** - * {@inheritDoc} - */ - public function addListener($eventName, $listener, $priority = 0) - { - $this->dispatcher->addListener($eventName, $listener, $priority); - } - - /** - * {@inheritdoc} - */ - public function addSubscriber(EventSubscriberInterface $subscriber) - { - $this->dispatcher->addSubscriber($subscriber); - } - - /** - * {@inheritdoc} - */ - public function removeListener($eventName, $listener) - { - return $this->dispatcher->removeListener($eventName, $listener); - } - - /** - * {@inheritdoc} - */ - public function removeSubscriber(EventSubscriberInterface $subscriber) - { - return $this->dispatcher->removeSubscriber($subscriber); - } - - /** - * {@inheritdoc} - */ - public function getListeners($eventName = null) - { - return $this->dispatcher->getListeners($eventName); - } - - /** - * {@inheritdoc} - */ - public function hasListeners($eventName = null) - { - return $this->dispatcher->hasListeners($eventName); } /** * {@inheritdoc} */ - public function dispatch($eventName, Event $event = null) - { - if (null === $event) { - $event = new Event(); - } - - $this->id = $eventId = ++$this->lastEventId; - - $this->preDispatch($eventName, $event); - - $e = $this->stopwatch->start($eventName, 'section'); - - $this->firstCalledEvent[$eventName] = $this->stopwatch->start($eventName.'.loading', 'event_listener_loading'); - - if (!$this->dispatcher->hasListeners($eventName)) { - $this->firstCalledEvent[$eventName]->stop(); - } - - $this->dispatcher->dispatch($eventName, $event); - - // reset the id as another event might have been dispatched during the dispatching of this event - $this->id = $eventId; - - unset($this->firstCalledEvent[$eventName]); - - $e->stop(); - - $this->postDispatch($eventName, $event); - - return $event; - } - - /** - * {@inheritDoc} - */ - public function getCalledListeners() - { - return $this->called; - } - - /** - * {@inheritDoc} - */ - public function getNotCalledListeners() + protected function preDispatch($eventName, Event $event) { - $notCalled = array(); - - foreach ($this->getListeners() as $name => $listeners) { - foreach ($listeners as $listener) { - $info = $this->getListenerInfo($listener, $name); - if (!isset($this->called[$name.'.'.$info['pretty']])) { - $notCalled[$name.'.'.$info['pretty']] = $info; - } - } - } - - return $notCalled; - } - - /** - * Proxies all method calls to the original event dispatcher. - * - * @param string $method The method name - * @param array $arguments The method arguments - * - * @return mixed - */ - public function __call($method, $arguments) - { - return call_user_func_array(array($this->dispatcher, $method), $arguments); - } - - /** - * This is a private method and must not be used. - * - * This method is public because it is used in a closure. - * Whenever Symfony will require PHP 5.4, this could be changed - * to a proper private method. - */ - public function logSkippedListeners($eventName, Event $event, $listener) - { - if (null === $this->logger) { - return; - } - - $info = $this->getListenerInfo($listener, $eventName); - - $this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName)); - - $skippedListeners = $this->getListeners($eventName); - $skipped = false; - - foreach ($skippedListeners as $skippedListener) { - $skippedListener = $this->unwrapListener($skippedListener); - - if ($skipped) { - $info = $this->getListenerInfo($skippedListener, $eventName); - $this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName)); - } - - if ($skippedListener === $listener) { - $skipped = true; - } - } - } - - /** - * This is a private method. - * - * This method is public because it is used in a closure. - * Whenever Symfony will require PHP 5.4, this could be changed - * to a proper private method. - */ - public function preListenerCall($eventName, $listener) - { - // is it the first called listener? - if (isset($this->firstCalledEvent[$eventName])) { - $this->firstCalledEvent[$eventName]->stop(); - - unset($this->firstCalledEvent[$eventName]); - } - - $info = $this->getListenerInfo($listener, $eventName); - - if (null !== $this->logger) { - $this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty'])); - } - - $this->called[$eventName.'.'.$info['pretty']] = $info; - - return $this->stopwatch->start(isset($info['class']) ? $info['class'] : $info['type'], 'event_listener'); - } - - /** - * Returns information about the listener - * - * @param object $listener The listener - * @param string $eventName The event name - * - * @return array Information about the listener - */ - private function getListenerInfo($listener, $eventName) - { - $listener = $this->unwrapListener($listener); - - $info = array( - 'event' => $eventName, - ); - if ($listener instanceof \Closure) { - $info += array( - 'type' => 'Closure', - 'pretty' => 'closure' - ); - } elseif (is_string($listener)) { - try { - $r = new \ReflectionFunction($listener); - $file = $r->getFileName(); - $line = $r->getStartLine(); - } catch (\ReflectionException $e) { - $file = null; - $line = null; - } - $info += array( - 'type' => 'Function', - 'function' => $listener, - 'file' => $file, - 'line' => $line, - 'pretty' => $listener, - ); - } elseif (is_array($listener) || (is_object($listener) && is_callable($listener))) { - if (!is_array($listener)) { - $listener = array($listener, '__invoke'); - } - $class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0]; - try { - $r = new \ReflectionMethod($class, $listener[1]); - $file = $r->getFileName(); - $line = $r->getStartLine(); - } catch (\ReflectionException $e) { - $file = null; - $line = null; - } - $info += array( - 'type' => 'Method', - 'class' => $class, - 'method' => $listener[1], - 'file' => $file, - 'line' => $line, - 'pretty' => $class.'::'.$listener[1], - ); - } - - return $info; - } - - /** - * Updates the stopwatch data in the profile hierarchy. - * - * @param string $token Profile token - * @param Boolean $updateChildren Whether to update the children altogether - */ - private function updateProfiles($token, $updateChildren) - { - if (!$this->profiler || !$profile = $this->profiler->loadProfile($token)) { - return; - } - - $this->saveInfoInProfile($profile, $updateChildren); - } - - /** - * Update the profiles with the timing and events information and saves them. - * - * @param Profile $profile The root profile - * @param Boolean $updateChildren Whether to update the children altogether - */ - private function saveInfoInProfile(Profile $profile, $updateChildren) - { - try { - $collector = $profile->getCollector('memory'); - $collector->updateMemoryUsage(); - } catch (\InvalidArgumentException $e) { - } - - try { - $collector = $profile->getCollector('time'); - $collector->setEvents($this->stopwatch->getSectionEvents($profile->getToken())); - } catch (\InvalidArgumentException $e) { - } - - try { - $collector = $profile->getCollector('events'); - $collector->setCalledListeners($this->getCalledListeners()); - $collector->setNotCalledListeners($this->getNotCalledListeners()); - } catch (\InvalidArgumentException $e) { - } - - $this->profiler->saveProfile($profile); - - if ($updateChildren) { - foreach ($profile->getChildren() as $child) { - $this->saveInfoInProfile($child, true); - } - } - } - - private function preDispatch($eventName, Event $event) - { - // wrap all listeners before they are called - $this->wrappedListeners[$this->id] = new \SplObjectStorage(); - - $listeners = $this->dispatcher->getListeners($eventName); - - foreach ($listeners as $listener) { - $this->dispatcher->removeListener($eventName, $listener); - $wrapped = $this->wrapListener($eventName, $listener); - $this->wrappedListeners[$this->id][$wrapped] = $listener; - $this->dispatcher->addListener($eventName, $wrapped); - } - switch ($eventName) { case KernelEvents::REQUEST: $this->stopwatch->openSection(); @@ -392,7 +58,7 @@ private function preDispatch($eventName, Event $event) break; case KernelEvents::TERMINATE: $token = $event->getResponse()->headers->get('X-Debug-Token'); - // There is a very special case when using builtin AppCache class as kernel wrapper, in the case + // There is a very special case when using built-in AppCache class as kernel wrapper, in the case // of an ESI request leading to a `stale` response [B] inside a `fresh` cached response [A]. // In this case, `$token` contains the [B] debug token, but the open `stopwatch` section ID // is equal to the [A] debug token. Trying to reopen section with the [B] token throws an exception @@ -404,7 +70,10 @@ private function preDispatch($eventName, Event $event) } } - private function postDispatch($eventName, Event $event) + /** + * {@inheritdoc} + */ + protected function postDispatch($eventName, Event $event) { switch ($eventName) { case KernelEvents::CONTROLLER: @@ -413,58 +82,15 @@ private function postDispatch($eventName, Event $event) case KernelEvents::RESPONSE: $token = $event->getResponse()->headers->get('X-Debug-Token'); $this->stopwatch->stopSection($token); - if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) { - // The profiles can only be updated once they have been created - // that is after the 'kernel.response' event of the main request - $this->updateProfiles($token, true); - } break; case KernelEvents::TERMINATE: - $token = $event->getResponse()->headers->get('X-Debug-Token'); // In the special case described in the `preDispatch` method above, the `$token` section // does not exist, then closing it throws an exception which must be caught. + $token = $event->getResponse()->headers->get('X-Debug-Token'); try { $this->stopwatch->stopSection($token); } catch (\LogicException $e) {} - // The children profiles have been updated by the previous 'kernel.response' - // event. Only the root profile need to be updated with the 'kernel.terminate' - // timing information. - $this->updateProfiles($token, false); break; } - - foreach ($this->wrappedListeners[$this->id] as $wrapped) { - $this->dispatcher->removeListener($eventName, $wrapped); - $this->dispatcher->addListener($eventName, $this->wrappedListeners[$this->id][$wrapped]); - } - - unset($this->wrappedListeners[$this->id]); - } - - private function wrapListener($eventName, $listener) - { - $self = $this; - - return function (Event $event) use ($self, $eventName, $listener) { - $e = $self->preListenerCall($eventName, $listener); - - call_user_func($listener, $event); - - $e->stop(); - - if ($event->isPropagationStopped()) { - $self->logSkippedListeners($eventName, $event, $listener); - } - }; - } - - private function unwrapListener($listener) - { - // get the original listener - if (is_object($listener) && isset($this->wrappedListeners[$this->id][$listener])) { - return $this->wrappedListeners[$this->id][$listener]; - } - - return $listener; } } diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php index c9b8a211d4291..69e7937d18c11 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ContainerAwareHttpKernel.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; @@ -35,10 +36,11 @@ class ContainerAwareHttpKernel extends HttpKernel * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance * @param ContainerInterface $container A ContainerInterface instance * @param ControllerResolverInterface $controllerResolver A ControllerResolverInterface instance + * @param RequestStack $requestStack A stack for master/sub requests */ - public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $controllerResolver) + public function __construct(EventDispatcherInterface $dispatcher, ContainerInterface $container, ControllerResolverInterface $controllerResolver, RequestStack $requestStack = null) { - parent::__construct($dispatcher, $controllerResolver); + parent::__construct($dispatcher, $controllerResolver, $requestStack); $this->container = $container; diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterListenersPass.php index 6c56634216920..0e14e91652b9a 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterListenersPass.php @@ -11,96 +11,13 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass as BaseRegisterListenersPass; /** * Compiler pass to register tagged services for an event dispatcher. + * + * @deprecated Deprecated in 2.5, to be removed in 3.0. Use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass instead. */ -class RegisterListenersPass implements CompilerPassInterface +class RegisterListenersPass extends BaseRegisterListenersPass { - /** - * @var string - */ - protected $dispatcherService; - - /** - * @var string - */ - protected $listenerTag; - - /** - * @var string - */ - protected $subscriberTag; - - /** - * Constructor. - * - * @param string $dispatcherService Service name of the event dispatcher in processed container - * @param string $listenerTag Tag name used for listener - * @param string $subscriberTag Tag name used for subscribers - */ - public function __construct($dispatcherService = 'event_dispatcher', $listenerTag = 'kernel.event_listener', $subscriberTag = 'kernel.event_subscriber') - { - $this->dispatcherService = $dispatcherService; - $this->listenerTag = $listenerTag; - $this->subscriberTag = $subscriberTag; - } - - public function process(ContainerBuilder $container) - { - if (!$container->hasDefinition($this->dispatcherService)) { - return; - } - - $definition = $container->getDefinition($this->dispatcherService); - - foreach ($container->findTaggedServiceIds($this->listenerTag) as $id => $events) { - $def = $container->getDefinition($id); - if (!$def->isPublic()) { - throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event listeners are lazy-loaded.', $id)); - } - - if ($def->isAbstract()) { - throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as event listeners are lazy-loaded.', $id)); - } - - foreach ($events as $event) { - $priority = isset($event['priority']) ? $event['priority'] : 0; - - if (!isset($event['event'])) { - throw new \InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); - } - - if (!isset($event['method'])) { - $event['method'] = 'on'.preg_replace_callback(array( - '/(?<=\b)[a-z]/i', - '/[^a-z0-9]/i', - ), function ($matches) { return strtoupper($matches[0]); }, $event['event']); - $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); - } - - $definition->addMethodCall('addListenerService', array($event['event'], array($id, $event['method']), $priority)); - } - } - - foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) { - $def = $container->getDefinition($id); - if (!$def->isPublic()) { - throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event subscribers are lazy-loaded.', $id)); - } - - // We must assume that the class value has been correctly filled, even if the service is created by a factory - $class = $def->getClass(); - - $refClass = new \ReflectionClass($class); - $interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface'; - if (!$refClass->implementsInterface($interface)) { - throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface)); - } - - $definition->addMethodCall('addSubscriberService', array($id, $class)); - } - } } diff --git a/src/Symfony/Component/HttpKernel/Event/FinishRequestEvent.php b/src/Symfony/Component/HttpKernel/Event/FinishRequestEvent.php new file mode 100644 index 0000000000000..ee724843cd843 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Event/FinishRequestEvent.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\HttpKernel\Event; + +/** + * Triggered whenever a request is fully processed. + * + * @author Benjamin Eberlei + */ +class FinishRequestEvent extends KernelEvent +{ +} diff --git a/src/Symfony/Component/HttpKernel/Event/KernelEvent.php b/src/Symfony/Component/HttpKernel/Event/KernelEvent.php index e57eed4d16f24..98763253daddc 100644 --- a/src/Symfony/Component/HttpKernel/Event/KernelEvent.php +++ b/src/Symfony/Component/HttpKernel/Event/KernelEvent.php @@ -86,4 +86,16 @@ public function getRequestType() { return $this->requestType; } + + /** + * Checks if this is a master request. + * + * @return Boolean True if the request is a master request + * + * @api + */ + public function isMasterRequest() + { + return HttpKernelInterface::MASTER_REQUEST === $this->requestType; + } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php b/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php index b90562b5d386c..686778afc4ad8 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/EsiListener.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpKernel\EventListener; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\HttpCache\Esi; @@ -43,7 +42,7 @@ public function __construct(Esi $esi = null) */ public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType() || null === $this->esi) { + if (!$event->isMasterRequest() || null === $this->esi) { return; } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index 55950b29edace..33ce993b079c4 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -51,18 +52,7 @@ public function onKernelException(GetResponseForExceptionEvent $event) $this->logException($exception, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine())); - $attributes = array( - '_controller' => $this->controller, - 'exception' => FlattenException::create($exception), - 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, - // keep for BC -- as $format can be an argument of the controller callable - // see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php - // @deprecated in 2.4, to be removed in 3.0 - 'format' => $request->getRequestFormat(), - ); - - $request = $request->duplicate(null, null, $attributes); - $request->setMethod('GET'); + $request = $this->duplicateRequest($exception, $request); try { $response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, true); @@ -109,4 +99,29 @@ protected function logException(\Exception $exception, $message, $original = tru error_log($message); } } + + /** + * Clones the request for the exception. + * + * @param \Exception $exception The thrown exception. + * @param Request $request The original request. + * + * @return Request $request The cloned request. + */ + protected function duplicateRequest(\Exception $exception, Request $request) + { + $attributes = array( + '_controller' => $this->controller, + 'exception' => FlattenException::create($exception), + 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, + // keep for BC -- as $format can be an argument of the controller callable + // see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php + // @deprecated in 2.4, to be removed in 3.0 + 'format' => $request->getRequestFormat(), + ); + $request = $request->duplicate(null, null, $attributes); + $request->setMethod('GET'); + + return $request; + } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php index 0b864c02f2bc4..bdcf4c7644a76 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php @@ -12,7 +12,9 @@ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RequestContextAwareInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -20,32 +22,48 @@ /** * Initializes the locale based on the current request. * + * This listener works in 2 modes: + * + * * 2.3 compatibility mode where you must call setRequest whenever the Request changes. + * * 2.4+ mode where you must pass a RequestStack instance in the constructor. + * * @author Fabien Potencier */ class LocaleListener implements EventSubscriberInterface { private $router; private $defaultLocale; + private $requestStack; - public function __construct($defaultLocale = 'en', RequestContextAwareInterface $router = null) + /** + * RequestStack will become required in 3.0. + */ + public function __construct($defaultLocale = 'en', RequestContextAwareInterface $router = null, RequestStack $requestStack = null) { $this->defaultLocale = $defaultLocale; + $this->requestStack = $requestStack; $this->router = $router; } + /** + * Sets the current Request. + * + * This method was used to synchronize the Request, but as the HttpKernel + * is doing that automatically now, you should never call it directly. + * It is kept public for BC with the 2.3 version. + * + * @param Request|null $request A Request instance + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. + */ public function setRequest(Request $request = null) { if (null === $request) { return; } - if ($locale = $request->attributes->get('_locale')) { - $request->setLocale($locale); - } - - if (null !== $this->router) { - $this->router->getContext()->setParameter('_locale', $request->getLocale()); - } + $this->setLocale($request); + $this->setRouterContext($request); } public function onKernelRequest(GetResponseEvent $event) @@ -53,7 +71,33 @@ public function onKernelRequest(GetResponseEvent $event) $request = $event->getRequest(); $request->setDefaultLocale($this->defaultLocale); - $this->setRequest($request); + $this->setLocale($request); + $this->setRouterContext($request); + } + + public function onKernelFinishRequest(FinishRequestEvent $event) + { + if (null === $this->requestStack) { + return; // removed when requestStack is required + } + + if (null !== $parentRequest = $this->requestStack->getParentRequest()) { + $this->setRouterContext($parentRequest); + } + } + + private function setLocale(Request $request) + { + if ($locale = $request->attributes->get('_locale')) { + $request->setLocale($locale); + } + } + + private function setRouterContext(Request $request) + { + if (null !== $this->router) { + $this->router->getContext()->setParameter('_locale', $request->getLocale()); + } } public static function getSubscribedEvents() @@ -61,6 +105,7 @@ public static function getSubscribedEvents() return array( // must be registered after the Router to have access to the _locale KernelEvents::REQUEST => array(array('onKernelRequest', 16)), + KernelEvents::FINISH_REQUEST => array(array('onKernelFinishRequest', 0)), ); } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php index 30d4011ffe86f..995c998b6ff13 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php @@ -11,14 +11,14 @@ namespace Symfony\Component\HttpKernel\EventListener; -use Symfony\Component\HttpKernel\HttpKernelInterface; 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\Profile; use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\HttpFoundation\RequestMatcherInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -33,9 +33,10 @@ class ProfilerListener implements EventSubscriberInterface protected $onlyException; protected $onlyMasterRequests; protected $exception; - protected $children; - protected $requests; + protected $requests = array(); protected $profiles; + protected $requestStack; + protected $parents; /** * Constructor. @@ -45,14 +46,15 @@ class ProfilerListener implements EventSubscriberInterface * @param Boolean $onlyException true if the profiler only collects data when an exception occurs, false otherwise * @param Boolean $onlyMasterRequests true if the profiler only collects data when the request is a master request, false otherwise */ - public function __construct(Profiler $profiler, RequestMatcherInterface $matcher = null, $onlyException = false, $onlyMasterRequests = false) + public function __construct(Profiler $profiler, RequestMatcherInterface $matcher = null, $onlyException = false, $onlyMasterRequests = false, RequestStack $requestStack = null) { $this->profiler = $profiler; $this->matcher = $matcher; $this->onlyException = (Boolean) $onlyException; $this->onlyMasterRequests = (Boolean) $onlyMasterRequests; - $this->children = new \SplObjectStorage(); - $this->profiles = array(); + $this->profiles = new \SplObjectStorage(); + $this->parents = new \SplObjectStorage(); + $this->requestStack = $requestStack; } /** @@ -62,16 +64,21 @@ public function __construct(Profiler $profiler, RequestMatcherInterface $matcher */ public function onKernelException(GetResponseForExceptionEvent $event) { - if ($this->onlyMasterRequests && HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if ($this->onlyMasterRequests && !$event->isMasterRequest()) { return; } $this->exception = $event->getException(); } + /** + * @deprecated Deprecated since version 2.4, to be removed in 3.0. + */ public function onKernelRequest(GetResponseEvent $event) { - $this->requests[] = $event->getRequest(); + if (null === $this->requestStack) { + $this->requests[] = $event->getRequest(); + } } /** @@ -81,7 +88,7 @@ public function onKernelRequest(GetResponseEvent $event) */ public function onKernelResponse(FilterResponseEvent $event) { - $master = HttpKernelInterface::MASTER_REQUEST === $event->getRequestType(); + $master = $event->isMasterRequest(); if ($this->onlyMasterRequests && !$master) { return; } @@ -102,42 +109,36 @@ public function onKernelResponse(FilterResponseEvent $event) return; } - $this->profiles[] = $profile; + $this->profiles[$request] = $profile; - if (null !== $exception) { - foreach ($this->profiles as $profile) { - $this->profiler->saveProfile($profile); - } - - return; - } - - // keep the profile as the child of its parent - if (!$master) { + if (null !== $this->requestStack) { + $this->parents[$request] = $this->requestStack->getParentRequest(); + } elseif (!$master) { + // to be removed when requestStack is required array_pop($this->requests); - $parent = end($this->requests); - - // when simulating requests, we might not have the parent - if ($parent) { - $profiles = isset($this->children[$parent]) ? $this->children[$parent] : array(); - $profiles[] = $profile; - $this->children[$parent] = $profiles; - } + $this->parents[$request] = end($this->requests); } + } - if (isset($this->children[$request])) { - foreach ($this->children[$request] as $child) { - $profile->addChild($child); + public function onKernelTerminate(PostResponseEvent $event) + { + // attach children to parents + foreach ($this->profiles as $request) { + // isset call should be removed when requestStack is required + if (isset($this->parents[$request]) && null !== $parentRequest = $this->parents[$request]) { + $this->profiles[$parentRequest]->addChild($this->profiles[$request]); } - $this->children[$request] = array(); } - if ($master) { - $this->saveProfiles($profile); - - $this->children = new \SplObjectStorage(); + // save profiles + foreach ($this->profiles as $request) { + $this->profiler->saveProfile($this->profiles[$request]); } + + $this->profiles = new \SplObjectStorage(); + $this->parents = new \SplObjectStorage(); + $this->requests = array(); } public static function getSubscribedEvents() @@ -148,19 +149,7 @@ public static function getSubscribedEvents() KernelEvents::REQUEST => array('onKernelRequest', 1024), KernelEvents::RESPONSE => array('onKernelResponse', -100), KernelEvents::EXCEPTION => 'onKernelException', + KernelEvents::TERMINATE => array('onKernelTerminate', -1024), ); } - - /** - * Saves the profile hierarchy. - * - * @param Profile $profile The root profile - */ - private function saveProfiles(Profile $profile) - { - $this->profiler->saveProfile($profile); - foreach ($profile->getChildren() as $profile) { - $this->saveProfiles($profile); - } - } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php b/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php index 669980cf2b616..eeb2b0fcb2355 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -37,7 +36,7 @@ public function __construct($charset) */ public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } diff --git a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php index 33d340f27a195..27ac632b6faee 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php @@ -13,9 +13,11 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; @@ -28,6 +30,11 @@ /** * Initializes the context from the request and sets request attributes based on a matching route. * + * This listener works in 2 modes: + * + * * 2.3 compatibility mode where you must call setRequest whenever the Request changes. + * * 2.4+ mode where you must pass a RequestStack instance in the constructor. + * * @author Fabien Potencier */ class RouterListener implements EventSubscriberInterface @@ -36,17 +43,20 @@ class RouterListener implements EventSubscriberInterface private $context; private $logger; private $request; + private $requestStack; /** * Constructor. * + * RequestStack will become required in 3.0. + * * @param UrlMatcherInterface|RequestMatcherInterface $matcher The Url or Request matcher * @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface) * @param LoggerInterface|null $logger The logger * * @throws \InvalidArgumentException */ - public function __construct($matcher, RequestContext $context = null, LoggerInterface $logger = null) + public function __construct($matcher, RequestContext $context = null, LoggerInterface $logger = null, RequestStack $requestStack = null) { if (!$matcher instanceof UrlMatcherInterface && !$matcher instanceof RequestMatcherInterface) { throw new \InvalidArgumentException('Matcher must either implement UrlMatcherInterface or RequestMatcherInterface.'); @@ -58,18 +68,20 @@ public function __construct($matcher, RequestContext $context = null, LoggerInte $this->matcher = $matcher; $this->context = $context ?: $matcher->getContext(); + $this->requestStack = $requestStack; $this->logger = $logger; } /** * Sets the current Request. * - * The application should call this method whenever the Request - * object changes (entering a Request scope for instance, but - * also when leaving a Request scope -- especially when they are - * nested). + * This method was used to synchronize the Request, but as the HttpKernel + * is doing that automatically now, you should never call it directly. + * It is kept public for BC with the 2.3 version. * * @param Request|null $request A Request instance + * + * @deprecated Deprecated since version 2.4, to be moved to a private function in 3.0. */ public function setRequest(Request $request = null) { @@ -79,6 +91,15 @@ public function setRequest(Request $request = null) $this->request = $request; } + public function onKernelFinishRequest(FinishRequestEvent $event) + { + if (null === $this->requestStack) { + return; // removed when requestStack is required + } + + $this->setRequest($this->requestStack->getParentRequest()); + } + public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); @@ -86,7 +107,10 @@ public function onKernelRequest(GetResponseEvent $event) // initialize the context that is also used by the generator (assuming matcher and generator share the same context instance) // we call setRequest even if most of the time, it has already been done to keep compatibility // with frameworks which do not use the Symfony service container - $this->setRequest($request); + // when we have a RequestStack, no need to do it + if (null !== $this->requestStack) { + $this->setRequest($request); + } if ($request->attributes->has('_controller')) { // routing is already done @@ -113,6 +137,10 @@ public function onKernelRequest(GetResponseEvent $event) } catch (ResourceNotFoundException $e) { $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo()); + if ($referer = $request->headers->get('referer')) { + $message .= sprintf(' (from "%s")', $referer); + } + throw new NotFoundHttpException($message, $e); } catch (MethodNotAllowedException $e) { $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getPathInfo(), implode(', ', $e->getAllowedMethods())); @@ -135,6 +163,7 @@ public static function getSubscribedEvents() { return array( KernelEvents::REQUEST => array(array('onKernelRequest', 32)), + KernelEvents::FINISH_REQUEST => array(array('onKernelFinishRequest', 0)), ); } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php new file mode 100644 index 0000000000000..d1023b297c0fa --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/SessionListener.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\HttpKernel\EventListener; + +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Sets the session in the request. + * + * @author Johannes M. Schmitt + */ +abstract class SessionListener implements EventSubscriberInterface +{ + public function onKernelRequest(GetResponseEvent $event) + { + if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + return; + } + + $request = $event->getRequest(); + $session = $this->getSession(); + if (null === $session || $request->hasSession()) { + return; + } + + $request->setSession($session); + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => array('onKernelRequest', 128), + ); + } + + /** + * Gets the session object. + * + * @return SessionInterface|null A SessionInterface instance of null if no session is available + */ + abstract protected function getSession(); +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php b/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php index 88505fac6e7a6..571cd74e343d0 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/StreamedResponseListener.php @@ -13,7 +13,6 @@ use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -32,7 +31,7 @@ class StreamedResponseListener implements EventSubscriberInterface */ public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } diff --git a/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php new file mode 100644 index 0000000000000..ee047a6accbdf --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * TestSessionListener. + * + * Saves session in test environment. + * + * @author Bulat Shakirzyanov + * @author Fabien Potencier + */ +abstract class TestSessionListener implements EventSubscriberInterface +{ + public function onKernelRequest(GetResponseEvent $event) + { + if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + return; + } + + // bootstrap the session + $session = $this->getSession(); + if (!$session) { + return; + } + + $cookies = $event->getRequest()->cookies; + + if ($cookies->has($session->getName())) { + $session->setId($cookies->get($session->getName())); + } + } + + /** + * Checks if session was initialized and saves if current request is master + * Runs on 'kernel.response' in test environment + * + * @param FilterResponseEvent $event + */ + public function onKernelResponse(FilterResponseEvent $event) + { + if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + return; + } + + $session = $event->getRequest()->getSession(); + if ($session && $session->isStarted()) { + $session->save(); + $params = session_get_cookie_params(); + $event->getResponse()->headers->setCookie(new Cookie($session->getName(), $session->getId(), 0 === $params['lifetime'] ? 0 : time() + $params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly'])); + } + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::REQUEST => array('onKernelRequest', 192), + KernelEvents::RESPONSE => array('onKernelResponse', -128), + ); + } + + /** + * Gets the session object. + * + * @return SessionInterface|null A SessionInterface instance of null if no session is available + */ + abstract protected function getSession(); +} diff --git a/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php index 68b1a87da65ca..a491a85ade22a 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/EsiFragmentRenderer.php @@ -35,7 +35,7 @@ class EsiFragmentRenderer extends RoutableFragmentRenderer * @param Esi $esi An Esi instance * @param InlineFragmentRenderer $inlineStrategy The inline strategy to use when ESI is not supported */ - public function __construct(Esi $esi, InlineFragmentRenderer $inlineStrategy) + public function __construct(Esi $esi = null, InlineFragmentRenderer $inlineStrategy) { $this->esi = $esi; $this->inlineStrategy = $inlineStrategy; @@ -56,7 +56,7 @@ public function __construct(Esi $esi, InlineFragmentRenderer $inlineStrategy) */ public function render($uri, Request $request, array $options = array()) { - if (!$this->esi->hasSurrogateEsiCapability($request)) { + if (!$this->esi || !$this->esi->hasSurrogateEsiCapability($request)) { return $this->inlineStrategy->render($uri, $request, $options); } diff --git a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php index af9b9ba98b744..0297304844397 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php +++ b/src/Symfony/Component/HttpKernel/Fragment/FragmentHandler.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Controller\ControllerReference; /** @@ -22,6 +23,11 @@ * This class handles the rendering of resource fragments that are included into * a main resource. The handling of the rendering is managed by specialized renderers. * + * This listener works in 2 modes: + * + * * 2.3 compatibility mode where you must call setRequest whenever the Request changes. + * * 2.4+ mode where you must pass a RequestStack instance in the constructor. + * * @author Fabien Potencier * * @see FragmentRendererInterface @@ -29,18 +35,22 @@ class FragmentHandler { private $debug; - private $renderers; + private $renderers = array(); private $request; + private $requestStack; /** * Constructor. * - * @param FragmentRendererInterface[] $renderers An array of FragmentRendererInterface instances - * @param Boolean $debug Whether the debug mode is enabled or not + * RequestStack will become required in 3.0. + * + * @param FragmentRendererInterface[] $renderers An array of FragmentRendererInterface instances + * @param Boolean $debug Whether the debug mode is enabled or not + * @param RequestStack|null $requestStack The Request stack that controls the lifecycle of requests */ - public function __construct(array $renderers = array(), $debug = false) + public function __construct(array $renderers = array(), $debug = false, RequestStack $requestStack = null) { - $this->renderers = array(); + $this->requestStack = $requestStack; foreach ($renderers as $renderer) { $this->addRenderer($renderer); } @@ -60,7 +70,13 @@ public function addRenderer(FragmentRendererInterface $renderer) /** * Sets the current Request. * - * @param Request $request The current Request + * This method was used to synchronize the Request, but as the HttpKernel + * is doing that automatically now, you should never call it directly. + * It is kept public for BC with the 2.3 version. + * + * @param Request|null $request A Request instance + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. */ public function setRequest(Request $request = null) { @@ -81,7 +97,7 @@ public function setRequest(Request $request = null) * @return string|null The Response content or null when the Response is streamed * * @throws \InvalidArgumentException when the renderer does not exist - * @throws \RuntimeException when the Response is not successful + * @throws \LogicException when the Request is not successful */ public function render($uri, $renderer = 'inline', array $options = array()) { @@ -93,11 +109,11 @@ public function render($uri, $renderer = 'inline', array $options = array()) throw new \InvalidArgumentException(sprintf('The "%s" renderer does not exist.', $renderer)); } - if (null === $this->request) { - throw new \LogicException('Rendering a fragment can only be done when handling a master Request.'); + if (!$request = $this->getRequest()) { + throw new \LogicException('Rendering a fragment can only be done when handling a Request.'); } - return $this->deliver($this->renderers[$renderer]->render($uri, $this->request, $options)); + return $this->deliver($this->renderers[$renderer]->render($uri, $request, $options)); } /** @@ -115,7 +131,7 @@ public function render($uri, $renderer = 'inline', array $options = array()) protected function deliver(Response $response) { if (!$response->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->request->getUri(), $response->getStatusCode())); + throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->getRequest()->getUri(), $response->getStatusCode())); } if (!$response instanceof StreamedResponse) { @@ -124,4 +140,9 @@ protected function deliver(Response $response) $response->sendContent(); } + + private function getRequest() + { + return $this->requestStack ? $this->requestStack->getCurrentRequest() : $this->request; + } } diff --git a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php index a3f37c359ae7c..c6ca3d475e882 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php @@ -32,7 +32,8 @@ class InlineFragmentRenderer extends RoutableFragmentRenderer /** * Constructor. * - * @param HttpKernelInterface $kernel A HttpKernelInterface instance + * @param HttpKernelInterface $kernel A HttpKernelInterface instance + * @param EventDispatcherInterface $dispatcher A EventDispatcherInterface instance */ public function __construct(HttpKernelInterface $kernel, EventDispatcherInterface $dispatcher = null) { @@ -60,14 +61,15 @@ public function render($uri, Request $request, array $options = array()) $attributes = $reference->attributes; $reference->attributes = array(); - // The request format and locale might have been overriden by the user + // The request format and locale might have been overridden by the user foreach (array('_format', '_locale') as $key) { if (isset($attributes[$key])) { $reference->attributes[$key] = $attributes[$key]; } } - $uri = $this->generateFragmentUri($uri, $request); + $uri = $this->generateFragmentUri($uri, $request, false, false); + $reference->attributes = array_merge($attributes, $reference->attributes); } @@ -130,7 +132,7 @@ protected function createSubRequest($uri, Request $request) $server['REMOTE_ADDR'] = '127.0.0.1'; - $subRequest = $request::create($uri, 'get', array(), $cookies, array(), $server); + $subRequest = Request::create($uri, 'get', array(), $cookies, array(), $server); if ($request->headers->has('Surrogate-Capability')) { $subRequest->headers->set('Surrogate-Capability', $request->headers->get('Surrogate-Capability')); } diff --git a/src/Symfony/Component/HttpKernel/Fragment/RoutableFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/RoutableFragmentRenderer.php index bd133ce46eb64..047c66087e938 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/RoutableFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/RoutableFragmentRenderer.php @@ -42,11 +42,16 @@ public function setFragmentPath($path) * @param ControllerReference $reference A ControllerReference instance * @param Request $request A Request instance * @param Boolean $absolute Whether to generate an absolute URL or not + * @param Boolean $strict Whether to allow non-scalar attributes or not * * @return string A fragment URI */ - protected function generateFragmentUri(ControllerReference $reference, Request $request, $absolute = false) + protected function generateFragmentUri(ControllerReference $reference, Request $request, $absolute = false, $strict = true) { + if ($strict) { + $this->checkNonScalar($reference->attributes); + } + // We need to forward the current _format and _locale values as we don't have // a proper routing pattern to do the job for us. // This makes things inconsistent if you switch from rendering a controller @@ -71,4 +76,15 @@ protected function generateFragmentUri(ControllerReference $reference, Request $ return $request->getBaseUrl().$path; } + + private function checkNonScalar($values) + { + foreach ($values as $key => $value) { + if (is_array($value)) { + $this->checkNonScalar($value); + } elseif (!is_scalar($value) && null !== $value) { + throw new \LogicException(sprintf('Controller attributes cannot contain non-scalar/non-null values (value for key "%s" is not a scalar or null).', $key)); + } + } + } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 1742980bfbd45..f80d6f03e73ce 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -34,7 +34,8 @@ class HttpCache implements HttpKernelInterface, TerminableInterface private $request; private $esi; private $esiCacheStrategy; - private $traces; + private $options = array(); + private $traces = array(); /** * Constructor. @@ -80,6 +81,7 @@ public function __construct(HttpKernelInterface $kernel, StoreInterface $store, { $this->store = $store; $this->kernel = $kernel; + $this->esi = $esi; // needed in case there is a fatal error because the backend is too slow to respond register_shutdown_function(array($this->store, 'cleanup')); @@ -93,8 +95,6 @@ public function __construct(HttpKernelInterface $kernel, StoreInterface $store, 'stale_while_revalidate' => 2, 'stale_if_error' => 60, ), $options); - $this->esi = $esi; - $this->traces = array(); } /** @@ -267,7 +267,7 @@ protected function invalidate(Request $request, $catch = false) // As per the RFC, invalidate Location and Content-Location URLs if present foreach (array('Location', 'Content-Location') as $header) { if ($uri = $response->headers->get($header)) { - $subRequest = $request::create($uri, 'get', array(), array(), array(), $request->server->all()); + $subRequest = Request::create($uri, 'get', array(), array(), array(), $request->server->all()); $this->store->invalidate($subRequest); } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php index d41b9f287ea97..1d55ab4118f2a 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php @@ -216,7 +216,7 @@ public function write(Request $request, Response $response) */ protected function generateContentDigest(Response $response) { - return 'en'.sha1($response->getContent()); + return 'en'.hash('sha256', $response->getContent()); } /** @@ -366,6 +366,25 @@ public function getPath($key) return $this->root.DIRECTORY_SEPARATOR.substr($key, 0, 2).DIRECTORY_SEPARATOR.substr($key, 2, 2).DIRECTORY_SEPARATOR.substr($key, 4, 2).DIRECTORY_SEPARATOR.substr($key, 6); } + /** + * Generates a cache key for the given Request. + * + * This method should return a key that must only depend on a + * normalized version of the request URI. + * + * If the same URI can have more than one representation, based on some + * headers, use a Vary header to indicate them, and each representation will + * be stored independently under the same cache key. + * + * @param Request $request A Request instance + * + * @return string A key for the given Request + */ + protected function generateCacheKey(Request $request) + { + return 'md'.hash('sha256', $request->getUri()); + } + /** * Returns a cache key for the given Request. * @@ -379,7 +398,7 @@ private function getCacheKey(Request $request) return $this->keyCache[$request]; } - return $this->keyCache[$request] = 'md'.sha1($request->getUri()); + return $this->keyCache[$request] = $this->generateCacheKey($request); } /** diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 837a16ff370e9..0be8e1b4dbb7c 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -16,11 +16,13 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Event\PostResponseEvent; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -35,19 +37,22 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface { protected $dispatcher; protected $resolver; + protected $requestStack; /** * Constructor * - * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance - * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance + * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance + * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance + * @param RequestStack $requestStack A stack for master/sub requests * * @api */ - public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver) + public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver, RequestStack $requestStack = null) { $this->dispatcher = $dispatcher; $this->resolver = $resolver; + $this->requestStack = $requestStack ?: new RequestStack(); } /** @@ -61,6 +66,8 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ return $this->handleRaw($request, $type); } catch (\Exception $e) { if (false === $catch) { + $this->finishRequest($request, $type); + throw $e; } @@ -93,6 +100,8 @@ public function terminate(Request $request, Response $response) */ private function handleRaw(Request $request, $type = self::MASTER_REQUEST) { + $this->requestStack->push($request); + // request $event = new GetResponseEvent($this, $request, $type); $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); @@ -156,9 +165,27 @@ private function filterResponse(Response $response, Request $request, $type) $this->dispatcher->dispatch(KernelEvents::RESPONSE, $event); + $this->finishRequest($request, $type); + return $event->getResponse(); } + /** + * Publishes the finish request event, then pop the request from the stack. + * + * Note that the order of the operations is important here, otherwise + * operations such as {@link RequestStack::getParentRequest()} can lead to + * weird results. + * + * @param Request $request + * @param int $type + */ + private function finishRequest(Request $request, $type) + { + $this->dispatcher->dispatch(KernelEvents::FINISH_REQUEST, new FinishRequestEvent($this, $request, $type)); + $this->requestStack->pop(); + } + /** * Handles an exception by trying to convert it to a Response. * @@ -179,6 +206,8 @@ private function handleException(\Exception $e, $request, $type) $e = $event->getException(); if (!$event->hasResponse()) { + $this->finishRequest($request, $type); + throw $e; } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index a179efca3af55..1364cde969370 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -47,23 +47,23 @@ abstract class Kernel implements KernelInterface, TerminableInterface /** * @var BundleInterface[] */ - protected $bundles; + protected $bundles = array(); protected $bundleMap; protected $container; protected $rootDir; protected $environment; protected $debug; - protected $booted; + protected $booted = false; protected $name; protected $startTime; protected $loadClassCache; - const VERSION = '2.3.10-DEV'; - const VERSION_ID = '20310'; + const VERSION = '2.5.0-DEV'; + const VERSION_ID = '20500'; const MAJOR_VERSION = '2'; - const MINOR_VERSION = '3'; - const RELEASE_VERSION = '10'; + const MINOR_VERSION = '5'; + const RELEASE_VERSION = '0'; const EXTRA_VERSION = 'DEV'; /** @@ -78,10 +78,8 @@ public function __construct($environment, $debug) { $this->environment = $environment; $this->debug = (Boolean) $debug; - $this->booted = false; $this->rootDir = $this->getRootDir(); $this->name = $this->getName(); - $this->bundles = array(); if ($this->debug) { $this->startTime = microtime(true); diff --git a/src/Symfony/Component/HttpKernel/KernelEvents.php b/src/Symfony/Component/HttpKernel/KernelEvents.php index fce48ac3a6ed8..5e6ebcb8d9926 100644 --- a/src/Symfony/Component/HttpKernel/KernelEvents.php +++ b/src/Symfony/Component/HttpKernel/KernelEvents.php @@ -102,4 +102,14 @@ final class KernelEvents * @var string */ const TERMINATE = 'kernel.terminate'; + + /** + * The REQUEST_FINISHED event occurs when a response was generated for a request. + * + * This event allows you to reset the global and environmental state of + * the application, when it was changed during the request. + * + * @var string + */ + const FINISH_REQUEST = 'kernel.finish_request'; } diff --git a/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php index 9265fc13f5c58..e225b0dd2eeb1 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php +++ b/src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php @@ -70,7 +70,7 @@ public function find($ip, $url, $limit, $method, $start = null, $end = null) } if (!empty($start) && $csvTime < $start) { - continue; + continue; } if (!empty($end) && $csvTime > $end) { diff --git a/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php index a78cec0c8a327..2034a19db1e6b 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php +++ b/src/Symfony/Component/HttpKernel/Profiler/MemcacheProfilerStorage.php @@ -42,7 +42,7 @@ protected function getMemcache() $host = $matches[1] ?: $matches[2]; $port = $matches[3]; - $memcache = new Memcache; + $memcache = new Memcache(); $memcache->addServer($host, $port); $this->memcache = $memcache; diff --git a/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php b/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php index f7f68423589a4..31f3136390851 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php +++ b/src/Symfony/Component/HttpKernel/Profiler/MemcachedProfilerStorage.php @@ -42,7 +42,7 @@ protected function getMemcached() $host = $matches[1] ?: $matches[2]; $port = $matches[3]; - $memcached = new Memcached; + $memcached = new Memcached(); //disable compression to allow appending $memcached->setOption(Memcached::OPT_COMPRESSION, false); diff --git a/src/Symfony/Component/HttpKernel/Profiler/Profiler.php b/src/Symfony/Component/HttpKernel/Profiler/Profiler.php index 743f314b2897d..b545b4acd510f 100644 --- a/src/Symfony/Component/HttpKernel/Profiler/Profiler.php +++ b/src/Symfony/Component/HttpKernel/Profiler/Profiler.php @@ -14,6 +14,7 @@ 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; /** @@ -108,6 +109,13 @@ public function loadProfile($token) */ public function saveProfile(Profile $profile) { + // late collect + foreach ($profile->getCollectors() as $collector) { + if ($collector instanceof LateDataCollectorInterface) { + $collector->lateCollect(); + } + } + if (!($ret = $this->storage->write($profile)) && null !== $this->logger) { $this->logger->warning('Unable to store the profiler information.'); } @@ -203,7 +211,7 @@ public function collect(Request $request, Response $response, \Exception $except return; } - $profile = new Profile(substr(sha1(uniqid(mt_rand(), true)), 0, 6)); + $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); $profile->setTime(time()); $profile->setUrl($request->getUri()); $profile->setIp($request->getClientIp()); @@ -214,8 +222,8 @@ public function collect(Request $request, Response $response, \Exception $except foreach ($this->collectors as $collector) { $collector->collect($request, $response, $exception); - // forces collectors to become "read/only" (they loose their object dependencies) - $profile->addCollector(unserialize(serialize($collector))); + // we need to clone for sub-requests + $profile->addCollector(clone $collector); } return $profile; diff --git a/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php b/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php index 8affb5f300bfb..f31d245c4b57e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php @@ -11,28 +11,16 @@ namespace Symfony\Component\HttpKernel\Tests\Bundle; -use Symfony\Component\HttpKernel\Bundle\Bundle; - -use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\ExtensionPresentBundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionAbsentBundle\ExtensionAbsentBundle; use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\Command\FooCommand; +use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\ExtensionPresentBundle; class BundleTest extends \PHPUnit_Framework_TestCase { public function testRegisterCommands() { - if (!class_exists('Symfony\Component\Console\Application')) { - $this->markTestSkipped('The "Console" component is not available'); - } - - if (!interface_exists('Symfony\Component\DependencyInjection\ContainerAwareInterface')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - - if (!class_exists('Symfony\Component\Finder\Finder')) { - $this->markTestSkipped('The "Finder" component is not available'); - } - $cmd = new FooCommand(); $app = $this->getMock('Symfony\Component\Console\Application'); $app->expects($this->once())->method('add')->with($this->equalTo($cmd)); @@ -43,6 +31,26 @@ public function testRegisterCommands() $bundle2 = new ExtensionAbsentBundle(); $this->assertNull($bundle2->registerCommands($app)); + } + public function testRegisterCommandsIngoreCommandAsAService() + { + $container = new ContainerBuilder(); + $commandClass = 'Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\Command\FooCommand'; + $definition = new Definition($commandClass); + $definition->addTag('console.command'); + $container->setDefinition('my-command', $definition); + $container->setAlias('console.command.'.strtolower(str_replace('\\', '_', $commandClass)), 'my-command'); + $container->compile(); + + $application = $this->getMock('Symfony\Component\Console\Application'); + // Never called, because it's the + // Symfony\Bundle\FrameworkBundle\Console\Application that register + // commands as a service + $application->expects($this->never())->method('add'); + + $bundle = new ExtensionPresentBundle(); + $bundle->setContainer($container); + $bundle->registerCommands($application); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/ClientTest.php b/src/Symfony/Component/HttpKernel/Tests/ClientTest.php index 755d7f614c97d..c58c9588d5d49 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ClientTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ClientTest.php @@ -22,13 +22,6 @@ class ClientTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\BrowserKit\Client')) { - $this->markTestSkipped('The "BrowserKit" component is not available'); - } - } - public function testDoRequest() { $client = new Client(new TestHttpKernel()); @@ -50,14 +43,6 @@ public function testDoRequest() public function testGetScript() { - if (!class_exists('Symfony\Component\Process\Process')) { - $this->markTestSkipped('The "Process" component is not available'); - } - - if (!class_exists('Symfony\Component\ClassLoader\ClassLoader')) { - $this->markTestSkipped('The "ClassLoader" component is not available'); - } - $client = new TestClient(new TestHttpKernel()); $client->insulate(); $client->request('GET', '/'); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php index a233c91346d5f..dd14186dd2676 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php @@ -17,13 +17,6 @@ class ControllerResolverTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testGetController() { $logger = new Logger(); diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php index 192c8083e0272..0c7396158631f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php @@ -19,13 +19,6 @@ class ConfigDataCollectorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testCollect() { $kernel = new KernelForTest('test', true); diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php index 54a8aabe671b9..ebea3ea6e1fc6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php @@ -18,13 +18,6 @@ class ExceptionDataCollectorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testCollect() { $e = new \Exception('foo',500); diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php index ea82d9d315733..7cd4d06c7ad60 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php @@ -13,18 +13,9 @@ use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; use Symfony\Component\HttpKernel\Debug\ErrorHandler; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; class LoggerDataCollectorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - /** * @dataProvider getCollectTestData */ @@ -35,7 +26,7 @@ public function testCollect($nb, $logs, $expectedLogs, $expectedDeprecationCount $logger->expects($this->exactly(2))->method('getLogs')->will($this->returnValue($logs)); $c = new LoggerDataCollector($logger); - $c->collect(new Request(), new Response()); + $c->lateCollect(); $this->assertSame('logger', $c->getName()); $this->assertSame($nb, $c->countErrors()); diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php index 9b7de4b904f10..340b428816882 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/MemoryDataCollectorTest.php @@ -17,13 +17,6 @@ class MemoryDataCollectorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testCollect() { $collector = new MemoryDataCollector(); diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php index cd0251c1ee4a8..bb80781ebeb56 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php @@ -22,13 +22,6 @@ class RequestDataCollectorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - /** * @dataProvider provider */ @@ -38,21 +31,23 @@ public function testCollect(Request $request, Response $response) $c->collect($request, $response); - $this->assertSame('request',$c->getName()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag',$c->getRequestHeaders()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag',$c->getRequestServer()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag',$c->getRequestCookies()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag',$c->getRequestAttributes()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag',$c->getRequestRequest()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag',$c->getRequestQuery()); - $this->assertEquals('html',$c->getFormat()); - $this->assertEquals(array(),$c->getSessionAttributes()); - $this->assertEquals('en',$c->getLocale()); - - $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag',$c->getResponseHeaders()); - $this->assertEquals('OK',$c->getStatusText()); - $this->assertEquals(200,$c->getStatusCode()); - $this->assertEquals('application/json',$c->getContentType()); + $this->assertSame('request', $c->getName()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag', $c->getRequestHeaders()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestServer()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestCookies()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestAttributes()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestRequest()); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestQuery()); + $this->assertSame('html', $c->getFormat()); + $this->assertSame('foobar', $c->getRoute()); + $this->assertSame(array('name' => 'foo'), $c->getRouteParams()); + $this->assertSame(array(), $c->getSessionAttributes()); + $this->assertSame('en', $c->getLocale()); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\HeaderBag', $c->getResponseHeaders()); + $this->assertSame('OK', $c->getStatusText()); + $this->assertSame(200, $c->getStatusCode()); + $this->assertSame('application/json', $c->getContentType()); } /** @@ -145,7 +140,7 @@ function () { return 'foo'; }, foreach ($controllerTests as $controllerTest) { $this->injectController($c, $controllerTest[1], $request); $c->collect($request, $response); - $this->assertEquals($controllerTest[2], $c->getController(), sprintf('Testing: %s', $controllerTest[0])); + $this->assertSame($controllerTest[2], $c->getController(), sprintf('Testing: %s', $controllerTest[0])); } } @@ -157,6 +152,8 @@ public function provider() $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')); $response = new Response(); $response->setStatusCode(200); diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php index 13abb67696943..b5d64bffe350a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/TimeDataCollectorTest.php @@ -17,16 +17,9 @@ class TimeDataCollectorTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testCollect() { - $c = new TimeDataCollector; + $c = new TimeDataCollector(); $request = new Request(); $request->server->set('REQUEST_TIME', 1); @@ -42,7 +35,7 @@ public function testCollect() $this->assertEquals(2000, $c->getStartTime()); $request = new Request(); - $c->collect($request, new Response); + $c->collect($request, new Response()); $this->assertEquals(0, $c->getStartTime()); $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); diff --git a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php index b9bc1d7f97e1a..0b4af59d59fa3 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php @@ -12,8 +12,6 @@ namespace Symfony\Component\HttpKernel\Tests\Debug; use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\EventDispatcher\Event; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpFoundation\Request; @@ -22,159 +20,6 @@ class TraceableEventDispatcherTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - - public function testAddRemoveListener() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $tdispatcher->addListener('foo', $listener = function () { ; }); - $listeners = $dispatcher->getListeners('foo'); - $this->assertCount(1, $listeners); - $this->assertSame($listener, $listeners[0]); - - $tdispatcher->removeListener('foo', $listener); - $this->assertCount(0, $dispatcher->getListeners('foo')); - } - - public function testGetListeners() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $tdispatcher->addListener('foo', $listener = function () { ; }); - $this->assertSame($dispatcher->getListeners('foo'), $tdispatcher->getListeners('foo')); - } - - public function testHasListeners() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $this->assertFalse($dispatcher->hasListeners('foo')); - $this->assertFalse($tdispatcher->hasListeners('foo')); - - $tdispatcher->addListener('foo', $listener = function () { ; }); - $this->assertTrue($dispatcher->hasListeners('foo')); - $this->assertTrue($tdispatcher->hasListeners('foo')); - } - - public function testAddRemoveSubscriber() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - - $subscriber = new EventSubscriber(); - - $tdispatcher->addSubscriber($subscriber); - $listeners = $dispatcher->getListeners('foo'); - $this->assertCount(1, $listeners); - $this->assertSame(array($subscriber, 'call'), $listeners[0]); - - $tdispatcher->removeSubscriber($subscriber); - $this->assertCount(0, $dispatcher->getListeners('foo')); - } - - public function testGetCalledListeners() - { - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - $tdispatcher->addListener('foo', $listener = function () { ; }); - - $this->assertEquals(array(), $tdispatcher->getCalledListeners()); - $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getNotCalledListeners()); - - $tdispatcher->dispatch('foo'); - - $this->assertEquals(array('foo.closure' => array('event' => 'foo', 'type' => 'Closure', 'pretty' => 'closure')), $tdispatcher->getCalledListeners()); - $this->assertEquals(array(), $tdispatcher->getNotCalledListeners()); - } - - public function testLogger() - { - $logger = $this->getMock('Psr\Log\LoggerInterface'); - - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); - $tdispatcher->addListener('foo', $listener1 = function () { ; }); - $tdispatcher->addListener('foo', $listener2 = function () { ; }); - - $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); - $logger->expects($this->at(1))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); - - $tdispatcher->dispatch('foo'); - } - - public function testLoggerWithStoppedEvent() - { - $logger = $this->getMock('Psr\Log\LoggerInterface'); - - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger); - $tdispatcher->addListener('foo', $listener1 = function (Event $event) { $event->stopPropagation(); }); - $tdispatcher->addListener('foo', $listener2 = function () { ; }); - - $logger->expects($this->at(0))->method('debug')->with("Notified event \"foo\" to listener \"closure\"."); - $logger->expects($this->at(1))->method('debug')->with("Listener \"closure\" stopped propagation of the event \"foo\"."); - $logger->expects($this->at(2))->method('debug')->with("Listener \"closure\" was not called for event \"foo\"."); - - $tdispatcher->dispatch('foo'); - } - - public function testDispatchCallListeners() - { - $called = array(); - - $dispatcher = new EventDispatcher(); - $tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch()); - $tdispatcher->addListener('foo', $listener1 = function () use (&$called) { $called[] = 'foo1'; }); - $tdispatcher->addListener('foo', $listener2 = function () use (&$called) { $called[] = 'foo2'; }); - - $tdispatcher->dispatch('foo'); - - $this->assertEquals(array('foo1', 'foo2'), $called); - } - - public function testDispatchNested() - { - $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); - $loop = 1; - $dispatcher->addListener('foo', $listener1 = function () use ($dispatcher, &$loop) { - ++$loop; - if (2 == $loop) { - $dispatcher->dispatch('foo'); - } - }); - - $dispatcher->dispatch('foo'); - } - - public function testDispatchReusedEventNested() - { - $nestedCall = false; - $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); - $dispatcher->addListener('foo', function (Event $e) use ($dispatcher) { - $dispatcher->dispatch('bar', $e); - }); - $dispatcher->addListener('bar', function (Event $e) use (&$nestedCall) { - $nestedCall = true; - }); - - $this->assertFalse($nestedCall); - $dispatcher->dispatch('foo'); - $this->assertTrue($nestedCall); - } - public function testStopwatchSections() { $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch = new Stopwatch()); @@ -243,11 +88,3 @@ protected function getHttpKernel($dispatcher, $controller) return new HttpKernel($dispatcher, $resolver); } } - -class EventSubscriber implements EventSubscriberInterface -{ - public static function getSubscribedEvents() - { - return array('foo' => 'call'); - } -} diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php index 5f24cf8c859b1..83182e63eae5b 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ContainerAwareHttpKernelTest.php @@ -13,27 +13,13 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\EventDispatcher\EventDispatcher; class ContainerAwareHttpKernelTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - /** * @dataProvider getProviderTypes */ @@ -41,59 +27,49 @@ public function testHandle($type) { $request = new Request(); $expected = new Response(); + $controller = function () use ($expected) { + return $expected; + }; $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - $container - ->expects($this->once()) - ->method('enterScope') - ->with($this->equalTo('request')) - ; - $container - ->expects($this->once()) - ->method('leaveScope') - ->with($this->equalTo('request')) - ; - $container - ->expects($this->at(0)) - ->method('hasScope') - ->with($this->equalTo('request')) - ->will($this->returnValue(false)); - $container - ->expects($this->at(1)) - ->method('addScope') - ->with($this->isInstanceOf('Symfony\Component\DependencyInjection\Scope')); - // enterScope() - $container - ->expects($this->at(3)) - ->method('set') - ->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request')) - ; - $container - ->expects($this->at(4)) - ->method('set') - ->with($this->equalTo('request'), $this->equalTo(null), $this->equalTo('request')) + $this + ->expectsEnterScopeOnce($container) + ->expectsLeaveScopeOnce($container) + ->expectsSetRequestWithAt($container, $request, 3) + ->expectsSetRequestWithAt($container, null, 4) ; $dispatcher = new EventDispatcher(); - $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver); + $resolver = $this->getResolverMockFor($controller, $request); + $stack = new RequestStack(); + $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver, $stack); + + $actual = $kernel->handle($request, $type); + + $this->assertSame($expected, $actual, '->handle() returns the response'); + } + /** + * @dataProvider getProviderTypes + */ + public function testVerifyRequestStackPushPopDuringHandle($type) + { + $request = new Request(); + $expected = new Response(); $controller = function () use ($expected) { return $expected; }; - $resolver->expects($this->once()) - ->method('getController') - ->with($request) - ->will($this->returnValue($controller)); - $resolver->expects($this->once()) - ->method('getArguments') - ->with($request, $controller) - ->will($this->returnValue(array())); + $stack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array('push', 'pop')); + $stack->expects($this->at(0))->method('push')->with($this->equalTo($request)); + $stack->expects($this->at(1))->method('pop'); - $actual = $kernel->handle($request, $type); + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $dispatcher = new EventDispatcher(); + $resolver = $this->getResolverMockFor($controller, $request); + $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver, $stack); - $this->assertSame($expected, $actual, '->handle() returns the response'); + $kernel->handle($request, $type); } /** @@ -103,51 +79,23 @@ public function testHandleRestoresThePreviousRequestOnException($type) { $request = new Request(); $expected = new \Exception(); + $controller = function () use ($expected) { + throw $expected; + }; $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - $container - ->expects($this->once()) - ->method('enterScope') - ->with($this->equalTo('request')) - ; - $container - ->expects($this->once()) - ->method('leaveScope') - ->with($this->equalTo('request')) - ; - $container - ->expects($this->at(0)) - ->method('hasScope') - ->with($this->equalTo('request')) - ->will($this->returnValue(true)); - // enterScope() - $container - ->expects($this->at(2)) - ->method('set') - ->with($this->equalTo('request'), $this->equalTo($request), $this->equalTo('request')) - ; - $container - ->expects($this->at(3)) - ->method('set') - ->with($this->equalTo('request'), $this->equalTo(null), $this->equalTo('request')) + $this + ->expectsEnterScopeOnce($container) + ->expectsLeaveScopeOnce($container) + ->expectsSetRequestWithAt($container, $request, 3) + ->expectsSetRequestWithAt($container, null, 4) ; $dispatcher = new EventDispatcher(); $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver); - - $controller = function () use ($expected) { - throw $expected; - }; - - $resolver->expects($this->once()) - ->method('getController') - ->with($request) - ->will($this->returnValue($controller)); - $resolver->expects($this->once()) - ->method('getArguments') - ->with($request, $controller) - ->will($this->returnValue(array())); + $resolver = $this->getResolverMockFor($controller, $request); + $stack = new RequestStack(); + $kernel = new ContainerAwareHttpKernel($dispatcher, $container, $resolver, $stack); try { $kernel->handle($request, $type); @@ -166,4 +114,52 @@ public function getProviderTypes() array(HttpKernelInterface::SUB_REQUEST), ); } + + private function getResolverMockFor($controller, $request) + { + $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); + $resolver->expects($this->once()) + ->method('getController') + ->with($request) + ->will($this->returnValue($controller)); + $resolver->expects($this->once()) + ->method('getArguments') + ->with($request, $controller) + ->will($this->returnValue(array())); + + return $resolver; + } + + private function expectsSetRequestWithAt($container, $with, $at) + { + $container + ->expects($this->at($at)) + ->method('set') + ->with($this->equalTo('request'), $this->equalTo($with), $this->equalTo('request')) + ; + + return $this; + } + + private function expectsEnterScopeOnce($container) + { + $container + ->expects($this->once()) + ->method('enterScope') + ->with($this->equalTo('request')) + ; + + return $this; + } + + private function expectsLeaveScopeOnce($container) + { + $container + ->expects($this->once()) + ->method('leaveScope') + ->with($this->equalTo('request')) + ; + + return $this; + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php index 1d1e92d6a546f..0df6a454b6602 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php @@ -15,17 +15,6 @@ class MergeExtensionConfigurationPassTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - - if (!class_exists('Symfony\Component\Config\FileLocator')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testAutoloadMainExtension() { $container = $this->getMock('Symfony\\Component\\DependencyInjection\\ContainerBuilder'); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php index 2eddc572a46d1..56f68535f1e21 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/EsiListenerTest.php @@ -22,13 +22,6 @@ class EsiListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - } - public function testFilterDoesNothingForSubRequests() { $dispatcher = new EventDispatcher(); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php index c6a01281a7d5c..30e8c74ecd294 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php @@ -26,17 +26,6 @@ */ class ExceptionListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testConstruct() { $logger = new TestLogger(); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php index 153d8a44ff9cc..ec9360a431eb6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/FragmentListenerTest.php @@ -19,13 +19,6 @@ class FragmentListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - } - public function testOnlyTriggeredOnFragmentRoute() { $request = Request::create('http://example.com/foo?_path=foo%3Dbar%26_controller%3Dfoo'); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php index e5e4e3a286dfc..d128753ffab06 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/LocaleListenerTest.php @@ -11,23 +11,24 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; -use Symfony\Component\HttpKernel\EventListener\LocaleListener; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\EventListener\LocaleListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; class LocaleListenerTest extends \PHPUnit_Framework_TestCase { + private $requestStack; + protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } + $this->requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array(), array(), '', false); } public function testDefaultLocaleWithoutSession() { - $listener = new LocaleListener('fr'); + $listener = new LocaleListener('fr', null, $this->requestStack); $event = $this->getEvent($request = Request::create('/')); $listener->onKernelRequest($event); @@ -41,7 +42,7 @@ public function testLocaleFromRequestAttribute() $request->cookies->set('foo', 'value'); $request->attributes->set('_locale', 'es'); - $listener = new LocaleListener('fr'); + $listener = new LocaleListener('fr', null, $this->requestStack); $event = $this->getEvent($request); $listener->onKernelRequest($event); @@ -49,6 +50,22 @@ public function testLocaleFromRequestAttribute() } public function testLocaleSetForRoutingContext() + { + // the request context is updated + $context = $this->getMock('Symfony\Component\Routing\RequestContext'); + $context->expects($this->once())->method('setParameter')->with('_locale', 'es'); + + $router = $this->getMock('Symfony\Component\Routing\Router', array('getContext'), array(), '', false); + $router->expects($this->once())->method('getContext')->will($this->returnValue($context)); + + $request = Request::create('/'); + + $request->attributes->set('_locale', 'es'); + $listener = new LocaleListener('fr', $router, $this->requestStack); + $listener->onKernelRequest($this->getEvent($request)); + } + + public function testRouterResetWithParentRequestOnKernelFinishRequest() { if (!class_exists('Symfony\Component\Routing\Router')) { $this->markTestSkipped('The "Routing" component is not available'); @@ -61,18 +78,22 @@ public function testLocaleSetForRoutingContext() $router = $this->getMock('Symfony\Component\Routing\Router', array('getContext'), array(), '', false); $router->expects($this->once())->method('getContext')->will($this->returnValue($context)); - $request = Request::create('/'); + $parentRequest = Request::create('/'); + $parentRequest->setLocale('es'); - $request->attributes->set('_locale', 'es'); - $listener = new LocaleListener('fr', $router); - $listener->onKernelRequest($this->getEvent($request)); + $this->requestStack->expects($this->once())->method('getParentRequest')->will($this->returnValue($parentRequest)); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\FinishRequestEvent', array(), array(), '', false); + + $listener = new LocaleListener('fr', $router, $this->requestStack); + $listener->onKernelFinishRequest($event); } public function testRequestLocaleIsNotOverridden() { $request = Request::create('/'); $request->setLocale('de'); - $listener = new LocaleListener('fr'); + $listener = new LocaleListener('fr', null, $this->requestStack); $event = $this->getEvent($request); $listener->onKernelRequest($event); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php new file mode 100644 index 0000000000000..772dfd0a4c116 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ProfilerListenerTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\EventListener; + +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; +use Symfony\Component\HttpKernel\EventListener\ProfilerListener; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Kernel; + +class ProfilerListenerTest extends \PHPUnit_Framework_TestCase +{ + /** + * Test to ensure BC without RequestStack + * + * @deprecated Deprecated since version 2.4, to be removed in 3.0. + */ + public function testEventsWithoutRequestStack() + { + $profile = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profile') + ->disableOriginalConstructor() + ->getMock(); + + $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') + ->disableOriginalConstructor() + ->getMock(); + $profiler->expects($this->once()) + ->method('collect') + ->will($this->returnValue($profile)); + + $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(); + + $listener = new ProfilerListener($profiler); + $listener->onKernelRequest(new GetResponseEvent($kernel, $request, Kernel::MASTER_REQUEST)); + $listener->onKernelResponse(new FilterResponseEvent($kernel, $request, Kernel::MASTER_REQUEST, $response)); + $listener->onKernelTerminate(new PostResponseEvent($kernel, $request, $response)); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php index 3f8e852104454..2698f8e7625ff 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ResponseListenerTest.php @@ -27,10 +27,6 @@ class ResponseListenerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - $this->dispatcher = new EventDispatcher(); $listener = new ResponseListener('UTF-8'); $this->dispatcher->addListener(KernelEvents::RESPONSE, array($listener, 'onKernelResponse')); diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php index 3ba56a8da78b8..ac742b35e08ce 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php @@ -11,23 +11,20 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; -use Symfony\Component\HttpKernel\EventListener\RouterListener; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\EventListener\RouterListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Routing\RequestContext; class RouterListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } + private $requestStack; - if (!class_exists('Symfony\Component\Routing\Router')) { - $this->markTestSkipped('The "Routing" component is not available'); - } + public function setUp() + { + $this->requestStack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array(), array(), '', false); } /** @@ -45,7 +42,7 @@ public function testPort($defaultHttpPort, $defaultHttpsPort, $uri, $expectedHtt ->method('getContext') ->will($this->returnValue($context)); - $listener = new RouterListener($urlMatcher); + $listener = new RouterListener($urlMatcher, null, null, $this->requestStack); $event = $this->createGetResponseEventForUri($uri); $listener->onKernelRequest($event); @@ -83,7 +80,7 @@ private function createGetResponseEventForUri($uri) */ public function testInvalidMatcher() { - new RouterListener(new \stdClass()); + new RouterListener(new \stdClass(), null, null, $this->requestStack); } public function testRequestMatcher() @@ -98,7 +95,7 @@ public function testRequestMatcher() ->with($this->isInstanceOf('Symfony\Component\HttpFoundation\Request')) ->will($this->returnValue(array())); - $listener = new RouterListener($requestMatcher, new RequestContext()); + $listener = new RouterListener($requestMatcher, new RequestContext(), null, $this->requestStack); $listener->onKernelRequest($event); } @@ -119,7 +116,7 @@ public function testSubRequestWithDifferentMethod() ->method('getContext') ->will($this->returnValue($context)); - $listener = new RouterListener($requestMatcher, new RequestContext()); + $listener = new RouterListener($requestMatcher, new RequestContext(), null, $this->requestStack); $listener->onKernelRequest($event); // sub-request with another HTTP method diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/TestSessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php similarity index 90% rename from src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/TestSessionListenerTest.php rename to src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php index 2c91254eb3ebb..907c20375fee6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/TestSessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php @@ -9,9 +9,8 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\Tests\EventListener; +namespace Symfony\Component\HttpKernel\Tests\EventListener; -use Symfony\Bundle\FrameworkBundle\EventListener\TestSessionListener; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -39,16 +38,10 @@ class TestSessionListenerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->listener = new TestSessionListener($this->getMock('Symfony\Component\DependencyInjection\ContainerInterface')); + $this->listener = $this->getMockForAbstractClass('Symfony\Component\HttpKernel\EventListener\TestSessionListener'); $this->session = $this->getSession(); } - protected function tearDown() - { - $this->listener = null; - $this->session = null; - } - public function testShouldSaveMasterRequestSession() { $this->sessionHasBeenStarted(); diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/KernelForTest.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/KernelForTest.php index e24daef2b353a..5fd61bbc7e7ce 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/KernelForTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/KernelForTest.php @@ -23,32 +23,15 @@ public function getBundleMap() public function registerBundles() { - } - - public function init() - { - } - - public function registerBundleDirs() - { + return array(); } public function registerContainerConfiguration(LoaderInterface $loader) { } - public function initializeBundles() - { - parent::initializeBundles(); - } - public function isBooted() { return $this->booted; } - - public function setIsBooted($value) - { - $this->booted = (Boolean) $value; - } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php index 1a63c4df6fa10..8acf45d96f6d7 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php @@ -18,13 +18,6 @@ class EsiFragmentRendererTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testRenderFallbackToInlineStrategyIfNoRequest() { $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy(true)); diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php index 3f6780a87657e..1893d85b80ac0 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/FragmentHandlerTest.php @@ -17,12 +17,27 @@ class FragmentHandlerTest extends \PHPUnit_Framework_TestCase { + private $requestStack; + + public function setUp() + { + $this->requestStack = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack') + ->disableOriginalConstructor() + ->getMock() + ; + $this->requestStack + ->expects($this->any()) + ->method('getCurrentRequest') + ->will($this->returnValue(Request::create('/'))) + ; + } + /** * @expectedException \InvalidArgumentException */ public function testRenderWhenRendererDoesNotExist() { - $handler = new FragmentHandler(); + $handler = new FragmentHandler(array(), null, $this->requestStack); $handler->render('/', 'foo'); } @@ -72,9 +87,8 @@ protected function getHandler($returnValue, $arguments = array()) call_user_func_array(array($e, 'with'), $arguments); } - $handler = new FragmentHandler(); + $handler = new FragmentHandler(array(), null, $this->requestStack); $handler->addRenderer($renderer); - $handler->setRequest(Request::create('/')); return $handler; } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php index cc1fe4557b0f4..2f266dba77cd0 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php @@ -18,13 +18,6 @@ class HIncludeFragmentRendererTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - /** * @expectedException \LogicException */ @@ -38,7 +31,7 @@ public function testRenderWithControllerAndSigner() { $strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo')); - $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', array(), array()), Request::create('/'))->getContent()); + $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', array(), array()), Request::create('/'))->getContent()); } public function testRenderWithUri() diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php index 89413a4f75d69..c7252c9cba558 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php @@ -21,17 +21,6 @@ class InlineFragmentRendererTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testRender() { $strategy = new InlineFragmentRenderer($this->getKernel($this->returnValue(new Response('foo')))); @@ -67,6 +56,24 @@ public function testRenderWithObjectsAsAttributes() $strategy->render(new ControllerReference('main_controller', array('object' => $object), array()), Request::create('/')); } + public function testRenderWithObjectsAsAttributesPassedAsObjectsInTheController() + { + $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver', array('getController')); + $resolver + ->expects($this->once()) + ->method('getController') + ->will($this->returnValue(function (\stdClass $object, Bar $object1) { + return new Response($object1->getBar()); + })) + ; + + $kernel = new HttpKernel(new EventDispatcher(), $resolver); + $renderer = new InlineFragmentRenderer($kernel); + + $response = $renderer->render(new ControllerReference('main_controller', array('object' => new \stdClass(), 'object1' => new Bar()), array()), Request::create('/')); + $this->assertEquals('bar', $response->getContent()); + } + public function testRenderWithTrustedHeaderDisabled() { $trustedHeaderName = Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP); @@ -197,3 +204,13 @@ public function testESIHeaderIsKeptInSubrequestWithTrustedHeaderDisabled() Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, $trustedHeaderName); } } + +class Bar +{ + public $bar = 'bar'; + + public function getBar() + { + return $this->bar; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/RoutableFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/RoutableFragmentRendererTest.php index c69bbc8e9af79..4af3601b41096 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/RoutableFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/RoutableFragmentRendererTest.php @@ -13,7 +13,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; -use Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer; class RoutableFragmentRendererTest extends \PHPUnit_Framework_TestCase { @@ -22,7 +21,7 @@ class RoutableFragmentRendererTest extends \PHPUnit_Framework_TestCase */ public function testGenerateFragmentUri($uri, $controller) { - $this->assertEquals($uri, $this->getRenderer()->doGenerateFragmentUri($controller, Request::create('/'))); + $this->assertEquals($uri, $this->callGenerateFragmentUriMethod($controller, Request::create('/'))); } /** @@ -30,7 +29,7 @@ public function testGenerateFragmentUri($uri, $controller) */ public function testGenerateAbsoluteFragmentUri($uri, $controller) { - $this->assertEquals('http://localhost'.$uri, $this->getRenderer()->doGenerateFragmentUri($controller, Request::create('/'), true)); + $this->assertEquals('http://localhost'.$uri, $this->callGenerateFragmentUriMethod($controller, Request::create('/'), true)); } public function getGenerateFragmentUriData() @@ -41,6 +40,7 @@ public function getGenerateFragmentUriData() array('/_fragment?_path=foo%3Dfoo%26_format%3Djson%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', array('foo' => 'foo', '_format' => 'json'), array())), array('/_fragment?bar=bar&_path=foo%3Dfoo%26_format%3Dhtml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', array('foo' => 'foo'), array('bar' => 'bar'))), array('/_fragment?foo=foo&_path=_format%3Dhtml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', array(), array('foo' => 'foo'))), + array('/_fragment?_path=foo%255B0%255D%3Dfoo%26foo%255B1%255D%3Dbar%26_format%3Dhtml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', array('foo' => array('foo', 'bar')), array())), ); } @@ -51,22 +51,48 @@ public function testGenerateFragmentUriWithARequest() $request->setLocale('fr'); $controller = new ControllerReference('controller', array(), array()); - $this->assertEquals('/_fragment?_path=_format%3Djson%26_locale%3Dfr%26_controller%3Dcontroller', $this->getRenderer()->doGenerateFragmentUri($controller, $request)); + $this->assertEquals('/_fragment?_path=_format%3Djson%26_locale%3Dfr%26_controller%3Dcontroller', $this->callGenerateFragmentUriMethod($controller, $request)); } - private function getRenderer() + /** + * @expectedException LogicException + * @dataProvider getGenerateFragmentUriDataWithNonScalar + */ + public function testGenerateFragmentUriWithNonScalar($controller) + { + $this->callGenerateFragmentUriMethod($controller, Request::create('/')); + } + + public function getGenerateFragmentUriDataWithNonScalar() + { + return array( + array(new ControllerReference('controller', array('foo' => new Foo(), 'bar' => 'bar'), array())), + array(new ControllerReference('controller', array('foo' => array('foo' => 'foo'), 'bar' => array('bar' => new Foo())), array())), + ); + } + + private function callGenerateFragmentUriMethod(ControllerReference $reference, Request $request, $absolute = false) { - return new Renderer(); + $renderer = $this->getMockForAbstractClass('Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer'); + $r = new \ReflectionObject($renderer); + $m = $r->getMethod('generateFragmentUri'); + $m->setAccessible(true); + + return $m->invoke($renderer, $reference, $request, $absolute); } } -class Renderer extends RoutableFragmentRenderer +class Foo { - public function render($uri, Request $request, array $options = array()) {} - public function getName() {} + public $foo; + + public function getFoo() + { + return $this->foo; + } - public function doGenerateFragmentUri(ControllerReference $reference, Request $request, $absolute = false) + public function doGenerateFragmentUri(ControllerReference $reference, Request $request, $absolute = false, $strict = true) { - return parent::generateFragmentUri($reference, $request, $absolute); + return parent::generateFragmentUri($reference, $request, $absolute, $strict); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php index 328f855cd308f..c50970638901e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php @@ -17,13 +17,6 @@ class EsiTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testHasSurrogateEsiCapability() { $esi = new Esi(); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php index a8064b832b7ff..a2b38bd807c02 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php @@ -19,19 +19,8 @@ class HttpCacheTest extends HttpCacheTestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testTerminateDelegatesTerminationOnlyForTerminableInterface() { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - $storeMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpCache\\StoreInterface') ->disableOriginalConstructor() ->getMock(); @@ -634,7 +623,7 @@ public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent() $r = new \ReflectionObject($this->store); $m = $r->getMethod('save'); $m->setAccessible(true); - $m->invoke($this->store, 'md'.sha1('http://localhost/'), serialize($tmp)); + $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp)); // build subsequent request; should be found but miss due to freshness $this->request('GET', '/'); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php index 4377f61fbeac5..9a1c7d767fdaa 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php @@ -31,10 +31,6 @@ class HttpCacheTestCase extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $this->kernel = null; $this->cache = null; diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php index 6a3e5653a781c..d8cf75ff7a9a8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php @@ -23,10 +23,6 @@ class StoreTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $this->request = Request::create('/'); $this->response = new Response('hello world', 200, array()); @@ -93,7 +89,7 @@ public function testSetsTheXContentDigestResponseHeaderBeforeStoring() $entries = $this->getStoreMetadata($cacheKey); list ($req, $res) = $entries[0]; - $this->assertEquals('ena94a8fe5ccb19ba61c4c0873d391e987982fbbd3', $res['x-content-digest'][0]); + $this->assertEquals('en9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', $res['x-content-digest'][0]); } public function testFindsAStoredEntryWithLookup() @@ -143,7 +139,7 @@ public function testRestoresResponseContentFromEntityStoreWithLookup() { $this->storeSimpleEntry(); $response = $this->store->lookup($this->request); - $this->assertEquals($this->getStorePath('en'.sha1('test')), $response->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->getContent()); } public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate() @@ -186,9 +182,9 @@ public function testStoresMultipleResponsesForEachVaryCombination() $res3 = new Response('test 3', 200, array('Vary' => 'Foo Bar')); $this->store->write($req3, $res3); - $this->assertEquals($this->getStorePath('en'.sha1('test 3')), $this->store->lookup($req3)->getContent()); - $this->assertEquals($this->getStorePath('en'.sha1('test 2')), $this->store->lookup($req2)->getContent()); - $this->assertEquals($this->getStorePath('en'.sha1('test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent()); $this->assertCount(3, $this->getStoreMetadata($key)); } @@ -198,17 +194,17 @@ public function testOverwritesNonVaryingResponseWithStore() $req1 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar')); $res1 = new Response('test 1', 200, array('Vary' => 'Foo Bar')); $key = $this->store->write($req1, $res1); - $this->assertEquals($this->getStorePath('en'.sha1('test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent()); $req2 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam')); $res2 = new Response('test 2', 200, array('Vary' => 'Foo Bar')); $this->store->write($req2, $res2); - $this->assertEquals($this->getStorePath('en'.sha1('test 2')), $this->store->lookup($req2)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent()); $req3 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar')); $res3 = new Response('test 3', 200, array('Vary' => 'Foo Bar')); $key = $this->store->write($req3, $res3); - $this->assertEquals($this->getStorePath('en'.sha1('test 3')), $this->store->lookup($req3)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent()); $this->assertCount(2, $this->getStoreMetadata($key)); } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php index cf23d7bf8abf7..da8c34c177680 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php @@ -23,9 +23,9 @@ class TestHttpKernel extends HttpKernel implements ControllerResolverInterface protected $body; protected $status; protected $headers; - protected $called; + protected $called = false; protected $customizer; - protected $catch; + protected $catch = false; protected $backendRequest; public function __construct($body, $status, $headers, \Closure $customizer = null) @@ -34,8 +34,6 @@ public function __construct($body, $status, $headers, \Closure $customizer = nul $this->status = $status; $this->headers = $headers; $this->customizer = $customizer; - $this->called = false; - $this->catch = false; parent::__construct(new EventDispatcher(), $this); } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php index 6dd3d9e499d61..89ef406bbb15f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php @@ -20,20 +20,14 @@ class TestMultipleHttpKernel extends HttpKernel implements ControllerResolverInterface { - protected $bodies; - protected $statuses; - protected $headers; - protected $catch; - protected $call; + protected $bodies = array(); + protected $statuses = array(); + protected $headers = array(); + protected $call = false; protected $backendRequest; public function __construct($responses) { - $this->bodies = array(); - $this->statuses = array(); - $this->headers = array(); - $this->call = false; - foreach ($responses as $response) { $this->bodies[] = $response['body']; $this->statuses[] = $response['status']; diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index f4d689f3a7afa..700b111228bee 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -23,17 +23,6 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - /** * @expectedException \RuntimeException */ @@ -249,6 +238,20 @@ public function testTerminate() $this->assertEquals($response, $capturedResponse); } + public function testVerifyRequestStackPushPopDuringHandle() + { + $request = new Request(); + + $stack = $this->getMock('Symfony\Component\HttpFoundation\RequestStack', array('push', 'pop')); + $stack->expects($this->at(0))->method('push')->with($this->equalTo($request)); + $stack->expects($this->at(1))->method('pop'); + + $dispatcher = new EventDispatcher(); + $kernel = new HttpKernel($dispatcher, $this->getResolver(), $stack); + + $kernel->handle($request, HttpKernelInterface::MASTER_REQUEST); + } + protected function getResolver($controller = null) { if (null === $controller) { diff --git a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php index 05d0c7507eaff..f4aba74c721d5 100644 --- a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Tests; use Symfony\Component\HttpKernel\Kernel; -use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -22,13 +21,6 @@ class KernelTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\DependencyInjection\Container')) { - $this->markTestSkipped('The "DependencyInjection" component is not available'); - } - } - public function testConstructor() { $env = 'test_env'; @@ -59,33 +51,22 @@ public function testClone() public function testBootInitializesBundlesAndContainer() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles')) - ->getMock(); + $kernel = $this->getKernel(array('initializeBundles', 'initializeContainer')); $kernel->expects($this->once()) ->method('initializeBundles'); $kernel->expects($this->once()) ->method('initializeContainer'); - $kernel->expects($this->once()) - ->method('getBundles') - ->will($this->returnValue(array())); $kernel->boot(); } public function testBootSetsTheContainerToTheBundles() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle') - ->disableOriginalConstructor() - ->getMock(); + $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); $bundle->expects($this->once()) ->method('setContainer'); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles')) - ->getMock(); + $kernel = $this->getKernel(array('initializeBundles', 'initializeContainer', 'getBundles')); $kernel->expects($this->once()) ->method('getBundles') ->will($this->returnValue(array($bundle))); @@ -95,13 +76,11 @@ public function testBootSetsTheContainerToTheBundles() public function testBootSetsTheBootedFlagToTrue() { + // use test kernel to access isBooted() $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles')) + ->setConstructorArgs(array('test', false)) + ->setMethods(array('initializeBundles', 'initializeContainer')) ->getMock(); - $kernel->expects($this->once()) - ->method('getBundles') - ->will($this->returnValue(array())); $kernel->boot(); @@ -110,14 +89,8 @@ public function testBootSetsTheBootedFlagToTrue() public function testClassCacheIsLoaded() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles', 'doLoadClassCache')) - ->getMock(); + $kernel = $this->getKernel(array('initializeBundles', 'initializeContainer', 'doLoadClassCache')); $kernel->loadClassCache('name', '.extension'); - $kernel->expects($this->any()) - ->method('getBundles') - ->will($this->returnValue(array())); $kernel->expects($this->once()) ->method('doLoadClassCache') ->with('name', '.extension'); @@ -127,13 +100,7 @@ public function testClassCacheIsLoaded() public function testClassCacheIsNotLoadedByDefault() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles', 'doLoadClassCache')) - ->getMock(); - $kernel->expects($this->any()) - ->method('getBundles') - ->will($this->returnValue(array())); + $kernel = $this->getKernel(array('initializeBundles', 'initializeContainer')); $kernel->expects($this->never()) ->method('doLoadClassCache'); @@ -142,27 +109,17 @@ public function testClassCacheIsNotLoadedByDefault() public function testClassCacheIsNotLoadedWhenKernelIsNotBooted() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles', 'doLoadClassCache')) - ->getMock(); + $kernel = $this->getKernel(array('initializeBundles', 'initializeContainer', 'doLoadClassCache')); $kernel->loadClassCache(); - $kernel->expects($this->any()) - ->method('getBundles') - ->will($this->returnValue(array())); $kernel->expects($this->never()) ->method('doLoadClassCache'); } public function testBootKernelSeveralTimesOnlyInitializesBundlesOnce() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('initializeBundles', 'initializeContainer', 'getBundles')) - ->getMock(); + $kernel = $this->getKernel(array('initializeBundles', 'initializeContainer')); $kernel->expects($this->once()) - ->method('getBundles') - ->will($this->returnValue(array())); + ->method('initializeBundles'); $kernel->boot(); $kernel->boot(); @@ -170,40 +127,29 @@ public function testBootKernelSeveralTimesOnlyInitializesBundlesOnce() public function testShutdownCallsShutdownOnAllBundles() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle') - ->disableOriginalConstructor() - ->getMock(); + $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); $bundle->expects($this->once()) ->method('shutdown'); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getBundles')) - ->getMock(); - $kernel->expects($this->once()) - ->method('getBundles') - ->will($this->returnValue(array($bundle))); + $kernel = $this->getKernel(array(), array($bundle)); + $kernel->boot(); $kernel->shutdown(); } public function testShutdownGivesNullContainerToAllBundles() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle') - ->disableOriginalConstructor() - ->getMock(); - $bundle->expects($this->once()) + $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); + $bundle->expects($this->at(3)) ->method('setContainer') ->with(null); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getBundles')) - ->getMock(); - $kernel->expects($this->once()) + $kernel = $this->getKernel(array('getBundles')); + $kernel->expects($this->any()) ->method('getBundles') ->will($this->returnValue(array($bundle))); + $kernel->boot(); $kernel->shutdown(); } @@ -221,11 +167,7 @@ public function testHandleCallsHandleOnHttpKernel() ->method('handle') ->with($request, $type, $catch); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getHttpKernel')) - ->getMock(); - + $kernel = $this->getKernel(array('getHttpKernel')); $kernel->expects($this->once()) ->method('getHttpKernel') ->will($this->returnValue($httpKernelMock)); @@ -243,11 +185,7 @@ public function testHandleBootsTheKernel() ->disableOriginalConstructor() ->getMock(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getHttpKernel', 'boot')) - ->getMock(); - + $kernel = $this->getKernel(array('getHttpKernel', 'boot')); $kernel->expects($this->once()) ->method('getHttpKernel') ->will($this->returnValue($httpKernelMock)); @@ -255,10 +193,6 @@ public function testHandleBootsTheKernel() $kernel->expects($this->once()) ->method('boot'); - // required as this value is initialized - // in the kernel constructor, which we don't call - $kernel->setIsBooted(false); - $kernel->handle($request, $type, $catch); } @@ -369,10 +303,7 @@ protected function getKernelMockForIsClassInActiveBundleTest() { $bundle = new FooBarBundle(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getBundles')) - ->getMock(); + $kernel = $this->getKernel(array('getBundles')); $kernel->expects($this->once()) ->method('getBundles') ->will($this->returnValue(array($bundle))); @@ -416,7 +347,7 @@ public function testSerialize() */ public function testLocateResourceThrowsExceptionWhenNameIsNotValid() { - $this->getKernelForInvalidLocateResource()->locateResource('Foo'); + $this->getKernel()->locateResource('Foo'); } /** @@ -424,7 +355,7 @@ public function testLocateResourceThrowsExceptionWhenNameIsNotValid() */ public function testLocateResourceThrowsExceptionWhenNameIsUnsafe() { - $this->getKernelForInvalidLocateResource()->locateResource('@FooBundle/../bar'); + $this->getKernel()->locateResource('@FooBundle/../bar'); } /** @@ -432,7 +363,7 @@ public function testLocateResourceThrowsExceptionWhenNameIsUnsafe() */ public function testLocateResourceThrowsExceptionWhenBundleDoesNotExist() { - $this->getKernelForInvalidLocateResource()->locateResource('@FooBundle/config/routing.xml'); + $this->getKernel()->locateResource('@FooBundle/config/routing.xml'); } /** @@ -440,7 +371,7 @@ public function testLocateResourceThrowsExceptionWhenBundleDoesNotExist() */ public function testLocateResourceThrowsExceptionWhenResourceDoesNotExist() { - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->once()) ->method('getBundle') @@ -452,7 +383,7 @@ public function testLocateResourceThrowsExceptionWhenResourceDoesNotExist() public function testLocateResourceReturnsTheFirstThatMatches() { - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->once()) ->method('getBundle') @@ -467,7 +398,7 @@ public function testLocateResourceReturnsTheFirstThatMatchesWithParent() $parent = $this->getBundle(__DIR__.'/Fixtures/Bundle1Bundle'); $child = $this->getBundle(__DIR__.'/Fixtures/Bundle2Bundle'); - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->exactly(2)) ->method('getBundle') @@ -483,7 +414,7 @@ public function testLocateResourceReturnsAllMatches() $parent = $this->getBundle(__DIR__.'/Fixtures/Bundle1Bundle'); $child = $this->getBundle(__DIR__.'/Fixtures/Bundle2Bundle'); - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->once()) ->method('getBundle') @@ -498,7 +429,7 @@ public function testLocateResourceReturnsAllMatches() public function testLocateResourceReturnsAllMatchesBis() { - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->once()) ->method('getBundle') @@ -516,7 +447,7 @@ public function testLocateResourceReturnsAllMatchesBis() public function testLocateResourceIgnoresDirOnNonResource() { - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->once()) ->method('getBundle') @@ -531,7 +462,7 @@ public function testLocateResourceIgnoresDirOnNonResource() public function testLocateResourceReturnsTheDirOneForResources() { - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->once()) ->method('getBundle') @@ -546,7 +477,7 @@ public function testLocateResourceReturnsTheDirOneForResources() public function testLocateResourceReturnsTheDirOneForResourcesAndBundleOnes() { - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->once()) ->method('getBundle') @@ -565,7 +496,7 @@ public function testLocateResourceOverrideBundleAndResourcesFolders() $parent = $this->getBundle(__DIR__.'/Fixtures/BaseBundle', null, 'BaseBundle', 'BaseBundle'); $child = $this->getBundle(__DIR__.'/Fixtures/ChildBundle', 'ParentBundle', 'ChildBundle', 'ChildBundle'); - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->exactly(4)) ->method('getBundle') @@ -600,7 +531,7 @@ public function testLocateResourceOverrideBundleAndResourcesFolders() public function testLocateResourceOnDirectories() { - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->exactly(2)) ->method('getBundle') @@ -616,7 +547,7 @@ public function testLocateResourceOnDirectories() $kernel->locateResource('@FooBundle/Resources', __DIR__.'/Fixtures/Resources') ); - $kernel = $this->getKernel(); + $kernel = $this->getKernel(array('getBundle')); $kernel ->expects($this->exactly(2)) ->method('getBundle') @@ -638,13 +569,19 @@ public function testInitializeBundles() $parent = $this->getBundle(null, null, 'ParentABundle'); $child = $this->getBundle(null, 'ParentABundle', 'ChildABundle'); - $kernel = $this->getKernel(); + // use test kernel so we can access getBundleMap() + $kernel = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') + ->setMethods(array('registerBundles')) + ->setConstructorArgs(array('test', false)) + ->getMock() + ; $kernel ->expects($this->once()) ->method('registerBundles') ->will($this->returnValue(array($parent, $child))) ; - $kernel->initializeBundles(); + $kernel->boot(); $map = $kernel->getBundleMap(); $this->assertEquals(array($child, $parent), $map['ParentABundle']); @@ -656,14 +593,20 @@ public function testInitializeBundlesSupportInheritanceCascade() $parent = $this->getBundle(null, 'GrandParentBBundle', 'ParentBBundle'); $child = $this->getBundle(null, 'ParentBBundle', 'ChildBBundle'); - $kernel = $this->getKernel(); + // use test kernel so we can access getBundleMap() + $kernel = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') + ->setMethods(array('registerBundles')) + ->setConstructorArgs(array('test', false)) + ->getMock() + ; $kernel ->expects($this->once()) ->method('registerBundles') ->will($this->returnValue(array($grandparent, $parent, $child))) ; - $kernel->initializeBundles(); + $kernel->boot(); $map = $kernel->getBundleMap(); $this->assertEquals(array($child, $parent, $grandparent), $map['GrandParentBBundle']); @@ -673,43 +616,45 @@ public function testInitializeBundlesSupportInheritanceCascade() /** * @expectedException \LogicException + * @expectedExceptionMessage Bundle "ChildCBundle" extends bundle "FooBar", which is not registered. */ public function testInitializeBundlesThrowsExceptionWhenAParentDoesNotExists() { $child = $this->getBundle(null, 'FooBar', 'ChildCBundle'); - - $kernel = $this->getKernel(); - $kernel - ->expects($this->once()) - ->method('registerBundles') - ->will($this->returnValue(array($child))) - ; - $kernel->initializeBundles(); + $kernel = $this->getKernel(array(), array($child)); + $kernel->boot(); } public function testInitializeBundlesSupportsArbitraryBundleRegistrationOrder() { - $grandparent = $this->getBundle(null, null, 'GrandParentCCundle'); - $parent = $this->getBundle(null, 'GrandParentCCundle', 'ParentCCundle'); - $child = $this->getBundle(null, 'ParentCCundle', 'ChildCCundle'); + $grandparent = $this->getBundle(null, null, 'GrandParentCBundle'); + $parent = $this->getBundle(null, 'GrandParentCBundle', 'ParentCBundle'); + $child = $this->getBundle(null, 'ParentCBundle', 'ChildCBundle'); - $kernel = $this->getKernel(); + // use test kernel so we can access getBundleMap() + $kernel = $this + ->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') + ->setMethods(array('registerBundles')) + ->setConstructorArgs(array('test', false)) + ->getMock() + ; $kernel ->expects($this->once()) ->method('registerBundles') ->will($this->returnValue(array($parent, $grandparent, $child))) ; - $kernel->initializeBundles(); + $kernel->boot(); $map = $kernel->getBundleMap(); - $this->assertEquals(array($child, $parent, $grandparent), $map['GrandParentCCundle']); - $this->assertEquals(array($child, $parent), $map['ParentCCundle']); - $this->assertEquals(array($child), $map['ChildCCundle']); + $this->assertEquals(array($child, $parent, $grandparent), $map['GrandParentCBundle']); + $this->assertEquals(array($child, $parent), $map['ParentCBundle']); + $this->assertEquals(array($child), $map['ChildCBundle']); } /** * @expectedException \LogicException + * @expectedExceptionMessage Bundle "ParentCBundle" is directly extended by two bundles "ChildC2Bundle" and "ChildC1Bundle". */ public function testInitializeBundlesThrowsExceptionWhenABundleIsDirectlyExtendedByTwoBundles() { @@ -717,59 +662,41 @@ public function testInitializeBundlesThrowsExceptionWhenABundleIsDirectlyExtende $child1 = $this->getBundle(null, 'ParentCBundle', 'ChildC1Bundle'); $child2 = $this->getBundle(null, 'ParentCBundle', 'ChildC2Bundle'); - $kernel = $this->getKernel(); - $kernel - ->expects($this->once()) - ->method('registerBundles') - ->will($this->returnValue(array($parent, $child1, $child2))) - ; - $kernel->initializeBundles(); + $kernel = $this->getKernel(array(), array($parent, $child1, $child2)); + $kernel->boot(); } /** * @expectedException \LogicException + * @expectedExceptionMessage Trying to register two bundles with the same name "DuplicateName" */ public function testInitializeBundleThrowsExceptionWhenRegisteringTwoBundlesWithTheSameName() { $fooBundle = $this->getBundle(null, null, 'FooBundle', 'DuplicateName'); $barBundle = $this->getBundle(null, null, 'BarBundle', 'DuplicateName'); - $kernel = $this->getKernel(); - $kernel - ->expects($this->once()) - ->method('registerBundles') - ->will($this->returnValue(array($fooBundle, $barBundle))) - ; - $kernel->initializeBundles(); + $kernel = $this->getKernel(array(), array($fooBundle, $barBundle)); + $kernel->boot(); } /** * @expectedException \LogicException + * @expectedExceptionMessage Bundle "CircularRefBundle" can not extend itself. */ public function testInitializeBundleThrowsExceptionWhenABundleExtendsItself() { $circularRef = $this->getBundle(null, 'CircularRefBundle', 'CircularRefBundle'); - $kernel = $this->getKernel(); - $kernel - ->expects($this->once()) - ->method('registerBundles') - ->will($this->returnValue(array($circularRef))) - ; - $kernel->initializeBundles(); + $kernel = $this->getKernel(array(), array($circularRef)); + $kernel->boot(); } public function testTerminateReturnsSilentlyIfKernelIsNotBooted() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getHttpKernel')) - ->getMock(); - + $kernel = $this->getKernel(array('getHttpKernel')); $kernel->expects($this->never()) ->method('getHttpKernel'); - $kernel->setIsBooted(false); $kernel->terminate(Request::create('/'), new Response()); } @@ -784,16 +711,12 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface() ->expects($this->never()) ->method('terminate'); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getHttpKernel')) - ->getMock(); - + $kernel = $this->getKernel(array('getHttpKernel')); $kernel->expects($this->once()) ->method('getHttpKernel') ->will($this->returnValue($httpKernelMock)); - $kernel->setIsBooted(true); + $kernel->boot(); $kernel->terminate(Request::create('/'), new Response()); // implements TerminableInterface @@ -806,19 +729,20 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface() ->expects($this->once()) ->method('terminate'); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->disableOriginalConstructor() - ->setMethods(array('getHttpKernel')) - ->getMock(); - + $kernel = $this->getKernel(array('getHttpKernel')); $kernel->expects($this->exactly(2)) ->method('getHttpKernel') ->will($this->returnValue($httpKernelMock)); - $kernel->setIsBooted(true); + $kernel->boot(); $kernel->terminate(Request::create('/'), new Response()); } + /** + * Returns a mock for the BundleInterface + * + * @return BundleInterface + */ protected function getBundle($dir = null, $parent = null, $className = null, $bundleName = null) { $bundle = $this @@ -854,22 +778,28 @@ protected function getBundle($dir = null, $parent = null, $className = null, $bu return $bundle; } - protected function getKernel() - { - return $this - ->getMockBuilder('Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest') - ->setMethods(array('getBundle', 'registerBundles')) - ->disableOriginalConstructor() - ->getMock() - ; - } - - protected function getKernelForInvalidLocateResource() + /** + * Returns a mock for the abstract kernel. + * + * @param array $methods Additional methods to mock (besides the abstract ones) + * @param array $bundles Bundles to register + * + * @return Kernel + */ + protected function getKernel(array $methods = array(), array $bundles = array()) { - return $this + $kernel = $this ->getMockBuilder('Symfony\Component\HttpKernel\Kernel') - ->disableOriginalConstructor() + ->setMethods($methods) + ->setConstructorArgs(array('test', false)) ->getMockForAbstractClass() ; + + $kernel->expects($this->any()) + ->method('registerBundles') + ->will($this->returnValue($bundles)) + ; + + return $kernel; } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcacheMock.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcacheMock.php index 014f5492fc8e9..9ff962c5b75e7 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcacheMock.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcacheMock.php @@ -18,14 +18,8 @@ */ class MemcacheMock { - private $connected; - private $storage; - - public function __construct() - { - $this->connected = false; - $this->storage = array(); - } + private $connected = false; + private $storage = array(); /** * Open memcached server connection diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcachedMock.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcachedMock.php index 2b17d70d2832e..d28d54211d111 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcachedMock.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/MemcachedMock.php @@ -18,14 +18,8 @@ */ class MemcachedMock { - private $connected; - private $storage; - - public function __construct() - { - $this->connected = false; - $this->storage = array(); - } + private $connected = false; + private $storage = array(); /** * Set a Memcached option diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/RedisMock.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/RedisMock.php index ca2980edde466..4a89e2db88728 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/RedisMock.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/Mock/RedisMock.php @@ -18,15 +18,8 @@ */ class RedisMock { - - private $connected; - private $storage; - - public function __construct() - { - $this->connected = false; - $this->storage = array(); - } + private $connected = false; + private $storage = array(); /** * Add a server to connection pool diff --git a/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php b/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php index 2a41531ecb186..ede7c3f14b0d7 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Profiler/ProfilerTest.php @@ -19,13 +19,6 @@ class ProfilerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testCollect() { if (!class_exists('SQLite3') && (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers()))) { diff --git a/src/Symfony/Component/HttpKernel/UriSigner.php b/src/Symfony/Component/HttpKernel/UriSigner.php index 665e99e2efb27..7ede0c32f76e5 100644 --- a/src/Symfony/Component/HttpKernel/UriSigner.php +++ b/src/Symfony/Component/HttpKernel/UriSigner.php @@ -67,6 +67,6 @@ public function check($uri) private function computeHash($uri) { - return urlencode(base64_encode(hash_hmac('sha1', $uri, $this->secret, true))); + return urlencode(base64_encode(hash_hmac('sha256', $uri, $this->secret, true))); } } diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index af659da904092..3854dd389d1de 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=5.3.3", "symfony/event-dispatcher": "~2.1", - "symfony/http-foundation": "~2.2", + "symfony/http-foundation": "~2.4", "symfony/debug": "~2.3", "psr/log": "~1.0" }, @@ -49,7 +49,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php index 8b3388e7ae373..429dae990aefb 100644 --- a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php +++ b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php @@ -154,7 +154,7 @@ class NumberFormatter private $attributes = array( self::FRACTION_DIGITS => 0, self::GROUPING_USED => 1, - self::ROUNDING_MODE => self::ROUND_HALFEVEN + self::ROUNDING_MODE => self::ROUND_HALFEVEN, ); /** @@ -171,7 +171,7 @@ class NumberFormatter */ private static $supportedStyles = array( 'CURRENCY' => self::CURRENCY, - 'DECIMAL' => self::DECIMAL + 'DECIMAL' => self::DECIMAL, ); /** @@ -182,7 +182,7 @@ class NumberFormatter private static $supportedAttributes = array( 'FRACTION_DIGITS' => self::FRACTION_DIGITS, 'GROUPING_USED' => self::GROUPING_USED, - 'ROUNDING_MODE' => self::ROUNDING_MODE + 'ROUNDING_MODE' => self::ROUNDING_MODE, ); /** @@ -195,7 +195,11 @@ class NumberFormatter private static $roundingModes = array( 'ROUND_HALFEVEN' => self::ROUND_HALFEVEN, 'ROUND_HALFDOWN' => self::ROUND_HALFDOWN, - 'ROUND_HALFUP' => self::ROUND_HALFUP + 'ROUND_HALFUP' => self::ROUND_HALFUP, + 'ROUND_CEILING' => self::ROUND_CEILING, + 'ROUND_FLOOR' => self::ROUND_FLOOR, + 'ROUND_DOWN' => self::ROUND_DOWN, + 'ROUND_UP' => self::ROUND_UP, ); /** @@ -209,7 +213,21 @@ class NumberFormatter private static $phpRoundingMap = array( self::ROUND_HALFDOWN => \PHP_ROUND_HALF_DOWN, self::ROUND_HALFEVEN => \PHP_ROUND_HALF_EVEN, - self::ROUND_HALFUP => \PHP_ROUND_HALF_UP + self::ROUND_HALFUP => \PHP_ROUND_HALF_UP, + ); + + /** + * The list of supported rounding modes which aren't available modes in + * PHP's round() function, but there's an equivalent. Keys are rounding + * modes, values does not matter. + * + * @var array + */ + private static $customRoundingList = array( + self::ROUND_CEILING => true, + self::ROUND_FLOOR => true, + self::ROUND_DOWN => true, + self::ROUND_UP => true, ); /** @@ -219,7 +237,7 @@ class NumberFormatter */ private static $int32Range = array( 'positive' => 2147483647, - 'negative' => -2147483648 + 'negative' => -2147483648, ); /** @@ -229,7 +247,7 @@ class NumberFormatter */ private static $int64Range = array( 'positive' => 9223372036854775807, - 'negative' => -9223372036854775808 + 'negative' => -9223372036854775808, ); private static $enSymbols = array( @@ -505,7 +523,7 @@ public function parseCurrency($value, &$currency, &$position = null) * Parse a number * * @param string $value The value to parse - * @param string $type Type of the formatting, one of the format type constants. NumberFormatter::TYPE_DOUBLE by default + * @param int $type Type of the formatting, one of the format type constants. NumberFormatter::TYPE_DOUBLE by default * @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended * * @return Boolean|string The parsed value of false on error @@ -549,9 +567,7 @@ public function parse($value, $type = self::TYPE_DOUBLE, &$position = 0) * @param int $attr An attribute specifier, one of the numeric attribute constants. * The only currently supported attributes are NumberFormatter::FRACTION_DIGITS, * NumberFormatter::GROUPING_USED and NumberFormatter::ROUNDING_MODE. - * @param int $value The attribute value. The only currently supported rounding modes are - * NumberFormatter::ROUND_HALFEVEN, NumberFormatter::ROUND_HALFDOWN and - * NumberFormatter::ROUND_HALFUP. + * @param int $value The attribute value. * * @return Boolean true on success or false on failure * @@ -700,10 +716,32 @@ private function roundCurrency($value, $currency) */ private function round($value, $precision) { - $precision = $this->getUnitializedPrecision($value, $precision); + $precision = $this->getUninitializedPrecision($value, $precision); + + $roundingModeAttribute = $this->getAttribute(self::ROUNDING_MODE); + if (isset(self::$phpRoundingMap[$roundingModeAttribute])) { + $value = round($value, $precision, self::$phpRoundingMap[$roundingModeAttribute]); + } elseif (isset(self::$customRoundingList[$roundingModeAttribute])) { + $roundingCoef = pow(10, $precision); + $value *= $roundingCoef; + + switch ($roundingModeAttribute) { + case self::ROUND_CEILING: + $value = ceil($value); + break; + case self::ROUND_FLOOR: + $value = floor($value); + break; + case self::ROUND_UP: + $value = $value > 0 ? ceil($value) : floor($value); + break; + case self::ROUND_DOWN: + $value = $value > 0 ? floor($value) : ceil($value); + break; + } - $roundingMode = self::$phpRoundingMap[$this->getAttribute(self::ROUNDING_MODE)]; - $value = round($value, $precision, $roundingMode); + $value /= $roundingCoef; + } return $value; } @@ -718,20 +756,20 @@ private function round($value, $precision) */ private function formatNumber($value, $precision) { - $precision = $this->getUnitializedPrecision($value, $precision); + $precision = $this->getUninitializedPrecision($value, $precision); return number_format($value, $precision, '.', $this->getAttribute(self::GROUPING_USED) ? ',' : ''); } /** - * Returns the precision value if the DECIMAL style is being used and the FRACTION_DIGITS attribute is unitialized. + * Returns the precision value if the DECIMAL style is being used and the FRACTION_DIGITS attribute is uninitialized. * - * @param integer|float $value The value to get the precision from if the FRACTION_DIGITS attribute is unitialized + * @param integer|float $value The value to get the precision from if the FRACTION_DIGITS attribute is uninitialized * @param int $precision The precision value to returns if the FRACTION_DIGITS attribute is initialized * * @return int The precision value */ - private function getUnitializedPrecision($value, $precision) + private function getUninitializedPrecision($value, $precision) { if ($this->style == self::CURRENCY) { return $precision; diff --git a/src/Symfony/Component/Intl/README.md b/src/Symfony/Component/Intl/README.md index d7f218f42331a..f5a4ca0432440 100644 --- a/src/Symfony/Component/Intl/README.md +++ b/src/Symfony/Component/Intl/README.md @@ -22,4 +22,4 @@ You can run the unit tests with the following command: $ phpunit [0]: http://www.php.net/manual/en/intl.setup.php -[1]: http://symfony.com/doc/2.3/components/intl.html +[1]: http://symfony.com/doc/2.5/components/intl.html diff --git a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php index 6f6cdfcb189c2..0a7ed806b4246 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php +++ b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php @@ -27,7 +27,7 @@ public function getLocaleName($ofLocale, $locale = null) $locale = \Locale::getDefault(); } - return $this->readEntry($locale, array('Locales', $ofLocale)); + return $this->readEntry($locale, array('Locales', $ofLocale), true); } /** @@ -39,7 +39,7 @@ public function getLocaleNames($locale = null) $locale = \Locale::getDefault(); } - if (null === ($locales = $this->readEntry($locale, array('Locales')))) { + if (null === ($locales = $this->readEntry($locale, array('Locales'), true))) { return array(); } diff --git a/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php b/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php index 9b11d5af0a403..7151bedb8cf5d 100644 --- a/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php +++ b/src/Symfony/Component/Intl/Tests/NumberFormatter/AbstractNumberFormatterTest.php @@ -474,6 +474,102 @@ public function formatRoundingModeRoundHalfEvenProvider() ); } + /** + * @dataProvider formatRoundingModeRoundCeilingProvider + */ + public function testFormatRoundingModeCeiling($value, $expected) + { + $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2); + + $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_CEILING); + $this->assertSame($expected, $formatter->format($value), '->format() with ROUND_CEILING rounding mode.'); + } + + public function formatRoundingModeRoundCeilingProvider() + { + return array( + array(1.123, '1.13'), + array(1.125, '1.13'), + array(1.127, '1.13'), + array(-1.123, '-1.12'), + array(-1.125, '-1.12'), + array(-1.127, '-1.12'), + ); + } + + /** + * @dataProvider formatRoundingModeRoundFloorProvider + */ + public function testFormatRoundingModeFloor($value, $expected) + { + $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2); + + $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_FLOOR); + $this->assertSame($expected, $formatter->format($value), '->format() with ROUND_FLOOR rounding mode.'); + } + + public function formatRoundingModeRoundFloorProvider() + { + return array( + array(1.123, '1.12'), + array(1.125, '1.12'), + array(1.127, '1.12'), + array(-1.123, '-1.13'), + array(-1.125, '-1.13'), + array(-1.127, '-1.13'), + ); + } + + /** + * @dataProvider formatRoundingModeRoundDownProvider + */ + public function testFormatRoundingModeDown($value, $expected) + { + $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2); + + $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_DOWN); + $this->assertSame($expected, $formatter->format($value), '->format() with ROUND_DOWN rounding mode.'); + } + + public function formatRoundingModeRoundDownProvider() + { + return array( + array(1.123, '1.12'), + array(1.125, '1.12'), + array(1.127, '1.12'), + array(-1.123, '-1.12'), + array(-1.125, '-1.12'), + array(-1.127, '-1.12'), + ); + } + + /** + * @dataProvider formatRoundingModeRoundUpProvider + */ + public function testFormatRoundingModeUp($value, $expected) + { + $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL); + $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2); + + $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_UP); + $this->assertSame($expected, $formatter->format($value), '->format() with ROUND_UP rounding mode.'); + } + + public function formatRoundingModeRoundUpProvider() + { + return array( + array(1.123, '1.13'), + array(1.125, '1.13'), + array(1.127, '1.13'), + array(-1.123, '-1.13'), + array(-1.125, '-1.13'), + array(-1.127, '-1.13'), + ); + } + public function testGetLocale() { $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL); diff --git a/src/Symfony/Component/Intl/composer.json b/src/Symfony/Component/Intl/composer.json index 29eedfa1f9d43..9be866e05fcb3 100644 --- a/src/Symfony/Component/Intl/composer.json +++ b/src/Symfony/Component/Intl/composer.json @@ -42,7 +42,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Locale/Tests/LocaleTest.php b/src/Symfony/Component/Locale/Tests/LocaleTest.php index 0d17a9509b067..ad1cceb6a2548 100644 --- a/src/Symfony/Component/Locale/Tests/LocaleTest.php +++ b/src/Symfony/Component/Locale/Tests/LocaleTest.php @@ -34,10 +34,22 @@ public function testGetDisplayCountries() $this->assertEquals('Brazil', $countries['BR']); } + public function testGetDisplayCountriesForSwitzerland() + { + $countries = Locale::getDisplayCountries('de_CH'); + $this->assertEquals('Schweiz', $countries['CH']); + } + public function testGetCountries() { $countries = Locale::getCountries(); - $this->assertTrue(in_array('BR', $countries)); + $this->assertContains('BR', $countries); + } + + public function testGetCountriesForSwitzerland() + { + $countries = Locale::getCountries(); + $this->assertContains('CH', $countries); } public function testGetDisplayLanguages() @@ -49,7 +61,7 @@ public function testGetDisplayLanguages() public function testGetLanguages() { $languages = Locale::getLanguages(); - $this->assertTrue(in_array('pt_BR', $languages)); + $this->assertContains('pt_BR', $languages); } public function testGetDisplayLocales() @@ -61,6 +73,6 @@ public function testGetDisplayLocales() public function testGetLocales() { $locales = Locale::getLocales(); - $this->assertTrue(in_array('pt', $locales)); + $this->assertContains('pt', $locales); } } diff --git a/src/Symfony/Component/Locale/composer.json b/src/Symfony/Component/Locale/composer.json index b43965717374b..26015cea6ef89 100644 --- a/src/Symfony/Component/Locale/composer.json +++ b/src/Symfony/Component/Locale/composer.json @@ -26,7 +26,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index d6554baa0a2ff..9780bd3196951 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -294,8 +294,14 @@ private function validateOptionsCompleteness(array $options) private function validateOptionValues(array $options) { foreach ($this->allowedValues as $option => $allowedValues) { - if (isset($options[$option]) && !in_array($options[$option], $allowedValues, true)) { - throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues))); + if (isset($options[$option])) { + if (is_array($allowedValues) && !in_array($options[$option], $allowedValues, true)) { + throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues))); + } + + if (is_callable($allowedValues) && !call_user_func($allowedValues, $options[$option])) { + throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", which it is not valid', $option, $options[$option])); + } } } } diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index d50cd3fcfe3e4..fc3b3fc5d38e3 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -658,6 +658,45 @@ public function testResolveSucceedsIfOptionRequiredAndValueAllowed() $this->assertEquals($options, $this->resolver->resolve($options)); } + public function testResolveSucceedsIfValueAllowedCallbackReturnsTrue() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return true; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfValueAllowedCallbackReturnsFalse() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return false; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + public function testClone() { $this->resolver->setDefaults(array('one' => '1')); diff --git a/src/Symfony/Component/OptionsResolver/composer.json b/src/Symfony/Component/OptionsResolver/composer.json index f13d246e95936..b0e71ea2997a1 100644 --- a/src/Symfony/Component/OptionsResolver/composer.json +++ b/src/Symfony/Component/OptionsResolver/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Process/CHANGELOG.md b/src/Symfony/Component/Process/CHANGELOG.md index 3bad982fcb124..3d13b99cc01b3 100644 --- a/src/Symfony/Component/Process/CHANGELOG.md +++ b/src/Symfony/Component/Process/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.4.0 +----- + + * added the ability to define an idle timeout + 2.3.0 ----- diff --git a/src/Symfony/Component/Process/Exception/ProcessTimedOutException.php b/src/Symfony/Component/Process/Exception/ProcessTimedOutException.php new file mode 100644 index 0000000000000..d45114696f640 --- /dev/null +++ b/src/Symfony/Component/Process/Exception/ProcessTimedOutException.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process times out. + * + * @author Johannes M. Schmitt + */ +class ProcessTimedOutException extends RuntimeException +{ + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf( + 'The process "%s" exceeded the timeout of %s seconds.', + $process->getCommandLine(), + $this->getExceededTimeout() + )); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return $this->timeoutType === self::TYPE_GENERAL; + } + + public function isIdleTimeout() + { + return $this->timeoutType === self::TYPE_IDLE; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index b83f8b997b700..7895256a1648f 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -13,6 +13,7 @@ use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; /** @@ -45,7 +46,9 @@ class Process private $env; private $stdin; private $starttime; + private $lastOutputTime; private $timeout; + private $idleTimeout; private $options; private $exitcode; private $fallbackExitcode; @@ -181,7 +184,7 @@ public function __clone() * The STDOUT and STDERR are also available after the process is finished * via the getOutput() and getErrorOutput() methods. * - * @param callback|null $callback A PHP callback to run whenever there is some + * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return integer The exit status code @@ -212,9 +215,11 @@ public function run($callback = null) * with true as a second parameter then the callback will get all data occurred * in (and since) the start call. * - * @param callback|null $callback A PHP callback to run whenever there is some + * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * + * @return Process The process itself + * * @throws RuntimeException When process can't be launch or is stopped * @throws RuntimeException When process is already running */ @@ -225,7 +230,7 @@ public function start($callback = null) } $this->resetProcessData(); - $this->starttime = microtime(true); + $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); $descriptors = $this->getDescriptors(); @@ -263,8 +268,8 @@ public function start($callback = null) * * Be warned that the process is cloned before being started. * - * @param callable $callback A PHP callback to run whenever there is some - * output available on STDOUT or STDERR + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR * * @return Process The new process * @@ -292,7 +297,7 @@ public function restart($callback = null) * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * - * @param callback|null $callback A valid PHP callback + * @param callable|null $callback A valid PHP callback * * @return integer The exitcode of the process * @@ -405,6 +410,19 @@ public function getIncrementalOutput() return $latest; } + /** + * Clears the process output. + * + * @return Process + */ + public function clearOutput() + { + $this->stdout = ''; + $this->incrementalOutputOffset = 0; + + return $this; + } + /** * Returns the current error output of the process (STDERR). * @@ -438,6 +456,19 @@ public function getIncrementalErrorOutput() return $latest; } + /** + * Clears the process output. + * + * @return Process + */ + public function clearErrorOutput() + { + $this->stderr = ''; + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + /** * Returns the exit code returned by the process. * @@ -659,6 +690,7 @@ public function stop($timeout = 10, $signal = null) */ public function addOutput($line) { + $this->lastOutputTime = microtime(true); $this->stdout .= $line; } @@ -669,6 +701,7 @@ public function addOutput($line) */ public function addErrorOutput($line) { + $this->lastOutputTime = microtime(true); $this->stderr .= $line; } @@ -697,7 +730,7 @@ public function setCommandLine($commandline) } /** - * Gets the process timeout. + * Gets the process timeout (max. runtime). * * @return float|null The timeout in seconds or null if it's disabled */ @@ -707,7 +740,17 @@ public function getTimeout() } /** - * Sets the process timeout. + * Gets the process idle timeout (max. time since last output). + * + * @return float|null The timeout in seconds or null if it's disabled + */ + public function getIdleTimeout() + { + return $this->idleTimeout; + } + + /** + * Sets the process timeout (max. runtime). * * To disable the timeout, set this value to null. * @@ -719,15 +762,25 @@ public function getTimeout() */ public function setTimeout($timeout) { - $timeout = (float) $timeout; + $this->timeout = $this->validateTimeout($timeout); - if (0.0 === $timeout) { - $timeout = null; - } elseif ($timeout < 0) { - throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); - } + return $this; + } - $this->timeout = $timeout; + /** + * Sets the process idle timeout (max. time since last output). + * + * To disable the timeout, set this value to null. + * + * @param integer|float|null $timeout The timeout in seconds + * + * @return self The current Process instance. + * + * @throws InvalidArgumentException if the timeout is negative + */ + public function setIdleTimeout($timeout) + { + $this->idleTimeout = $this->validateTimeout($timeout); return $this; } @@ -930,14 +983,20 @@ public function setEnhanceSigchildCompatibility($enhance) * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * - * @throws RuntimeException In case the timeout was reached + * @throws ProcessTimedOutException In case the timeout was reached */ public function checkTimeout() { if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { $this->stop(0); - throw new RuntimeException('The process timed-out.'); + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); } } @@ -967,9 +1026,9 @@ private function getDescriptors() * The callbacks adds all occurred output to the specific buffer and calls * the user callback (if present) with the received output. * - * @param callback|null $callback The user defined PHP callback + * @param callable|null $callback The user defined PHP callback * - * @return callback A PHP callable + * @return callable A PHP callable */ protected function buildCallback($callback) { @@ -1030,6 +1089,26 @@ protected function isSigchildEnabled() return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); } + /** + * Validates and returns the filtered timeout. + * + * @param integer|float|null $timeout + * + * @return float|null + */ + private function validateTimeout($timeout) + { + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + /** * Reads pipes, executes callback. * diff --git a/src/Symfony/Component/Process/ProcessBuilder.php b/src/Symfony/Component/Process/ProcessBuilder.php index ddd064a2b877e..b6168feb62d0e 100644 --- a/src/Symfony/Component/Process/ProcessBuilder.php +++ b/src/Symfony/Component/Process/ProcessBuilder.php @@ -23,21 +23,16 @@ class ProcessBuilder { private $arguments; private $cwd; - private $env; + private $env = array(); private $stdin; - private $timeout; - private $options; - private $inheritEnv; - private $prefix; + private $timeout = 60; + private $options = array(); + private $inheritEnv = true; + private $prefix = array(); public function __construct(array $arguments = array()) { $this->arguments = $arguments; - - $this->timeout = 60; - $this->options = array(); - $this->env = array(); - $this->inheritEnv = true; } public static function create(array $arguments = array()) @@ -62,15 +57,15 @@ public function add($argument) /** * Adds an unescaped prefix to the command string. * - * The prefix is preserved when reseting arguments. + * The prefix is preserved when resetting arguments. * - * @param string $prefix A command prefix + * @param string|array $prefix A command prefix or an array of command prefixes * * @return ProcessBuilder */ public function setPrefix($prefix) { - $this->prefix = $prefix; + $this->prefix = is_array($prefix) ? $prefix : array($prefix); return $this; } @@ -108,6 +103,13 @@ public function setEnv($name, $value) return $this; } + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + public function setInput($stdin) { $this->stdin = $stdin; @@ -154,17 +156,18 @@ public function setOption($name, $value) public function getProcess() { - if (!$this->prefix && !count($this->arguments)) { + if (0 === count($this->prefix) && 0 === count($this->arguments)) { throw new LogicException('You must add() command arguments before calling getProcess().'); } $options = $this->options; - $arguments = $this->prefix ? array_merge(array($this->prefix), $this->arguments) : $this->arguments; + $arguments = array_merge($this->prefix, $this->arguments); $script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments)); if ($this->inheritEnv) { - $env = $this->env ? $this->env + $_ENV : null; + // include $_ENV for BC purposes + $env = array_replace($_ENV, $_SERVER, $this->env); } else { $env = $this->env; } diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index c377e084b0945..4e486327b80c5 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Process\Tests; +use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\RuntimeException; @@ -183,6 +184,15 @@ public function testGetIncrementalErrorOutput() } } + public function testFlushErrorOutput() + { + $p = new Process(sprintf('php -r %s', escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + + $p->run(); + $p->clearErrorOutput(); + $this->assertEmpty($p->getErrorOutput()); + } + public function testGetOutput() { $p = new Process(sprintf('php -r %s', escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++; usleep(500); }'))); @@ -202,6 +212,15 @@ public function testGetIncrementalOutput() } } + public function testFlushOutput() + { + $p = new Process(sprintf('php -r %s', escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}'))); + + $p->run(); + $p->clearOutput(); + $this->assertEmpty($p->getOutput()); + } + public function testExitCodeCommandFailed() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { @@ -492,6 +511,45 @@ public function testCheckTimeoutOnStartedProcess() $this->assertFalse($process->isSuccessful()); } + /** + * @group idle-timeout + */ + public function testIdleTimeout() + { + $process = $this->getProcess('sleep 3'); + $process->setTimeout(10); + $process->setIdleTimeout(1); + + try { + $process->run(); + + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $ex) { + $this->assertTrue($ex->isIdleTimeout()); + $this->assertFalse($ex->isGeneralTimeout()); + $this->assertEquals(1.0, $ex->getExceededTimeout()); + } + } + + /** + * @group idle-timeout + */ + public function testIdleTimeoutNotExceededWhenOutputIsSent() + { + $process = $this->getProcess('echo "foo" && sleep 1 && echo "foo" && sleep 1 && echo "foo" && sleep 1 && echo "foo" && sleep 5'); + $process->setTimeout(5); + $process->setIdleTimeout(3); + + try { + $process->run(); + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $ex) { + $this->assertTrue($ex->isGeneralTimeout()); + $this->assertFalse($ex->isIdleTimeout()); + $this->assertEquals(5.0, $ex->getExceededTimeout()); + } + } + public function testStartAfterATimeout() { $process = $this->getProcess('php -r "while (true) {echo \'\'; usleep(1000); }"'); diff --git a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php index 4c88b55ce110a..2982aff9f9886 100644 --- a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php @@ -17,75 +17,51 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase { public function testInheritEnvironmentVars() { - $snapshot = $_ENV; - $_ENV = $expected = array('foo' => 'bar'); + $_ENV['MY_VAR_1'] = 'foo'; - $pb = new ProcessBuilder(); - $pb->add('foo')->inheritEnvironmentVariables(); - $proc = $pb->getProcess(); - - $this->assertNull($proc->getEnv(), '->inheritEnvironmentVariables() copies $_ENV'); - - $_ENV = $snapshot; - } - - public function testProcessShouldInheritAndOverrideEnvironmentVars() - { - $snapshot = $_ENV; - $_ENV = array('foo' => 'bar', 'bar' => 'baz'); - $expected = array('foo' => 'foo', 'bar' => 'baz'); - - $pb = new ProcessBuilder(); - $pb->add('foo')->inheritEnvironmentVariables() - ->setEnv('foo', 'foo'); - $proc = $pb->getProcess(); - - $this->assertEquals($expected, $proc->getEnv(), '->inheritEnvironmentVariables() copies $_ENV'); - - $_ENV = $snapshot; - } - - public function testProcessBuilderShouldNotPassEnvArrays() - { - $snapshot = $_ENV; - $_ENV = array('a' => array('b', 'c'), 'd' => 'e', 'f' => 'g'); - $expected = array('d' => 'e', 'f' => 'g'); - - $pb = new ProcessBuilder(); - $pb->add('a')->inheritEnvironmentVariables() - ->setEnv('d', 'e'); - $proc = $pb->getProcess(); + $proc = ProcessBuilder::create() + ->add('foo') + ->getProcess(); - $this->assertEquals($expected, $proc->getEnv(), '->inheritEnvironmentVariables() removes array values from $_ENV'); + unset($_ENV['MY_VAR_1']); - $_ENV = $snapshot; + $env = $proc->getEnv(); + $this->assertArrayHasKey('MY_VAR_1', $env); + $this->assertEquals('foo', $env['MY_VAR_1']); } - public function testInheritEnvironmentVarsByDefault() + public function testAddEnvironmentVariables() { $pb = new ProcessBuilder(); - $proc = $pb->add('foo')->getProcess(); + $env = array( + 'foo' => 'bar', + 'foo2' => 'bar2', + ); + $proc = $pb + ->add('command') + ->setEnv('foo', 'bar2') + ->addEnvironmentVariables($env) + ->inheritEnvironmentVariables(false) + ->getProcess() + ; - $this->assertNull($proc->getEnv()); + $this->assertSame($env, $proc->getEnv()); } - public function testNotReplaceExplicitlySetVars() + public function testProcessShouldInheritAndOverrideEnvironmentVars() { - $snapshot = $_ENV; - $_ENV = array('foo' => 'bar'); - $expected = array('foo' => 'baz'); + $_ENV['MY_VAR_1'] = 'foo'; - $pb = new ProcessBuilder(); - $pb - ->setEnv('foo', 'baz') - ->inheritEnvironmentVariables() + $proc = ProcessBuilder::create() + ->setEnv('MY_VAR_1', 'bar') ->add('foo') - ; - $proc = $pb->getProcess(); + ->getProcess(); - $this->assertEquals($expected, $proc->getEnv(), '->inheritEnvironmentVariables() copies $_ENV'); + unset($_ENV['MY_VAR_1']); - $_ENV = $snapshot; + $env = $proc->getEnv(); + $this->assertArrayHasKey('MY_VAR_1', $env); + $this->assertEquals('bar', $env['MY_VAR_1']); } /** @@ -140,6 +116,26 @@ public function testPrefixIsPrependedToAllGeneratedProcess() } } + public function testArrayPrefixesArePrependedToAllGeneratedProcess() + { + $pb = new ProcessBuilder(); + $pb->setPrefix(array('/usr/bin/php', 'composer.phar')); + + $proc = $pb->setArguments(array('-v'))->getProcess(); + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->assertEquals('"/usr/bin/php" "composer.phar" "-v"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-v'", $proc->getCommandLine()); + } + + $proc = $pb->setArguments(array('-i'))->getProcess(); + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->assertEquals('"/usr/bin/php" "composer.phar" "-i"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-i'", $proc->getCommandLine()); + } + } + public function testShouldEscapeArguments() { $pb = new ProcessBuilder(array('%path%', 'foo " bar')); diff --git a/src/Symfony/Component/Process/composer.json b/src/Symfony/Component/Process/composer.json index 427e63b87fce4..b5dbfe1390a2b 100644 --- a/src/Symfony/Component/Process/composer.json +++ b/src/Symfony/Component/Process/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/PropertyAccess/CHANGELOG.md b/src/Symfony/Component/PropertyAccess/CHANGELOG.md index 071ef3b5201b0..fb5ebfa55034a 100644 --- a/src/Symfony/Component/PropertyAccess/CHANGELOG.md +++ b/src/Symfony/Component/PropertyAccess/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.5.0 +------ + + * allowed non alpha numeric characters in second level and deeper object properties names + 2.3.0 ------ diff --git a/src/Symfony/Component/PropertyAccess/Exception/AccessException.php b/src/Symfony/Component/PropertyAccess/Exception/AccessException.php new file mode 100644 index 0000000000000..b3a854646efce --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/AccessException.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\PropertyAccess\Exception; + +/** + * Thrown when a property path is not available. + * + * @author Stéphane Escandell + */ +class AccessException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/PropertyAccess/Exception/NoSuchIndexException.php b/src/Symfony/Component/PropertyAccess/Exception/NoSuchIndexException.php new file mode 100644 index 0000000000000..597b9904a22a0 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/NoSuchIndexException.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\PropertyAccess\Exception; + +/** + * Thrown when an index cannot be found. + * + * @author Stéphane Escandell + */ +class NoSuchIndexException extends AccessException +{ +} diff --git a/src/Symfony/Component/PropertyAccess/Exception/NoSuchPropertyException.php b/src/Symfony/Component/PropertyAccess/Exception/NoSuchPropertyException.php index ebaa5a3079e02..1c7eda5f83839 100644 --- a/src/Symfony/Component/PropertyAccess/Exception/NoSuchPropertyException.php +++ b/src/Symfony/Component/PropertyAccess/Exception/NoSuchPropertyException.php @@ -16,6 +16,6 @@ * * @author Bernhard Schussek */ -class NoSuchPropertyException extends RuntimeException +class NoSuchPropertyException extends AccessException { } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 0a2eae259ab06..d48891ef275d8 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyAccess; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; /** @@ -24,15 +25,24 @@ class PropertyAccessor implements PropertyAccessorInterface const VALUE = 0; const IS_REF = 1; + /** + * @var Boolean + */ private $magicCall; + /** + * @var Boolean + */ + private $throwExceptionOnInvalidIndex; + /** * Should not be used by application code. Use - * {@link PropertyAccess::getPropertyAccessor()} instead. + * {@link PropertyAccess::createPropertyAccessor()} instead. */ - public function __construct($magicCall = false) + public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false) { $this->magicCall = $magicCall; + $this->throwExceptionOnInvalidIndex = $throwExceptionOnInvalidIndex; } /** @@ -46,7 +56,7 @@ public function getValue($objectOrArray, $propertyPath) throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); } - $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength()); + $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength(), $this->throwExceptionOnInvalidIndex); return $propertyValues[count($propertyValues) - 1][self::VALUE]; } @@ -106,7 +116,7 @@ public function setValue(&$objectOrArray, $propertyPath, $value) * * @throws UnexpectedTypeException If a value within the path is neither object nor array. */ - private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex) + private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex, $throwExceptionOnNonexistantIndex = false) { $propertyValues = array(); @@ -121,6 +131,9 @@ private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $pr // Create missing nested arrays on demand if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) { + if ($throwExceptionOnNonexistantIndex) { + throw new NoSuchIndexException(sprintf('Cannot read property "%s". Available properties are "%s"', $property, print_r(array_keys($objectOrArray), true))); + } $objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null; } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php index 25686e953feb1..50b872f3a35c7 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorBuilder.php @@ -24,7 +24,12 @@ class PropertyAccessorBuilder private $magicCall = false; /** - * Enables the use of "__call" by the ProperyAccessor. + * @var Boolean + */ + private $throwExceptionOnInvalidIndex = false; + + /** + * Enables the use of "__call" by the PropertyAccessor. * * @return PropertyAccessorBuilder The builder object */ @@ -36,7 +41,7 @@ public function enableMagicCall() } /** - * Disables the use of "__call" by the ProperyAccessor. + * Disables the use of "__call" by the PropertyAccessor. * * @return PropertyAccessorBuilder The builder object */ @@ -48,13 +53,45 @@ public function disableMagicCall() } /** - * @return Boolean true if the use of "__call" by the ProperyAccessor is enabled + * @return Boolean true if the use of "__call" by the PropertyAccessor is enabled */ public function isMagicCallEnabled() { return $this->magicCall; } + /** + * Enables exceptions in read context for array by PropertyAccessor + * + * @return PropertyAccessorBuilder The builder object + */ + public function enableExceptionOnInvalidIndex() + { + $this->throwExceptionOnInvalidIndex = true; + + return $this; + } + + /** + * Disables exceptions in read context for array by PropertyAccessor + * + * @return PropertyAccessorBuilder The builder object + */ + public function disableExceptionOnInvalidIndex() + { + $this->throwExceptionOnInvalidIndex = false; + + return $this; + } + + /** + * @return Boolean true is exceptions in read context for array is enabled + */ + public function isExceptionOnInvalidIndexEnabled() + { + return $this->throwExceptionOnInvalidIndex; + } + /** * Builds and returns a new propertyAccessor object. * @@ -62,6 +99,6 @@ public function isMagicCallEnabled() */ public function getPropertyAccessor() { - return new PropertyAccessor($this->magicCall); + return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex); } } diff --git a/src/Symfony/Component/PropertyAccess/PropertyPath.php b/src/Symfony/Component/PropertyAccess/PropertyPath.php index 840fc71572339..833dd0543bc75 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyPath.php +++ b/src/Symfony/Component/PropertyAccess/PropertyPath.php @@ -118,7 +118,7 @@ public function __construct($propertyPath) $position += strlen($matches[1]); $remaining = $matches[4]; - $pattern = '/^(\.(\w+)|\[([^\]]+)\])(.*)/'; + $pattern = '/^(\.([^\.|\[]+)|\[([^\]]+)\])(.*)/'; } if ('' !== $remaining) { diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index e22ca097be56d..72fbfe428f4fc 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -15,24 +15,49 @@ use Symfony\Component\PropertyAccess\Tests\Fixtures\Author; use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician; use Symfony\Component\PropertyAccess\Tests\Fixtures\MagicianCall; +use Symfony\Component\PropertyAccess\PropertyAccessorBuilder; class PropertyAccessorTest extends \PHPUnit_Framework_TestCase { /** - * @var PropertyAccessor + * @var PropertyAccessorBuilder */ - private $propertyAccessor; + private $propertyAccessorBuilder; protected function setUp() { - $this->propertyAccessor = new PropertyAccessor(); + $this->propertyAccessorBuilder = new PropertyAccessorBuilder(); + } + + /** + * Get PropertyAccessor configured + * + * @param string $withMagicCall + * @param string $throwExceptionOnInvalidIndex + * @return PropertyAccessorInterface + */ + protected function getPropertyAccessor($withMagicCall = false, $throwExceptionOnInvalidIndex = false) + { + if ($withMagicCall) { + $this->propertyAccessorBuilder->enableMagicCall(); + } else { + $this->propertyAccessorBuilder->disableMagicCall(); + } + + if ($throwExceptionOnInvalidIndex) { + $this->propertyAccessorBuilder->enableExceptionOnInvalidIndex(); + } else { + $this->propertyAccessorBuilder->disableExceptionOnInvalidIndex(); + } + + return $this->propertyAccessorBuilder->getPropertyAccessor(); } public function testGetValueReadsArray() { $array = array('firstName' => 'Bernhard'); - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[firstName]')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[firstName]')); } /** @@ -42,42 +67,50 @@ public function testGetValueThrowsExceptionIfIndexNotationExpected() { $array = array('firstName' => 'Bernhard'); - $this->propertyAccessor->getValue($array, 'firstName'); + $this->getPropertyAccessor()->getValue($array, 'firstName'); } public function testGetValueReadsZeroIndex() { $array = array('Bernhard'); - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[0]')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[0]')); } public function testGetValueReadsIndexWithSpecialChars() { $array = array('%!@$§.' => 'Bernhard'); - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[%!@$§.]')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[%!@$§.]')); } public function testGetValueReadsNestedIndexWithSpecialChars() { $array = array('root' => array('%!@$§.' => 'Bernhard')); - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[root][%!@$§.]')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[root][%!@$§.]')); } public function testGetValueReadsArrayWithCustomPropertyPath() { $array = array('child' => array('index' => array('firstName' => 'Bernhard'))); - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[child][index][firstName]')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '[child][index][firstName]')); } public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath() { $array = array('child' => array('index' => array())); - $this->assertNull($this->propertyAccessor->getValue($array, '[child][index][firstName]')); + // No BC break + $this->assertNull($this->getPropertyAccessor()->getValue($array, '[child][index][firstName]')); + + try { + $this->getPropertyAccessor(false, true)->getValue($array, '[child][index][firstName]'); + $this->fail('Getting value on a nonexistent path from array should throw a Symfony\Component\PropertyAccess\Exception\NoSuchIndexException exception'); + } catch (\Exception $e) { + $this->assertInstanceof('Symfony\Component\PropertyAccess\Exception\NoSuchIndexException', $e, 'Getting value on a nonexistent path from array should throw a Symfony\Component\PropertyAccess\Exception\NoSuchIndexException exception'); + } } public function testGetValueReadsProperty() @@ -85,7 +118,7 @@ public function testGetValueReadsProperty() $object = new Author(); $object->firstName = 'Bernhard'; - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, 'firstName')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($object, 'firstName')); } public function testGetValueIgnoresSingular() @@ -94,14 +127,21 @@ public function testGetValueIgnoresSingular() $object = (object) array('children' => 'Many'); - $this->assertEquals('Many', $this->propertyAccessor->getValue($object, 'children|child')); + $this->assertEquals('Many', $this->getPropertyAccessor()->getValue($object, 'children|child')); } public function testGetValueReadsPropertyWithSpecialCharsExceptDot() { $array = (object) array('%!@$§' => 'Bernhard'); - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '%!@$§')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($array, '%!@$§')); + } + + public function testGetValueReadsPropertyWithSpecialCharsExceptDotNested() + { + $object = (object) array('nested' => (object) array('@child' => 'foo')); + + $this->assertEquals('foo', $this->getPropertyAccessor()->getValue($object, 'nested.@child')); } public function testGetValueReadsPropertyWithCustomPropertyPath() @@ -111,7 +151,7 @@ public function testGetValueReadsPropertyWithCustomPropertyPath() $object->child['index'] = new Author(); $object->child['index']->firstName = 'Bernhard'; - $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, 'child[index].firstName')); + $this->assertEquals('Bernhard', $this->getPropertyAccessor()->getValue($object, 'child[index].firstName')); } /** @@ -119,7 +159,7 @@ public function testGetValueReadsPropertyWithCustomPropertyPath() */ public function testGetValueThrowsExceptionIfPropertyIsNotPublic() { - $this->propertyAccessor->getValue(new Author(), 'privateProperty'); + $this->getPropertyAccessor()->getValue(new Author(), 'privateProperty'); } public function testGetValueReadsGetters() @@ -127,7 +167,7 @@ public function testGetValueReadsGetters() $object = new Author(); $object->setLastName('Schussek'); - $this->assertEquals('Schussek', $this->propertyAccessor->getValue($object, 'lastName')); + $this->assertEquals('Schussek', $this->getPropertyAccessor()->getValue($object, 'lastName')); } public function testGetValueCamelizesGetterNames() @@ -135,7 +175,7 @@ public function testGetValueCamelizesGetterNames() $object = new Author(); $object->setLastName('Schussek'); - $this->assertEquals('Schussek', $this->propertyAccessor->getValue($object, 'last_name')); + $this->assertEquals('Schussek', $this->getPropertyAccessor()->getValue($object, 'last_name')); } /** @@ -143,7 +183,7 @@ public function testGetValueCamelizesGetterNames() */ public function testGetValueThrowsExceptionIfGetterIsNotPublic() { - $this->propertyAccessor->getValue(new Author(), 'privateGetter'); + $this->getPropertyAccessor()->getValue(new Author(), 'privateGetter'); } public function testGetValueReadsIssers() @@ -151,7 +191,7 @@ public function testGetValueReadsIssers() $object = new Author(); $object->setAustralian(false); - $this->assertFalse($this->propertyAccessor->getValue($object, 'australian')); + $this->assertFalse($this->getPropertyAccessor()->getValue($object, 'australian')); } public function testGetValueReadHassers() @@ -159,7 +199,7 @@ public function testGetValueReadHassers() $object = new Author(); $object->setReadPermissions(true); - $this->assertTrue($this->propertyAccessor->getValue($object, 'read_permissions')); + $this->assertTrue($this->getPropertyAccessor()->getValue($object, 'read_permissions')); } public function testGetValueReadsMagicGet() @@ -167,7 +207,7 @@ public function testGetValueReadsMagicGet() $object = new Magician(); $object->__set('magicProperty', 'foobar'); - $this->assertSame('foobar', $this->propertyAccessor->getValue($object, 'magicProperty')); + $this->assertSame('foobar', $this->getPropertyAccessor()->getValue($object, 'magicProperty')); } /* @@ -177,7 +217,7 @@ public function testGetValueReadsMagicGetThatReturnsConstant() { $object = new Magician(); - $this->assertNull($this->propertyAccessor->getValue($object, 'magicProperty')); + $this->assertNull($this->getPropertyAccessor()->getValue($object, 'magicProperty')); } /** @@ -185,7 +225,7 @@ public function testGetValueReadsMagicGetThatReturnsConstant() */ public function testGetValueThrowsExceptionIfIsserIsNotPublic() { - $this->propertyAccessor->getValue(new Author(), 'privateIsser'); + $this->getPropertyAccessor()->getValue(new Author(), 'privateIsser'); } /** @@ -193,7 +233,7 @@ public function testGetValueThrowsExceptionIfIsserIsNotPublic() */ public function testGetValueThrowsExceptionIfPropertyDoesNotExist() { - $this->propertyAccessor->getValue(new Author(), 'foobar'); + $this->getPropertyAccessor()->getValue(new Author(), 'foobar'); } /** @@ -201,7 +241,7 @@ public function testGetValueThrowsExceptionIfPropertyDoesNotExist() */ public function testGetValueThrowsExceptionIfNotObjectOrArray() { - $this->propertyAccessor->getValue('baz', 'foobar'); + $this->getPropertyAccessor()->getValue('baz', 'foobar'); } /** @@ -209,7 +249,7 @@ public function testGetValueThrowsExceptionIfNotObjectOrArray() */ public function testGetValueThrowsExceptionIfNull() { - $this->propertyAccessor->getValue(null, 'foobar'); + $this->getPropertyAccessor()->getValue(null, 'foobar'); } /** @@ -217,14 +257,14 @@ public function testGetValueThrowsExceptionIfNull() */ public function testGetValueThrowsExceptionIfEmpty() { - $this->propertyAccessor->getValue('', 'foobar'); + $this->getPropertyAccessor()->getValue('', 'foobar'); } public function testSetValueUpdatesArrays() { $array = array(); - $this->propertyAccessor->setValue($array, '[firstName]', 'Bernhard'); + $this->getPropertyAccessor()->setValue($array, '[firstName]', 'Bernhard'); $this->assertEquals(array('firstName' => 'Bernhard'), $array); } @@ -236,14 +276,14 @@ public function testSetValueThrowsExceptionIfIndexNotationExpected() { $array = array(); - $this->propertyAccessor->setValue($array, 'firstName', 'Bernhard'); + $this->getPropertyAccessor()->setValue($array, 'firstName', 'Bernhard'); } public function testSetValueUpdatesArraysWithCustomPropertyPath() { $array = array(); - $this->propertyAccessor->setValue($array, '[child][index][firstName]', 'Bernhard'); + $this->getPropertyAccessor()->setValue($array, '[child][index][firstName]', 'Bernhard'); $this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array); } @@ -252,7 +292,7 @@ public function testSetValueUpdatesProperties() { $object = new Author(); - $this->propertyAccessor->setValue($object, 'firstName', 'Bernhard'); + $this->getPropertyAccessor()->setValue($object, 'firstName', 'Bernhard'); $this->assertEquals('Bernhard', $object->firstName); } @@ -263,7 +303,7 @@ public function testSetValueUpdatesPropertiesWithCustomPropertyPath() $object->child = array(); $object->child['index'] = new Author(); - $this->propertyAccessor->setValue($object, 'child[index].firstName', 'Bernhard'); + $this->getPropertyAccessor()->setValue($object, 'child[index].firstName', 'Bernhard'); $this->assertEquals('Bernhard', $object->child['index']->firstName); } @@ -272,7 +312,7 @@ public function testSetValueUpdateMagicSet() { $object = new Magician(); - $this->propertyAccessor->setValue($object, 'magicProperty', 'foobar'); + $this->getPropertyAccessor()->setValue($object, 'magicProperty', 'foobar'); $this->assertEquals('foobar', $object->__get('magicProperty')); } @@ -281,7 +321,7 @@ public function testSetValueUpdatesSetters() { $object = new Author(); - $this->propertyAccessor->setValue($object, 'lastName', 'Schussek'); + $this->getPropertyAccessor()->setValue($object, 'lastName', 'Schussek'); $this->assertEquals('Schussek', $object->getLastName()); } @@ -290,17 +330,28 @@ public function testSetValueCamelizesSetterNames() { $object = new Author(); - $this->propertyAccessor->setValue($object, 'last_name', 'Schussek'); + $this->getPropertyAccessor()->setValue($object, 'last_name', 'Schussek'); $this->assertEquals('Schussek', $object->getLastName()); } + public function testSetValueWithSpecialCharsNested() + { + $object = new \stdClass(); + $person = new \stdClass(); + $person->{'@email'} = null; + $object->person = $person; + + $this->getPropertyAccessor()->setValue($object, 'person.@email', 'bar'); + $this->assertEquals('bar', $object->person->{'@email'}); + } + /** * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ public function testSetValueThrowsExceptionIfGetterIsNotPublic() { - $this->propertyAccessor->setValue(new Author(), 'privateSetter', 'foobar'); + $this->getPropertyAccessor()->setValue(new Author(), 'privateSetter', 'foobar'); } /** @@ -310,7 +361,7 @@ public function testSetValueThrowsExceptionIfNotObjectOrArray() { $value = 'baz'; - $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); } /** @@ -320,7 +371,7 @@ public function testSetValueThrowsExceptionIfNull() { $value = null; - $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); } /** @@ -330,7 +381,7 @@ public function testSetValueThrowsExceptionIfEmpty() { $value = ''; - $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); } /** @@ -340,7 +391,7 @@ public function testSetValueFailsIfMagicCallDisabled() { $value = new MagicianCall(); - $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + $this->getPropertyAccessor()->setValue($value, 'foobar', 'bam'); } /** @@ -350,7 +401,7 @@ public function testGetValueFailsIfMagicCallDisabled() { $value = new MagicianCall(); - $this->propertyAccessor->getValue($value, 'foobar', 'bam'); + $this->getPropertyAccessor()->getValue($value, 'foobar', 'bam'); } public function testGetValueReadsMagicCall() diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php index 4e77a913ff851..c6f1fd6e14e38 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php @@ -38,12 +38,26 @@ public function testDotCannotBePresentAtTheBeginning() new PropertyPath('.property'); } + public function providePathsContainingUnexpectedCharacters() + { + return array( + array('property.'), + array('property.['), + array('property..'), + array('property['), + array('property[['), + array('property[.'), + array('property[]'), + ); + } + /** + * @dataProvider providePathsContainingUnexpectedCharacters * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException */ - public function testUnexpectedCharacters() + public function testUnexpectedCharacters($path) { - new PropertyPath('property.$foo'); + new PropertyPath($path); } /** diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index 318604274a3df..a34297fd9ed11 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Routing/Annotation/Route.php b/src/Symfony/Component/Routing/Annotation/Route.php index abdbea27c6996..ebda0971c5fda 100644 --- a/src/Symfony/Component/Routing/Annotation/Route.php +++ b/src/Symfony/Component/Routing/Annotation/Route.php @@ -22,12 +22,13 @@ class Route { private $path; private $name; - private $requirements; - private $options; - private $defaults; + private $requirements = array(); + private $options = array(); + private $defaults = array(); private $host; - private $methods; - private $schemes; + private $methods = array(); + private $schemes = array(); + private $condition; /** * Constructor. @@ -38,12 +39,6 @@ class Route */ public function __construct(array $data) { - $this->requirements = array(); - $this->options = array(); - $this->defaults = array(); - $this->methods = array(); - $this->schemes = array(); - if (isset($data['value'])) { $data['path'] = $data['value']; unset($data['value']); @@ -153,4 +148,14 @@ public function getMethods() { return $this->methods; } + + public function setCondition($condition) + { + $this->condition = $condition; + } + + public function getCondition() + { + return $this->condition; + } } diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index f0c616d080b5e..8b604ead11510 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +2.5.0 +----- + + * [DEPRECATION] The `ApacheMatcherDumper` and `ApacheUrlMatcher` were deprecated and + will be removed in Symfony 3.0, since the performance gains were minimal and + it's hard to replicate the behaviour of PHP implementation. + 2.3.0 ----- diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php index c52fab4b7b183..8d4203019c4f1 100644 --- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php +++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php @@ -92,6 +92,7 @@ private function generateDeclaredRoutes() $properties[] = $route->getRequirements(); $properties[] = $compiledRoute->getTokens(); $properties[] = $compiledRoute->getHostTokens(); + $properties[] = $route->getSchemes(); $routes .= sprintf(" '%s' => %s,\n", $name, str_replace("\n", '', var_export($properties, true))); } @@ -114,9 +115,9 @@ public function generate(\$name, \$parameters = array(), \$referenceType = self: throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', \$name)); } - list(\$variables, \$defaults, \$requirements, \$tokens, \$hostTokens) = self::\$declaredRoutes[\$name]; + list(\$variables, \$defaults, \$requirements, \$tokens, \$hostTokens, \$requiredSchemes) = self::\$declaredRoutes[\$name]; - return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$referenceType, \$hostTokens); + return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$referenceType, \$hostTokens, \$requiredSchemes); } EOF; } diff --git a/src/Symfony/Component/Routing/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php index ac64abc6aaa59..468708479bbe9 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php @@ -137,7 +137,7 @@ public function generate($name, $parameters = array(), $referenceType = self::AB // the Route has a cache of its own and is not recompiled as long as it does not get modified $compiledRoute = $route->compile(); - return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens()); + return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes()); } /** @@ -145,7 +145,7 @@ public function generate($name, $parameters = array(), $referenceType = self::AB * @throws InvalidParameterException When a parameter value for a placeholder is not correct because * it does not match the requirement */ - protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens) + protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array()) { $variables = array_flip($variables); $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); @@ -204,7 +204,24 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa $schemeAuthority = ''; if ($host = $this->context->getHost()) { $scheme = $this->context->getScheme(); - if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme !== $req) { + + if ($requiredSchemes) { + $schemeMatched = false; + foreach ($requiredSchemes as $requiredScheme) { + if ($scheme === $requiredScheme) { + $schemeMatched = true; + + break; + } + } + + if (!$schemeMatched) { + $referenceType = self::ABSOLUTE_URL; + $scheme = current($requiredSchemes); + } + + } elseif (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme !== $req) { + // We do this for BC; to be removed if _scheme is not supported anymore $referenceType = self::ABSOLUTE_URL; $scheme = $req; } diff --git a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php index 7abab5a631abd..2c5fb18199339 100644 --- a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php +++ b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php @@ -108,53 +108,12 @@ public function load($class, $type = null) throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); } - $globals = array( - 'path' => '', - 'requirements' => array(), - 'options' => array(), - 'defaults' => array(), - 'schemes' => array(), - 'methods' => array(), - 'host' => '', - ); - $class = new \ReflectionClass($class); if ($class->isAbstract()) { throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class)); } - if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { - // for BC reasons - if (null !== $annot->getPath()) { - $globals['path'] = $annot->getPath(); - } elseif (null !== $annot->getPattern()) { - $globals['path'] = $annot->getPattern(); - } - - if (null !== $annot->getRequirements()) { - $globals['requirements'] = $annot->getRequirements(); - } - - if (null !== $annot->getOptions()) { - $globals['options'] = $annot->getOptions(); - } - - if (null !== $annot->getDefaults()) { - $globals['defaults'] = $annot->getDefaults(); - } - - if (null !== $annot->getSchemes()) { - $globals['schemes'] = $annot->getSchemes(); - } - - if (null !== $annot->getMethods()) { - $globals['methods'] = $annot->getMethods(); - } - - if (null !== $annot->getHost()) { - $globals['host'] = $annot->getHost(); - } - } + $globals = $this->getGlobals($class); $collection = new RouteCollection(); $collection->addResource(new FileResource($class->getFileName())); @@ -194,7 +153,12 @@ protected function addRoute(RouteCollection $collection, $annot, $globals, \Refl $host = $globals['host']; } - $route = new Route($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods); + $condition = $annot->getCondition(); + if (null === $condition) { + $condition = $globals['condition']; + } + + $route = $this->createRoute($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods, $condition); $this->configureRoute($route, $class, $method, $annot); @@ -242,5 +206,63 @@ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMetho return $name; } + protected function getGlobals(\ReflectionClass $class) + { + $globals = array( + 'path' => '', + 'requirements' => array(), + 'options' => array(), + 'defaults' => array(), + 'schemes' => array(), + 'methods' => array(), + 'host' => '', + 'condition' => '', + ); + + if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) { + // for BC reasons + if (null !== $annot->getPath()) { + $globals['path'] = $annot->getPath(); + } elseif (null !== $annot->getPattern()) { + $globals['path'] = $annot->getPattern(); + } + + if (null !== $annot->getRequirements()) { + $globals['requirements'] = $annot->getRequirements(); + } + + if (null !== $annot->getOptions()) { + $globals['options'] = $annot->getOptions(); + } + + if (null !== $annot->getDefaults()) { + $globals['defaults'] = $annot->getDefaults(); + } + + if (null !== $annot->getSchemes()) { + $globals['schemes'] = $annot->getSchemes(); + } + + if (null !== $annot->getMethods()) { + $globals['methods'] = $annot->getMethods(); + } + + if (null !== $annot->getHost()) { + $globals['host'] = $annot->getHost(); + } + + if (null !== $annot->getCondition()) { + $globals['condition'] = $annot->getCondition(); + } + } + + return $globals; + } + + protected function createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition) + { + return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + } + abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot); } diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index da7b33d856ff1..5838b8d3416c2 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -129,9 +129,9 @@ protected function parseRoute(RouteCollection $collection, \DOMElement $node, $p $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY); $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY); - list($defaults, $requirements, $options) = $this->parseConfigs($node, $path); + list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path); - $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods); + $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition); $collection->add($id, $route); } @@ -157,7 +157,7 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null; $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null; - list($defaults, $requirements, $options) = $this->parseConfigs($node, $path); + list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path); $this->setCurrentDir(dirname($path)); @@ -167,6 +167,9 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, $ if (null !== $host) { $subCollection->setHost($host); } + if (null !== $condition) { + $subCollection->setCondition($condition); + } if (null !== $schemes) { $subCollection->setSchemes($schemes); } @@ -211,6 +214,7 @@ private function parseConfigs(\DOMElement $node, $path) $defaults = array(); $requirements = array(); $options = array(); + $condition = null; foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) { switch ($n->localName) { @@ -228,11 +232,14 @@ private function parseConfigs(\DOMElement $node, $path) case 'option': $options[$n->getAttribute('key')] = trim($n->textContent); break; + case 'condition': + $condition = trim($n->textContent); + break; default: throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement" or "option".', $n->localName, $path)); } } - return array($defaults, $requirements, $options); + return array($defaults, $requirements, $options, $condition); } } diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index 9deea7fe4f552..8dca68b7a7497 100644 --- a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -28,7 +28,7 @@ class YamlFileLoader extends FileLoader { private static $availableKeys = array( - 'resource', 'type', 'prefix', 'pattern', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', + 'resource', 'type', 'prefix', 'pattern', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition' ); private $yamlParser; @@ -123,8 +123,9 @@ protected function parseRoute(RouteCollection $collection, $name, array $config, $host = isset($config['host']) ? $config['host'] : ''; $schemes = isset($config['schemes']) ? $config['schemes'] : array(); $methods = isset($config['methods']) ? $config['methods'] : array(); + $condition = isset($config['condition']) ? $config['condition'] : null; - $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods); + $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition); $collection->add($name, $route); } @@ -145,6 +146,7 @@ protected function parseImport(RouteCollection $collection, array $config, $path $requirements = isset($config['requirements']) ? $config['requirements'] : array(); $options = isset($config['options']) ? $config['options'] : array(); $host = isset($config['host']) ? $config['host'] : null; + $condition = isset($config['condition']) ? $config['condition'] : null; $schemes = isset($config['schemes']) ? $config['schemes'] : null; $methods = isset($config['methods']) ? $config['methods'] : null; @@ -156,6 +158,9 @@ protected function parseImport(RouteCollection $collection, array $config, $path if (null !== $host) { $subCollection->setHost($host); } + if (null !== $condition) { + $subCollection->setCondition($condition); + } if (null !== $schemes) { $subCollection->setSchemes($schemes); } diff --git a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd index daea8143865c8..9ab969a41d8a2 100644 --- a/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd +++ b/src/Symfony/Component/Routing/Loader/schema/routing/routing-1.0.xsd @@ -29,6 +29,7 @@ + @@ -61,4 +62,9 @@ + + + + + diff --git a/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php index 55aac6b7079dc..35b1dc50ed2d7 100644 --- a/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/ApacheUrlMatcher.php @@ -16,6 +16,10 @@ /** * ApacheUrlMatcher matches URL based on Apache mod_rewrite matching (see ApacheMatcherDumper). * + * @deprecated Deprecated since version 2.5, to be removed in 3.0. + * The performance gains are minimal and it's very hard to replicate + * the behavior of PHP implementation. + * * @author Fabien Potencier * @author Arnaud Le Blanc */ diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php index 01d8c03589476..fc76c192778c5 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/ApacheMatcherDumper.php @@ -16,6 +16,10 @@ /** * Dumps a set of Apache mod_rewrite rules. * + * @deprecated Deprecated since version 2.5, to be removed in 3.0. + * The performance gains are minimal and it's very hard to replicate + * the behavior of PHP implementation. + * * @author Fabien Potencier * @author Kris Wallsmith */ @@ -50,6 +54,9 @@ public function dump(array $options = array()) $prevHostRegex = ''; foreach ($this->getRoutes()->all() as $name => $route) { + if ($route->getCondition()) { + throw new \LogicException(sprintf('Unable to dump the routes for Apache as route "%s" has a condition.', $name)); + } $compiledRoute = $route->compile(); $hostRegex = $compiledRoute->getHostRegex(); diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php index dc17ffbe984a3..784f3099daf59 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php @@ -13,6 +13,7 @@ use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes. @@ -23,6 +24,8 @@ */ class PhpMatcherDumper extends MatcherDumper { + private $expressionLanguage; + /** * Dumps a set of routes to a PHP class. * @@ -91,6 +94,8 @@ public function match(\$pathinfo) { \$allow = array(); \$pathinfo = rawurldecode(\$pathinfo); + \$context = \$this->context; + \$request = \$this->request; $code @@ -237,6 +242,10 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren $hostMatches = true; } + if ($route->getCondition()) { + $conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request')); + } + $conditions = implode(' && ', $conditions); $code .= <<context->getMethod() != '$methods[0]') { @@ -280,14 +288,15 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren EOF; } - if ($scheme = $route->getRequirement('_scheme')) { + if ($schemes = $route->getSchemes()) { if (!$supportsRedirections) { - throw new \LogicException('The "_scheme" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.'); + throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.'); } - + $schemes = str_replace("\n", '', var_export(array_flip($schemes), true)); $code .= <<context->getScheme() !== '$scheme') { - return \$this->redirect(\$pathinfo, '$name', '$scheme'); + \$requiredSchemes = $schemes; + if (!isset(\$requiredSchemes[\$this->context->getScheme()])) { + return \$this->redirect(\$pathinfo, '$name', key(\$requiredSchemes)); } @@ -305,8 +314,11 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren } $vars[] = "array('_route' => '$name')"; - $code .= sprintf(" return \$this->mergeDefaults(array_replace(%s), %s);\n" - , implode(', ', $vars), str_replace("\n", '', var_export($route->getDefaults(), true))); + $code .= sprintf( + " return \$this->mergeDefaults(array_replace(%s), %s);\n", + implode(', ', $vars), + str_replace("\n", '', var_export($route->getDefaults(), true)) + ); } elseif ($route->getDefaults()) { $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true))); @@ -375,4 +387,16 @@ private function buildPrefixTree(DumperCollection $collection) return $tree; } + + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } } diff --git a/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php index 51e80057cd53b..3d13181f467dd 100644 --- a/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/RedirectableUrlMatcher.php @@ -50,10 +50,16 @@ public function match($pathinfo) */ protected function handleRouteRequirements($pathinfo, $name, Route $route) { + // expression condition + if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request))) { + return array(self::REQUIREMENT_MISMATCH, null); + } + // check HTTP scheme requirement - $scheme = $route->getRequirement('_scheme'); - if ($scheme && $this->context->getScheme() !== $scheme) { - return array(self::ROUTE_MATCH, $this->redirect($pathinfo, $name, $scheme)); + $scheme = $this->context->getScheme(); + $schemes = $route->getSchemes(); + if ($schemes && !$route->hasScheme($scheme)) { + return array(self::ROUTE_MATCH, $this->redirect($pathinfo, $name, current($schemes))); } return array(self::REQUIREMENT_MATCH, null); diff --git a/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php b/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php index ff560c76cae5c..22af699d4a7c2 100644 --- a/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/TraceableUrlMatcher.php @@ -93,10 +93,21 @@ protected function matchCollection($pathinfo, RouteCollection $routes) } } + // check condition + if ($condition = $route->getCondition()) { + if (!$this->getExpressionLanguage()->evaluate($condition, array('context' => $this->context, 'request' => $this->request))) { + $this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $condition), self::ROUTE_ALMOST_MATCHES, $name, $route); + + continue; + } + } + // check HTTP scheme requirement - if ($scheme = $route->getRequirement('_scheme')) { - if ($this->context->getScheme() !== $scheme) { - $this->addTrace(sprintf('Scheme "%s" does not match the requirement ("%s"); the user will be redirected', $this->context->getScheme(), $scheme), self::ROUTE_ALMOST_MATCHES, $name, $route); + if ($requiredSchemes = $route->getSchemes()) { + $scheme = $this->context->getScheme(); + + if (!$route->hasScheme($scheme)) { + $this->addTrace(sprintf('Scheme "%s" does not match any of the required schemes ("%s"); the user will be redirected to first required scheme', $scheme, implode(', ', $requiredSchemes)), self::ROUTE_ALMOST_MATCHES, $name, $route); return true; } diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index db18ec4e7b259..8d081a8629de5 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -16,6 +16,8 @@ use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * UrlMatcher matches URL based on a set of routes. @@ -24,7 +26,7 @@ * * @api */ -class UrlMatcher implements UrlMatcherInterface +class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface { const REQUIREMENT_MATCH = 0; const REQUIREMENT_MISMATCH = 1; @@ -45,6 +47,9 @@ class UrlMatcher implements UrlMatcherInterface */ protected $routes; + protected $request; + protected $expressionLanguage; + /** * Constructor. * @@ -91,6 +96,20 @@ public function match($pathinfo) : new ResourceNotFoundException(); } + /** + * {@inheritdoc} + */ + public function matchRequest(Request $request) + { + $this->request = $request; + + $ret = $this->match($request->getPathInfo()); + + $this->request = null; + + return $ret; + } + /** * Tries to match a URL with a set of routes. * @@ -180,9 +199,14 @@ protected function getAttributes(Route $route, $name, array $attributes) */ protected function handleRouteRequirements($pathinfo, $name, Route $route) { + // expression condition + if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), array('context' => $this->context, 'request' => $this->request))) { + return array(self::REQUIREMENT_MISMATCH, null); + } + // check HTTP scheme requirement - $scheme = $route->getRequirement('_scheme'); - $status = $scheme && $scheme !== $this->context->getScheme() ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH; + $scheme = $this->context->getScheme(); + $status = $route->getSchemes() && !$route->hasScheme($scheme) ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH; return array($status, null); } @@ -205,4 +229,16 @@ protected function mergeDefaults($params, $defaults) return $defaults; } + + protected function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } } diff --git a/src/Symfony/Component/Routing/Route.php b/src/Symfony/Component/Routing/Route.php index 5bc535c683ed2..08005541461ef 100644 --- a/src/Symfony/Component/Routing/Route.php +++ b/src/Symfony/Component/Routing/Route.php @@ -61,6 +61,8 @@ class Route implements \Serializable */ private $compiled; + private $condition; + /** * Constructor. * @@ -75,10 +77,11 @@ class Route implements \Serializable * @param string $host The host pattern to match * @param string|array $schemes A required URI scheme or an array of restricted schemes * @param string|array $methods A required HTTP method or an array of restricted methods + * @param string $condition A condition that should evaluate to true for the route to match * * @api */ - public function __construct($path, array $defaults = array(), array $requirements = array(), array $options = array(), $host = '', $schemes = array(), $methods = array()) + public function __construct($path, array $defaults = array(), array $requirements = array(), array $options = array(), $host = '', $schemes = array(), $methods = array(), $condition = null) { $this->setPath($path); $this->setDefaults($defaults); @@ -93,6 +96,7 @@ public function __construct($path, array $defaults = array(), array $requirement if ($methods) { $this->setMethods($methods); } + $this->setCondition($condition); } public function serialize() @@ -105,6 +109,7 @@ public function serialize() 'options' => $this->options, 'schemes' => $this->schemes, 'methods' => $this->methods, + 'condition' => $this->condition, )); } @@ -118,6 +123,7 @@ public function unserialize($data) $this->options = $data['options']; $this->schemes = $data['schemes']; $this->methods = $data['methods']; + $this->condition = $data['condition']; } /** @@ -241,6 +247,25 @@ public function setSchemes($schemes) return $this; } + /** + * Checks if a scheme requirement has been set. + * + * @param string $scheme + * + * @return Boolean true if the scheme requirement exists, otherwise false + */ + public function hasScheme($scheme) + { + $scheme = strtolower($scheme); + foreach ($this->schemes as $requiredScheme) { + if ($scheme === $requiredScheme) { + return true; + } + } + + return false; + } + /** * Returns the uppercased HTTP methods this route is restricted to. * So an empty array means that any method is allowed. @@ -543,6 +568,33 @@ public function setRequirement($key, $regex) return $this; } + /** + * Returns the condition. + * + * @return string The condition + */ + public function getCondition() + { + return $this->condition; + } + + /** + * Sets the condition. + * + * This method implements a fluent interface. + * + * @param string $condition The condition + * + * @return Route The current Route instance + */ + public function setCondition($condition) + { + $this->condition = (string) $condition; + $this->compiled = null; + + return $this; + } + /** * Compiles the route. * diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index 499fe0f04f83a..f02832424236d 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -177,6 +177,20 @@ public function setHost($pattern, array $defaults = array(), array $requirements } } + /** + * Sets a condition on all routes. + * + * Existing conditions will be overridden. + * + * @param string $condition The condition + */ + public function setCondition($condition) + { + foreach ($this->routes as $route) { + $route->setCondition($condition); + } + } + /** * Adds defaults to all routes. * diff --git a/src/Symfony/Component/Routing/Router.php b/src/Symfony/Component/Routing/Router.php index 35b6e2a54fef0..62abec387245f 100644 --- a/src/Symfony/Component/Routing/Router.php +++ b/src/Symfony/Component/Routing/Router.php @@ -16,7 +16,11 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface; +use Symfony\Component\Routing\Matcher\RequestMatcherInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; +use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface; +use Symfony\Component\HttpFoundation\Request; /** * The Router class is an example of the integration of all pieces of the @@ -24,7 +28,7 @@ * * @author Fabien Potencier */ -class Router implements RouterInterface +class Router implements RouterInterface, RequestMatcherInterface { /** * @var UrlMatcherInterface|null @@ -80,7 +84,7 @@ public function __construct(LoaderInterface $loader, $resource, array $options = $this->loader = $loader; $this->resource = $resource; $this->logger = $logger; - $this->context = null === $context ? new RequestContext() : $context; + $this->context = $context ?: new RequestContext(); $this->setOptions($options); } @@ -215,6 +219,20 @@ public function match($pathinfo) return $this->getMatcher()->match($pathinfo); } + /** + * {@inheritdoc} + */ + public function matchRequest(Request $request) + { + $matcher = $this->getMatcher(); + if (!$matcher instanceof RequestMatcherInterface) { + // fallback to the default UrlMatcherInterface + return $matcher->match($request->getPathInfo()); + } + + return $matcher->matchRequest($request); + } + /** * Gets the UrlMatcher instance associated with this Router. * @@ -233,7 +251,7 @@ public function getMatcher() $class = $this->options['matcher_cache_class']; $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); if (!$cache->isFresh()) { - $dumper = new $this->options['matcher_dumper_class']($this->getRouteCollection()); + $dumper = $this->getMatcherDumperInstance(); $options = array( 'class' => $class, @@ -265,7 +283,7 @@ public function getGenerator() $class = $this->options['generator_cache_class']; $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); if (!$cache->isFresh()) { - $dumper = new $this->options['generator_dumper_class']($this->getRouteCollection()); + $dumper = $this->getGeneratorDumperInstance(); $options = array( 'class' => $class, @@ -286,4 +304,20 @@ public function getGenerator() return $this->generator; } + + /** + * @return GeneratorDumperInterface + */ + protected function getGeneratorDumperInstance() + { + return new $this->options['generator_dumper_class']($this->getRouteCollection()); + } + + /** + * @return MatcherDumperInterface + */ + protected function getMatcherDumperInstance() + { + return new $this->options['matcher_dumper_class']($this->getRouteCollection()); + } } diff --git a/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php b/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php index b58869f7afc69..bd0167089e05d 100644 --- a/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/Annotation/RouteTest.php @@ -35,15 +35,16 @@ public function testRouteParameters($parameter, $value, $getter) public function getValidParameters() { return array( - array('value', '/Blog', 'getPattern'), - array('value', '/Blog', 'getPath'), - array('requirements', array('_method' => 'GET'), 'getRequirements'), - array('options', array('compiler_class' => 'RouteCompiler'), 'getOptions'), - array('name', 'blog_index', 'getName'), - array('defaults', array('_controller' => 'MyBlogBundle:Blog:index'), 'getDefaults'), - array('schemes', array('https'), 'getSchemes'), - array('methods', array('GET', 'POST'), 'getMethods'), - array('host', array('{locale}.example.com'), 'getHost') + array('value', '/Blog', 'getPattern'), + array('value', '/Blog', 'getPath'), + array('requirements', array('_method' => 'GET'), 'getRequirements'), + array('options', array('compiler_class' => 'RouteCompiler'), 'getOptions'), + array('name', 'blog_index', 'getName'), + array('defaults', array('_controller' => 'MyBlogBundle:Blog:index'), 'getDefaults'), + array('schemes', array('https'), 'getSchemes'), + array('methods', array('GET', 'POST'), 'getMethods'), + array('host', array('{locale}.example.com'), 'getHost'), + array('condition', array('context.getMethod() == "GET"'), 'getCondition'), ); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php index e5f7665c81bc9..248a4f153fc3e 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher1.php @@ -24,6 +24,8 @@ public function match($pathinfo) { $allow = array(); $pathinfo = rawurldecode($pathinfo); + $context = $this->context; + $request = $this->request; // foo if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?Pbaz|symfony)$#s', $pathinfo, $matches)) { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php index ad157909b8c0e..acf1163c5b390 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher2.php @@ -24,6 +24,8 @@ public function match($pathinfo) { $allow = array(); $pathinfo = rawurldecode($pathinfo); + $context = $this->context; + $request = $this->request; // foo if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?Pbaz|symfony)$#s', $pathinfo, $matches)) { @@ -319,8 +321,9 @@ public function match($pathinfo) // secure if ($pathinfo === '/secure') { - if ($this->context->getScheme() !== 'https') { - return $this->redirect($pathinfo, 'secure', 'https'); + $requiredSchemes = array ( 'https' => 0,); + if (!isset($requiredSchemes[$this->context->getScheme()])) { + return $this->redirect($pathinfo, 'secure', key($requiredSchemes)); } return array('_route' => 'secure'); @@ -328,8 +331,9 @@ public function match($pathinfo) // nonsecure if ($pathinfo === '/nonsecure') { - if ($this->context->getScheme() !== 'http') { - return $this->redirect($pathinfo, 'nonsecure', 'http'); + $requiredSchemes = array ( 'http' => 0,); + if (!isset($requiredSchemes[$this->context->getScheme()])) { + return $this->redirect($pathinfo, 'nonsecure', key($requiredSchemes)); } return array('_route' => 'nonsecure'); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php index f2f642eb86691..c3463fa77208a 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/dumper/url_matcher3.php @@ -24,6 +24,8 @@ public function match($pathinfo) { $allow = array(); $pathinfo = rawurldecode($pathinfo); + $context = $this->context; + $request = $this->request; if (0 === strpos($pathinfo, '/rootprefix')) { // static @@ -38,6 +40,11 @@ public function match($pathinfo) } + // with-condition + if ($pathinfo === '/with-condition' && ($context->getMethod() == "GET")) { + return array('_route' => 'with-condition'); + } + throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException(); } } diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.php b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.php index b8bbbb5f8f01d..b652d94656599 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.php @@ -10,14 +10,18 @@ array('compiler_class' => 'RouteCompiler'), '{locale}.example.com', array('https'), - array('GET','POST','put','OpTiOnS') + array('GET','POST','put','OpTiOnS'), + 'context.getMethod() == "GET"' )); $collection->add('blog_show_legacy', new Route( '/blog/{slug}', array('_controller' => 'MyBlogBundle:Blog:show'), array('_method' => 'GET|POST|put|OpTiOnS', '_scheme' => 'https', 'locale' => '\w+',), array('compiler_class' => 'RouteCompiler'), - '{locale}.example.com' + '{locale}.example.com', + array(), + array(), + 'context.getMethod() == "GET"' )); return $collection; diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml index b9f22347b40cb..a8221314cb559 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml @@ -8,6 +8,7 @@ MyBundle:Blog:show \w+ + context.getMethod() == "GET" @@ -17,5 +18,8 @@ hTTps \w+ + context.getMethod() == "GET" + + diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.yml b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.yml index 4ada8832197b8..26136c3969d9c 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.yml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.yml @@ -5,6 +5,7 @@ blog_show: requirements: { 'locale': '\w+' } methods: ['GET','POST','put','OpTiOnS'] schemes: ['https'] + condition: 'context.getMethod() == "GET"' options: compiler_class: RouteCompiler @@ -13,5 +14,9 @@ blog_show_legacy: defaults: { _controller: "MyBundle:Blog:show" } host: "{locale}.example.com" requirements: { '_method': 'GET|POST|put|OpTiOnS', _scheme: https, 'locale': '\w+' } + condition: 'context.getMethod() == "GET"' options: compiler_class: RouteCompiler + +blog_show_inherited: + path: /blog/{slug} diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.xml b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.xml index 295c3cc428a6e..b7a15ddc7ee1f 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.xml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.xml @@ -8,5 +8,6 @@ 123 \d+ + context.getMethod() == "POST" diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml index 495ed854d1dd1..faf2263ae52f4 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml +++ b/src/Symfony/Component/Routing/Tests/Fixtures/validresource.yml @@ -5,3 +5,4 @@ _blog: requirements: { 'foo': '\d+' } options: { 'foo': 'bar' } host: "" + condition: 'context.getMethod() == "POST"' diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php index ab5f4cdae0aab..78e3907fd56bf 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/PhpGeneratorDumperTest.php @@ -114,4 +114,37 @@ public function testDumpForRouteWithDefaults() $this->assertEquals($url, '/testing'); } + + public function testDumpWithSchemeRequirement() + { + $this->routeCollection->add('Test1', new Route('/testing', array(), array(), array(), '', array('ftp', 'https'))); + $this->routeCollection->add('Test2', new Route('/testing_bc', array(), array('_scheme' => 'https'))); // BC + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump(array('class' => 'SchemeUrlGenerator'))); + include ($this->testTmpFilepath); + + $projectUrlGenerator = new \SchemeUrlGenerator(new RequestContext('/app.php')); + + $absoluteUrl = $projectUrlGenerator->generate('Test1', array(), true); + $absoluteUrlBC = $projectUrlGenerator->generate('Test2', array(), true); + $relativeUrl = $projectUrlGenerator->generate('Test1', array(), false); + $relativeUrlBC = $projectUrlGenerator->generate('Test2', array(), false); + + $this->assertEquals($absoluteUrl, 'ftp://localhost/app.php/testing'); + $this->assertEquals($absoluteUrlBC, 'https://localhost/app.php/testing_bc'); + $this->assertEquals($relativeUrl, 'ftp://localhost/app.php/testing'); + $this->assertEquals($relativeUrlBC, 'https://localhost/app.php/testing_bc'); + + $projectUrlGenerator = new \SchemeUrlGenerator(new RequestContext('/app.php', 'GET', 'localhost', 'https')); + + $absoluteUrl = $projectUrlGenerator->generate('Test1', array(), true); + $absoluteUrlBC = $projectUrlGenerator->generate('Test2', array(), true); + $relativeUrl = $projectUrlGenerator->generate('Test1', array(), false); + $relativeUrlBC = $projectUrlGenerator->generate('Test2', array(), false); + + $this->assertEquals($absoluteUrl, 'https://localhost/app.php/testing'); + $this->assertEquals($absoluteUrlBC, 'https://localhost/app.php/testing_bc'); + $this->assertEquals($relativeUrl, '/app.php/testing'); + $this->assertEquals($relativeUrlBC, '/app.php/testing_bc'); + } } diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index 5f8ef491276c2..143e3448a29c1 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php @@ -207,10 +207,6 @@ public function testGenerateForRouteWithInvalidOptionalParameterNonStrict() public function testGenerateForRouteWithInvalidOptionalParameterNonStrictWithLogger() { - if (!interface_exists('Psr\Log\LoggerInterface')) { - $this->markTestSkipped('The "psr/log" package is not available'); - } - $routes = $this->getRoutes('test', new Route('/testing/{foo}', array('foo' => '1'), array('foo' => 'd+'))); $logger = $this->getMock('Psr\Log\LoggerInterface'); $logger->expects($this->once()) @@ -248,22 +244,40 @@ public function testRequiredParamAndEmptyPassed() public function testSchemeRequirementDoesNothingIfSameCurrentScheme() { - $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http'))); + $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http'))); // BC + $this->assertEquals('/app.php/', $this->getGenerator($routes)->generate('test')); + + $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https'))); // BC + $this->assertEquals('/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test')); + + $routes = $this->getRoutes('test', new Route('/', array(), array(), array(), '', array('http'))); $this->assertEquals('/app.php/', $this->getGenerator($routes)->generate('test')); - $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https'))); + $routes = $this->getRoutes('test', new Route('/', array(), array(), array(), '', array('https'))); $this->assertEquals('/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test')); } public function testSchemeRequirementForcesAbsoluteUrl() { - $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https'))); + $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https'))); // BC + $this->assertEquals('https://localhost/app.php/', $this->getGenerator($routes)->generate('test')); + + $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http'))); // BC + $this->assertEquals('http://localhost/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test')); + + $routes = $this->getRoutes('test', new Route('/', array(), array(), array(), '', array('https'))); $this->assertEquals('https://localhost/app.php/', $this->getGenerator($routes)->generate('test')); - $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http'))); + $routes = $this->getRoutes('test', new Route('/', array(), array(), array(), '', array('http'))); $this->assertEquals('http://localhost/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test')); } + public function testSchemeRequirementCreatesUrlForFirstRequiredScheme() + { + $routes = $this->getRoutes('test', new Route('/', array(), array(), array(), '', array('Ftp', 'https'))); + $this->assertEquals('ftp://localhost/app.php/', $this->getGenerator($routes)->generate('test')); + } + public function testPathWithTwoStartingSlashes() { $routes = $this->getRoutes('test', new Route('//path-and-not-domain')); @@ -447,7 +461,7 @@ public function testUrlWithInvalidParameterInHostInNonStrictMode() $this->assertNull($generator->generate('test', array('foo' => 'baz'), false)); } - public function testGenerateNetworkPath() + public function testGenerateNetworkPathBC() { $routes = $this->getRoutes('test', new Route('/{name}', array(), array('_scheme' => 'http'), array(), '{locale}.example.com')); @@ -465,13 +479,32 @@ public function testGenerateNetworkPath() ); } + public function testGenerateNetworkPath() + { + $routes = $this->getRoutes('test', new Route('/{name}', array(), array(), array(), '{locale}.example.com', array('http'))); + + $this->assertSame('//fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test', + array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::NETWORK_PATH), 'network path with different host' + ); + $this->assertSame('//fr.example.com/app.php/Fabien?query=string', $this->getGenerator($routes, array('host' => 'fr.example.com'))->generate('test', + array('name' =>'Fabien', 'locale' => 'fr', 'query' => 'string'), UrlGeneratorInterface::NETWORK_PATH), 'network path although host same as context' + ); + $this->assertSame('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test', + array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::NETWORK_PATH), 'absolute URL because scheme requirement does not match context' + ); + $this->assertSame('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test', + array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::ABSOLUTE_URL), 'absolute URL with same scheme because it is requested' + ); + } + public function testGenerateRelativePath() { $routes = new RouteCollection(); $routes->add('article', new Route('/{author}/{article}/')); $routes->add('comments', new Route('/{author}/{article}/comments')); $routes->add('host', new Route('/{article}', array(), array(), array(), '{author}.example.com')); - $routes->add('scheme', new Route('/{author}', array(), array('_scheme' => 'https'))); + $routes->add('schemeBC', new Route('/{author}', array(), array('_scheme' => 'https'))); // BC + $routes->add('scheme', new Route('/{author}/blog', array(), array(), array(), '', array('https'))); $routes->add('unrelated', new Route('/about')); $generator = $this->getGenerator($routes, array('host' => 'example.com', 'pathInfo' => '/fabien/symfony-is-great/')); @@ -491,9 +524,12 @@ public function testGenerateRelativePath() $this->assertSame('//bernhard.example.com/app.php/forms-are-great', $generator->generate('host', array('author' =>'bernhard', 'article' => 'forms-are-great'), UrlGeneratorInterface::RELATIVE_PATH) ); - $this->assertSame('https://example.com/app.php/bernhard', $generator->generate('scheme', + $this->assertSame('https://example.com/app.php/bernhard', $generator->generate('schemeBC', array('author' =>'bernhard'), UrlGeneratorInterface::RELATIVE_PATH) ); + $this->assertSame('https://example.com/app.php/bernhard/blog', $generator->generate('scheme', + array('author' =>'bernhard'), UrlGeneratorInterface::RELATIVE_PATH) + ); $this->assertSame('../../about', $generator->generate('unrelated', array(), UrlGeneratorInterface::RELATIVE_PATH) ); diff --git a/src/Symfony/Component/Routing/Tests/Loader/AbstractAnnotationLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/AbstractAnnotationLoaderTest.php index c927ae4a85fee..288bf64ffaf78 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AbstractAnnotationLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AbstractAnnotationLoaderTest.php @@ -13,13 +13,6 @@ abstract class AbstractAnnotationLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Doctrine\\Common\\Version')) { - $this->markTestSkipped('Doctrine is not available.'); - } - } - public function getReader() { return $this->getMockBuilder('Doctrine\Common\Annotations\Reader') diff --git a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php index 5b7325c317c57..3b39a666edc26 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/AnnotationClassLoaderTest.php @@ -84,7 +84,12 @@ public function getLoadTests() array( 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass', array('name' => 'route1', 'defaults' => array('arg2' => 'foobar')), - array('arg2' => false, 'arg3' => 'defaultValue3') + array('arg2' => 'defaultValue2', 'arg3' =>'defaultValue3') + ), + array( + 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass', + array('name' => 'route1', 'defaults' => array('arg2' => 'foo'), 'condition' => 'context.getMethod() == "GET"'), + array('arg2' => 'defaultValue2', 'arg3' =>'defaultValue3') ), ); } @@ -102,6 +107,7 @@ public function testLoad($className, $routeDatas = array(), $methodArgs = array( 'defaults' => array(), 'schemes' => array(), 'methods' => array(), + 'condition' => null, ), $routeDatas); $this->reader @@ -116,6 +122,33 @@ public function testLoad($className, $routeDatas = array(), $methodArgs = array( $this->assertSame($routeDatas['requirements'],$route->getRequirements(), '->load preserves requirements annotation'); $this->assertCount(0, array_intersect($route->getOptions(), $routeDatas['options']), '->load preserves options annotation'); $this->assertSame(array_replace($methodArgs, $routeDatas['defaults']), $route->getDefaults(), '->load preserves defaults annotation'); + $this->assertEquals($routeDatas['condition'], $route->getCondition(), '->load preserves condition annotation'); + } + + public function testClassRouteLoad() + { + $classRouteDatas = array('path' => '/classRoutePrefix'); + + $routeDatas = array( + 'name' => 'route1', + 'path' => '/', + ); + + $this->reader + ->expects($this->once()) + ->method('getClassAnnotation') + ->will($this->returnValue($this->getAnnotatedRoute($classRouteDatas))) + ; + + $this->reader + ->expects($this->once()) + ->method('getMethodAnnotations') + ->will($this->returnValue(array($this->getAnnotatedRoute($routeDatas)))) + ; + $routeCollection = $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass'); + $route = $routeCollection->get($routeDatas['name']); + + $this->assertSame($classRouteDatas['path'].$routeDatas['path'], $route->getPath(), '->load preserves class route path annotation'); } private function getAnnotatedRoute($datas) diff --git a/src/Symfony/Component/Routing/Tests/Loader/ClosureLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/ClosureLoaderTest.php index 64d1b086ef692..d34fa87ec5331 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/ClosureLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/ClosureLoaderTest.php @@ -17,13 +17,6 @@ class ClosureLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\FileLocator')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testSupports() { $loader = new ClosureLoader(); diff --git a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php index 18b166fc558cb..bf76d3465b9ae 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php @@ -16,13 +16,6 @@ class PhpFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\FileLocator')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testSupports() { $loader = new PhpFileLoader($this->getMock('Symfony\Component\Config\FileLocator')); diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php index 9f038c1611a7c..f83b9ecba8df3 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php @@ -17,13 +17,6 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\FileLocator')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testSupports() { $loader = new XmlFileLoader($this->getMock('Symfony\Component\Config\FileLocator')); @@ -41,10 +34,12 @@ public function testLoadWithRoute() $routeCollection = $loader->load('validpattern.xml'); $routes = $routeCollection->all(); - $this->assertCount(2, $routes, 'Two routes are loaded'); + $this->assertCount(3, $routes, 'Three routes are loaded'); $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); - foreach ($routes as $route) { + $identicalRoutes = array_slice($routes, 0, 2); + + foreach ($identicalRoutes as $route) { $this->assertSame('/blog/{slug}', $route->getPath()); $this->assertSame('{locale}.example.com', $route->getHost()); $this->assertSame('MyBundle:Blog:show', $route->getDefault('_controller')); @@ -52,6 +47,7 @@ public function testLoadWithRoute() $this->assertSame('RouteCompiler', $route->getOption('compiler_class')); $this->assertEquals(array('GET', 'POST', 'PUT', 'OPTIONS'), $route->getMethods()); $this->assertEquals(array('https'), $route->getSchemes()); + $this->assertEquals('context.getMethod() == "GET"', $route->getCondition()); } } @@ -78,7 +74,7 @@ public function testLoadWithImport() $routeCollection = $loader->load('validresource.xml'); $routes = $routeCollection->all(); - $this->assertCount(2, $routes, 'Two routes are loaded'); + $this->assertCount(3, $routes, 'Three routes are loaded'); $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); foreach ($routes as $route) { @@ -87,6 +83,7 @@ public function testLoadWithImport() $this->assertSame('\d+', $route->getRequirement('foo')); $this->assertSame('bar', $route->getOption('foo')); $this->assertSame('', $route->getHost()); + $this->assertSame('context.getMethod() == "POST"', $route->getCondition()); } } diff --git a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php index a3e934cef02bf..e7a86a93da296 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/YamlFileLoaderTest.php @@ -17,17 +17,6 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\FileLocator')) { - $this->markTestSkipped('The "Config" component is not available'); - } - - if (!class_exists('Symfony\Component\Yaml\Yaml')) { - $this->markTestSkipped('The "Yaml" component is not available'); - } - } - public function testSupports() { $loader = new YamlFileLoader($this->getMock('Symfony\Component\Config\FileLocator')); @@ -79,10 +68,12 @@ public function testLoadWithRoute() $routeCollection = $loader->load('validpattern.yml'); $routes = $routeCollection->all(); - $this->assertCount(2, $routes, 'Two routes are loaded'); + $this->assertCount(3, $routes, 'Three routes are loaded'); $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); - foreach ($routes as $route) { + $identicalRoutes = array_slice($routes, 0, 2); + + foreach ($identicalRoutes as $route) { $this->assertSame('/blog/{slug}', $route->getPath()); $this->assertSame('{locale}.example.com', $route->getHost()); $this->assertSame('MyBundle:Blog:show', $route->getDefault('_controller')); @@ -90,6 +81,7 @@ public function testLoadWithRoute() $this->assertSame('RouteCompiler', $route->getOption('compiler_class')); $this->assertEquals(array('GET', 'POST', 'PUT', 'OPTIONS'), $route->getMethods()); $this->assertEquals(array('https'), $route->getSchemes()); + $this->assertEquals('context.getMethod() == "GET"', $route->getCondition()); } } @@ -99,7 +91,7 @@ public function testLoadWithResource() $routeCollection = $loader->load('validresource.yml'); $routes = $routeCollection->all(); - $this->assertCount(2, $routes, 'Two routes are loaded'); + $this->assertCount(3, $routes, 'Three routes are loaded'); $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes); foreach ($routes as $route) { @@ -108,6 +100,8 @@ public function testLoadWithResource() $this->assertSame('\d+', $route->getRequirement('foo')); $this->assertSame('bar', $route->getOption('foo')); $this->assertSame('', $route->getHost()); + $this->assertSame('context.getMethod() == "POST"', $route->getCondition()); } } + } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperPrefixCollectionTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperPrefixCollectionTest.php index 7b4565c40375d..de01a75d0b1b1 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperPrefixCollectionTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/DumperPrefixCollectionTest.php @@ -20,7 +20,7 @@ class DumperPrefixCollectionTest extends \PHPUnit_Framework_TestCase { public function testAddPrefixRoute() { - $coll = new DumperPrefixCollection; + $coll = new DumperPrefixCollection(); $coll->setPrefix(''); $route = new DumperRoute('bar', new Route('/foo/bar')); @@ -66,7 +66,7 @@ public function testAddPrefixRoute() public function testMergeSlashNodes() { - $coll = new DumperPrefixCollection; + $coll = new DumperPrefixCollection(); $coll->setPrefix(''); $route = new DumperRoute('bar', new Route('/foo/bar')); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php index 542ede85c0017..473af6aa5cea3 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php @@ -251,6 +251,9 @@ public function getRouteCollections() $rootprefixCollection->add('static', new Route('/test')); $rootprefixCollection->add('dynamic', new Route('/{var}')); $rootprefixCollection->addPrefix('rootprefix'); + $route = new Route('/with-condition'); + $route->setCondition('context.getMethod() == "GET"'); + $rootprefixCollection->add('with-condition', $route); return array( array($collection, 'url_matcher1.php', array()), diff --git a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php index 2ad4fc8725a82..5cbb605479117 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/RedirectableUrlMatcherTest.php @@ -41,7 +41,7 @@ public function testRedirectWhenNoSlashForNonSafeMethod() $matcher->match('/foo'); } - public function testSchemeRedirect() + public function testSchemeRedirectBC() { $coll = new RouteCollection(); $coll->add('foo', new Route('/foo', array(), array('_scheme' => 'https'))); @@ -55,4 +55,32 @@ public function testSchemeRedirect() ; $matcher->match('/foo'); } + + public function testSchemeRedirectRedirectsToFirstScheme() + { + $coll = new RouteCollection(); + $coll->add('foo', new Route('/foo', array(), array(), array(), '', array('FTP', 'HTTPS'))); + + $matcher = $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($coll, new RequestContext())); + $matcher + ->expects($this->once()) + ->method('redirect') + ->with('/foo', 'foo', 'ftp') + ->will($this->returnValue(array('_route' => 'foo'))) + ; + $matcher->match('/foo'); + } + + public function testNoSchemaRedirectIfOnOfMultipleSchemesMatches() + { + $coll = new RouteCollection(); + $coll->add('foo', new Route('/foo', array(), array(), array(), '', array('https', 'http'))); + + $matcher = $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($coll, new RequestContext())); + $matcher + ->expects($this->never()) + ->method('redirect') + ; + $matcher->match('/foo'); + } } diff --git a/src/Symfony/Component/Routing/Tests/Matcher/TraceableUrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/TraceableUrlMatcherTest.php index 04d012628dc27..969ab0a2f0d00 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/TraceableUrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/TraceableUrlMatcherTest.php @@ -26,13 +26,14 @@ public function test() $coll->add('bar1', new Route('/bar/{name}', array(), array('id' => '\w+', '_method' => 'POST'))); $coll->add('bar2', new Route('/foo', array(), array(), array(), 'baz')); $coll->add('bar3', new Route('/foo1', array(), array(), array(), 'baz')); + $coll->add('bar4', new Route('/foo2', array(), array(), array(), 'baz', array(), array(), 'context.getMethod() == "GET"')); $context = new RequestContext(); $context->setHost('baz'); $matcher = new TraceableUrlMatcher($coll, $context); $traces = $matcher->getTraces('/babar'); - $this->assertEquals(array(0, 0, 0, 0, 0), $this->getLevels($traces)); + $this->assertEquals(array(0, 0, 0, 0, 0, 0), $this->getLevels($traces)); $traces = $matcher->getTraces('/foo'); $this->assertEquals(array(1, 0, 0, 2), $this->getLevels($traces)); @@ -41,7 +42,7 @@ public function test() $this->assertEquals(array(0, 2), $this->getLevels($traces)); $traces = $matcher->getTraces('/bar/dd'); - $this->assertEquals(array(0, 1, 1, 0, 0), $this->getLevels($traces)); + $this->assertEquals(array(0, 1, 1, 0, 0, 0), $this->getLevels($traces)); $traces = $matcher->getTraces('/foo1'); $this->assertEquals(array(0, 0, 0, 0, 2), $this->getLevels($traces)); @@ -52,6 +53,9 @@ public function test() $traces = $matcher->getTraces('/bar/dd'); $this->assertEquals(array(0, 1, 2), $this->getLevels($traces)); + + $traces = $matcher->getTraces('/foo2'); + $this->assertEquals(array(0, 0, 0, 0, 0, 1), $this->getLevels($traces)); } public function testMatchRouteOnMultipleHosts() diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index 8a1428f170859..b03b0c3aa077a 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -313,13 +313,36 @@ public function testDefaultRequirementOfVariableDisallowsNextSeparator() /** * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException */ - public function testSchemeRequirement() + public function testSchemeRequirementBC() { $coll = new RouteCollection(); $coll->add('foo', new Route('/foo', array(), array('_scheme' => 'https'))); $matcher = new UrlMatcher($coll, new RequestContext()); $matcher->match('/foo'); } + /** + * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException + */ + public function testSchemeRequirement() + { + $coll = new RouteCollection(); + $coll->add('foo', new Route('/foo', array(), array(), array(), '', array('https'))); + $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher->match('/foo'); + } + + /** + * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException + */ + public function testCondition() + { + $coll = new RouteCollection(); + $route = new Route('/foo'); + $route->setCondition('context.getMethod() == "POST"'); + $coll->add('foo', $route); + $matcher = new UrlMatcher($coll, new RequestContext()); + $matcher->match('/foo'); + } public function testDecodeOnce() { diff --git a/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php b/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php index 321ed64b926ee..4c12ed56df3ea 100644 --- a/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteCollectionTest.php @@ -103,10 +103,6 @@ public function testAddCollection() public function testAddCollectionWithResources() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $collection = new RouteCollection(); $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml')); $collection1 = new RouteCollection(); @@ -178,10 +174,6 @@ public function testAddPrefixOverridesDefaultsAndRequirements() public function testResource() { - if (!class_exists('Symfony\Component\Config\Resource\FileResource')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $collection = new RouteCollection(); $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml')); $collection->addResource($bar = new FileResource(__DIR__.'/Fixtures/bar.xml')); @@ -253,6 +245,20 @@ public function testSetHost() $this->assertEquals('{locale}.example.com', $routeb->getHost()); } + public function testSetCondition() + { + $collection = new RouteCollection(); + $routea = new Route('/a'); + $routeb = new Route('/b', array(), array(), array(), '{locale}.example.net', array(), array(), 'context.getMethod() == "GET"'); + $collection->add('a', $routea); + $collection->add('b', $routeb); + + $collection->setCondition('context.getMethod() == "POST"'); + + $this->assertEquals('context.getMethod() == "POST"', $routea->getCondition()); + $this->assertEquals('context.getMethod() == "POST"', $routeb->getCondition()); + } + public function testClone() { $collection = new RouteCollection(); diff --git a/src/Symfony/Component/Routing/Tests/RouteTest.php b/src/Symfony/Component/Routing/Tests/RouteTest.php index 9f496e259ea48..cbd5ccb43b3f7 100644 --- a/src/Symfony/Component/Routing/Tests/RouteTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteTest.php @@ -24,9 +24,10 @@ public function testConstructor() $this->assertEquals('bar', $route->getOption('foo'), '__construct() takes options as its fourth argument'); $this->assertEquals('{locale}.example.com', $route->getHost(), '__construct() takes a host pattern as its fifth argument'); - $route = new Route('/', array(), array(), array(), '', array('Https'), array('POST', 'put')); + $route = new Route('/', array(), array(), array(), '', array('Https'), array('POST', 'put'), 'context.getMethod() == "GET"'); $this->assertEquals(array('https'), $route->getSchemes(), '__construct() takes schemes as its sixth argument and lowercases it'); $this->assertEquals(array('POST', 'PUT'), $route->getMethods(), '__construct() takes methods as its seventh argument and uppercases it'); + $this->assertEquals('context.getMethod() == "GET"', $route->getCondition(), '__construct() takes a condition as its eight argument'); $route = new Route('/', array(), array(), array(), '', 'Https', 'Post'); $this->assertEquals(array('https'), $route->getSchemes(), '__construct() takes a single scheme as its sixth argument'); @@ -44,7 +45,7 @@ public function testPath() $this->assertEquals('/bar', $route->getPath(), '->setPath() adds a / at the beginning of the path if needed'); $this->assertEquals($route, $route->setPath(''), '->setPath() implements a fluent interface'); $route->setPath('//path'); - $this->assertEquals('/path', $route->getPath(), '->setPath() does not allow two slahes "//" at the beginning of the path as it would be confused with a network path when generating the path from the route'); + $this->assertEquals('/path', $route->getPath(), '->setPath() does not allow two slashes "//" at the beginning of the path as it would be confused with a network path when generating the path from the route'); } public function testOptions() @@ -152,10 +153,15 @@ public function testScheme() { $route = new Route('/'); $this->assertEquals(array(), $route->getSchemes(), 'schemes is initialized with array()'); + $this->assertFalse($route->hasScheme('http')); $route->setSchemes('hTTp'); $this->assertEquals(array('http'), $route->getSchemes(), '->setSchemes() accepts a single scheme string and lowercases it'); + $this->assertTrue($route->hasScheme('htTp')); + $this->assertFalse($route->hasScheme('httpS')); $route->setSchemes(array('HttpS', 'hTTp')); $this->assertEquals(array('https', 'http'), $route->getSchemes(), '->setSchemes() accepts an array of schemes and lowercases them'); + $this->assertTrue($route->hasScheme('htTp')); + $this->assertTrue($route->hasScheme('httpS')); } public function testSchemeIsBC() @@ -164,6 +170,9 @@ public function testSchemeIsBC() $route->setRequirement('_scheme', 'http|https'); $this->assertEquals('http|https', $route->getRequirement('_scheme')); $this->assertEquals(array('http', 'https'), $route->getSchemes()); + $this->assertTrue($route->hasScheme('https')); + $this->assertTrue($route->hasScheme('http')); + $this->assertFalse($route->hasScheme('ftp')); $route->setSchemes(array('hTTp')); $this->assertEquals('http', $route->getRequirement('_scheme')); $route->setSchemes(array()); @@ -192,6 +201,14 @@ public function testMethodIsBC() $this->assertNull($route->getRequirement('_method')); } + public function testCondition() + { + $route = new Route('/'); + $this->assertEquals(null, $route->getCondition()); + $route->setCondition('context.getMethod() == "GET"'); + $this->assertEquals('context.getMethod() == "GET"', $route->getCondition()); + } + public function testCompile() { $route = new Route('/{foo}'); diff --git a/src/Symfony/Component/Routing/Tests/RouterTest.php b/src/Symfony/Component/Routing/Tests/RouterTest.php index a3c336e5b775e..42a344c74a73b 100644 --- a/src/Symfony/Component/Routing/Tests/RouterTest.php +++ b/src/Symfony/Component/Routing/Tests/RouterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Routing\Tests; use Symfony\Component\Routing\Router; +use Symfony\Component\HttpFoundation\Request; class RouterTest extends \PHPUnit_Framework_TestCase { @@ -135,4 +136,28 @@ public function provideGeneratorOptionsPreventingCaching() array('generator_cache_class') ); } + + public function testMatchRequestWithUrlMatcherInterface() + { + $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); + $matcher->expects($this->once())->method('match'); + + $p = new \ReflectionProperty($this->router, 'matcher'); + $p->setAccessible(true); + $p->setValue($this->router, $matcher); + + $this->router->matchRequest(Request::create('/')); + } + + public function testMatchRequestWithRequestMatcherInterface() + { + $matcher = $this->getMock('Symfony\Component\Routing\Matcher\RequestMatcherInterface'); + $matcher->expects($this->once())->method('matchRequest'); + + $p = new \ReflectionProperty($this->router, 'matcher'); + $p->setAccessible(true); + $p->setValue($this->router, $matcher); + + $this->router->matchRequest(Request::create('/')); + } } diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index 9a737c6b0287b..9a88cc149fb33 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -2,7 +2,7 @@ "name": "symfony/routing", "type": "library", "description": "Symfony Routing Component", - "keywords": [], + "keywords": ["routing", "router", "URL", "URI"], "homepage": "http://symfony.com", "license": "MIT", "authors": [ @@ -21,13 +21,15 @@ "require-dev": { "symfony/config": "~2.2", "symfony/yaml": "~2.0", - "doctrine/common": "~2.2", + "symfony/expression-language": "~2.4", + "doctrine/annotations": "~1.0", "psr/log": "~1.0" }, "suggest": { - "symfony/config": "", - "symfony/yaml": "", - "doctrine/common": "" + "symfony/config": "For using the all-in-one router or any loader", + "symfony/yaml": "For using the YAML loader", + "symfony/expression-language": "For using expression matching", + "doctrine/annotations": "For using the annotation loader" }, "autoload": { "psr-0": { "Symfony\\Component\\Routing\\": "" } @@ -36,7 +38,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Security/Acl/.gitignore b/src/Symfony/Component/Security/Acl/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Security/Acl/Dbal/AclProvider.php b/src/Symfony/Component/Security/Acl/Dbal/AclProvider.php index 1d1cb160ed0c3..b4791faf740a9 100644 --- a/src/Symfony/Component/Security/Acl/Dbal/AclProvider.php +++ b/src/Symfony/Component/Security/Acl/Dbal/AclProvider.php @@ -40,8 +40,8 @@ class AclProvider implements AclProviderInterface protected $cache; protected $connection; - protected $loadedAces; - protected $loadedAcls; + protected $loadedAces = array(); + protected $loadedAcls = array(); protected $options; private $permissionGrantingStrategy; @@ -57,8 +57,6 @@ public function __construct(Connection $connection, PermissionGrantingStrategyIn { $this->cache = $cache; $this->connection = $connection; - $this->loadedAces = array(); - $this->loadedAcls = array(); $this->options = $options; $this->permissionGrantingStrategy = $permissionGrantingStrategy; } @@ -167,13 +165,13 @@ public function findAcls(array $oids, array $sids = array()) if ((self::MAX_BATCH_SIZE === count($currentBatch) || ($i + 1) === $c) && count($currentBatch) > 0) { try { $loadedBatch = $this->lookupObjectIdentities($currentBatch, $sids, $oidLookup); - } catch (AclNotFoundException $aclNotFoundexception) { + } catch (AclNotFoundException $aclNotFoundException) { if ($result->count()) { $partialResultException = new NotAllAclsFoundException('The provider could not find ACLs for all object identities.'); $partialResultException->setPartialResult($result); throw $partialResultException; } else { - throw $aclNotFoundexception; + throw $aclNotFoundException; } } foreach ($loadedBatch as $loadedOid) { diff --git a/src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php b/src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php index 29d3cfd640ae1..e4b2a75ad420e 100644 --- a/src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php +++ b/src/Symfony/Component/Security/Acl/Dbal/MutableAclProvider.php @@ -108,6 +108,18 @@ public function deleteAcl(ObjectIdentityInterface $oid) } } + /** + * Deletes the security identity from the database. + * ACL entries have the CASCADE option on their foreign key so they will also get deleted + * + * @param SecurityIdentityInterface $sid + * @throws \InvalidArgumentException + */ + public function deleteSecurityIdentity(SecurityIdentityInterface $sid) + { + $this->connection->executeQuery($this->getDeleteSecurityIdentityIdSql($sid)); + } + /** * {@inheritDoc} */ @@ -253,7 +265,7 @@ public function updateAcl(MutableAclInterface $acl) } // check properties for deleted, and created ACEs, and perform deletions - // we need to perfom deletions before updating existing ACEs, in order to + // we need to perform deletions before updating existing ACEs, in order to // preserve uniqueness of the order field if (isset($propertyChanges['classAces'])) { $this->updateOldAceProperty('classAces', $propertyChanges['classAces']); @@ -351,6 +363,17 @@ public function updateAcl(MutableAclInterface $acl) } } + /** + * Updates a user security identity when the user's username changes + * + * @param UserSecurityIdentity $usid + * @param string $oldUsername + */ + public function updateUserSecurityIdentity(UserSecurityIdentity $usid, $oldUsername) + { + $this->connection->executeQuery($this->getUpdateUserSecurityIdentitySql($usid, $oldUsername)); + } + /** * Constructs the SQL for deleting access control entries. * @@ -360,7 +383,7 @@ public function updateAcl(MutableAclInterface $acl) protected function getDeleteAccessControlEntriesSql($oidPK) { return sprintf( - 'DELETE FROM %s WHERE object_identity_id = %d', + 'DELETE FROM %s WHERE object_identity_id = %d', $this->options['entry_table_name'], $oidPK ); @@ -611,6 +634,21 @@ protected function getSelectSecurityIdentityIdSql(SecurityIdentityInterface $sid ); } + /** + * Constructs the SQL to delete a security identity. + * + * @param SecurityIdentityInterface $sid + * @throws \InvalidArgumentException + * @return string + */ + protected function getDeleteSecurityIdentityIdSql(SecurityIdentityInterface $sid) + { + $select = $this->getSelectSecurityIdentityIdSql($sid); + $delete = preg_replace('/^SELECT id FROM/', 'DELETE FROM', $select); + + return $delete; + } + /** * Constructs the SQL for updating an object identity. * @@ -633,6 +671,31 @@ protected function getUpdateObjectIdentitySql($pk, array $changes) ); } + /** + * Constructs the SQL for updating a user security identity. + * + * @param UserSecurityIdentity $usid + * @param string $oldUsername + * @return string + */ + protected function getUpdateUserSecurityIdentitySql(UserSecurityIdentity $usid, $oldUsername) + { + if ($usid->getUsername() == $oldUsername) { + throw new \InvalidArgumentException('There are no changes.'); + } + + $oldIdentifier = $usid->getClass().'-'.$oldUsername; + $newIdentifier = $usid->getClass().'-'.$usid->getUsername(); + + return sprintf( + 'UPDATE %s SET identifier = %s WHERE identifier = %s AND username = %s', + $this->options['sid_table_name'], + $this->connection->quote($newIdentifier), + $this->connection->quote($oldIdentifier), + $this->connection->getDatabasePlatform()->convertBooleans(true) + ); + } + /** * Constructs the SQL for updating an ACE. * @@ -806,7 +869,7 @@ private function updateNewFieldAceProperty($name, array $changes) * @param string $name * @param array $changes */ - private function updateOldFieldAceProperty($ane, array $changes) + private function updateOldFieldAceProperty($name, array $changes) { $currentIds = array(); foreach ($changes[1] as $field => $new) { @@ -925,11 +988,12 @@ private function updateAce(\SplObjectStorage $aces, $ace) if (isset($propertyChanges['aceOrder']) && $propertyChanges['aceOrder'][1] > $propertyChanges['aceOrder'][0] && $propertyChanges == $aces->offsetGet($ace)) { - $aces->next(); - if ($aces->valid()) { + + $aces->next(); + if ($aces->valid()) { $this->updateAce($aces, $aces->current()); - } } + } if (isset($propertyChanges['mask'])) { $sets[] = sprintf('mask = %d', $propertyChanges['mask'][1]); @@ -949,5 +1013,4 @@ private function updateAce(\SplObjectStorage $aces, $ace) $this->connection->executeQuery($this->getUpdateAccessControlEntrySql($ace->getId(), $sets)); } - } diff --git a/src/Symfony/Component/Security/Acl/Domain/Acl.php b/src/Symfony/Component/Security/Acl/Domain/Acl.php index 4665c0e18dbe2..dd3e8d4f51691 100644 --- a/src/Symfony/Component/Security/Acl/Domain/Acl.php +++ b/src/Symfony/Component/Security/Acl/Domain/Acl.php @@ -37,14 +37,14 @@ class Acl implements AuditableAclInterface, NotifyPropertyChanged private $parentAcl; private $permissionGrantingStrategy; private $objectIdentity; - private $classAces; - private $classFieldAces; - private $objectAces; - private $objectFieldAces; + private $classAces = array(); + private $classFieldAces = array(); + private $objectAces = array(); + private $objectFieldAces = array(); private $id; private $loadedSids; private $entriesInheriting; - private $listeners; + private $listeners = array(); /** * Constructor @@ -62,12 +62,6 @@ public function __construct($id, ObjectIdentityInterface $objectIdentity, Permis $this->permissionGrantingStrategy = $permissionGrantingStrategy; $this->loadedSids = $loadedSids; $this->entriesInheriting = $entriesInheriting; - $this->parentAcl = null; - $this->classAces = array(); - $this->classFieldAces = array(); - $this->objectAces = array(); - $this->objectFieldAces = array(); - $this->listeners = array(); } /** diff --git a/src/Symfony/Component/Security/Acl/LICENSE b/src/Symfony/Component/Security/Acl/LICENSE new file mode 100644 index 0000000000000..0b3292cf90235 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2014 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php b/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php index 017e7c024109e..1f6ab1e655ba6 100644 --- a/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php +++ b/src/Symfony/Component/Security/Acl/Permission/MaskBuilder.php @@ -96,13 +96,7 @@ public function __construct($mask = 0) */ public function add($mask) { - if (is_string($mask) && defined($name = 'static::MASK_'.strtoupper($mask))) { - $mask = constant($name); - } elseif (!is_int($mask)) { - throw new \InvalidArgumentException('$mask must be an integer.'); - } - - $this->mask |= $mask; + $this->mask |= $this->getMask($mask); return $this; } @@ -152,13 +146,7 @@ public function getPattern() */ public function remove($mask) { - if (is_string($mask) && defined($name = 'static::MASK_'.strtoupper($mask))) { - $mask = constant($name); - } elseif (!is_int($mask)) { - throw new \InvalidArgumentException('$mask must be an integer.'); - } - - $this->mask &= ~$mask; + $this->mask &= ~$this->getMask($mask); return $this; } @@ -191,19 +179,43 @@ public static function getCode($mask) $reflection = new \ReflectionClass(get_called_class()); foreach ($reflection->getConstants() as $name => $cMask) { - if (0 !== strpos($name, 'MASK_')) { + if (0 !== strpos($name, 'MASK_') || $mask !== $cMask) { continue; } - if ($mask === $cMask) { - if (!defined($cName = 'static::CODE_'.substr($name, 5))) { - throw new \RuntimeException('There was no code defined for this mask.'); - } - - return constant($cName); + if (!defined($cName = 'static::CODE_'.substr($name, 5))) { + throw new \RuntimeException('There was no code defined for this mask.'); } + + return constant($cName); } throw new \InvalidArgumentException(sprintf('The mask "%d" is not supported.', $mask)); } + + /** + * Returns the mask for the passed code + * + * @param mixed $code + * + * @return integer + * + * @throws \InvalidArgumentException + */ + private function getMask($code) + { + if (is_string($code)) { + if (!defined($name = sprintf('static::MASK_%s', strtoupper($code)))) { + throw new \InvalidArgumentException(sprintf('The code "%s" is not supported', $code)); + } + + return constant($name); + } + + if (!is_int($code)) { + throw new \InvalidArgumentException('$code must be an integer.'); + } + + return $code; + } } diff --git a/src/Symfony/Component/Security/Acl/README.md b/src/Symfony/Component/Security/Acl/README.md new file mode 100644 index 0000000000000..6c009a368eb66 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/README.md @@ -0,0 +1,23 @@ +Security Component - ACL (Access Control List) +============================================== + +Security provides an infrastructure for sophisticated authorization systems, +which makes it possible to easily separate the actual authorization logic from +so called user providers that hold the users credentials. It is inspired by +the Java Spring framework. + +Resources +--------- + +Documentation: + +http://symfony.com/doc/2.5/book/security.html + +Tests +----- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Security/Acl/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Component/Security/Tests/Acl/Dbal/AclProviderBenchmarkTest.php b/src/Symfony/Component/Security/Acl/Tests/Dbal/AclProviderBenchmarkTest.php similarity index 97% rename from src/Symfony/Component/Security/Tests/Acl/Dbal/AclProviderBenchmarkTest.php rename to src/Symfony/Component/Security/Acl/Tests/Dbal/AclProviderBenchmarkTest.php index 8f6a30ceb7e82..8f68f1f5c7f0b 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Dbal/AclProviderBenchmarkTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Dbal/AclProviderBenchmarkTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Dbal; +namespace Symfony\Component\Security\Acl\Tests\Dbal; use Symfony\Component\Security\Acl\Dbal\AclProvider; use Symfony\Component\Security\Acl\Domain\PermissionGrantingStrategy; @@ -32,10 +32,6 @@ class AclProviderBenchmarkTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Doctrine\DBAL\DriverManager')) { - $this->markTestSkipped('The "Doctrine DBAL" library is not available'); - } - try { $this->con = DriverManager::getConnection(array( 'driver' => 'pdo_mysql', diff --git a/src/Symfony/Component/Security/Tests/Acl/Dbal/AclProviderTest.php b/src/Symfony/Component/Security/Acl/Tests/Dbal/AclProviderTest.php similarity index 97% rename from src/Symfony/Component/Security/Tests/Acl/Dbal/AclProviderTest.php rename to src/Symfony/Component/Security/Acl/Tests/Dbal/AclProviderTest.php index ad58d72ab2b83..717a2585e45f0 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Dbal/AclProviderTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Dbal/AclProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Dbal; +namespace Symfony\Component\Security\Acl\Tests\Dbal; use Symfony\Component\Security\Acl\Dbal\AclProvider; use Symfony\Component\Security\Acl\Domain\PermissionGrantingStrategy; @@ -141,9 +141,6 @@ public function testFindAcl() protected function setUp() { - if (!class_exists('Doctrine\DBAL\DriverManager')) { - $this->markTestSkipped('The Doctrine2 DBAL is required for this test'); - } if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers())) { self::markTestSkipped('This test requires SQLite support in your environment'); } diff --git a/src/Symfony/Component/Security/Tests/Acl/Dbal/MutableAclProviderTest.php b/src/Symfony/Component/Security/Acl/Tests/Dbal/MutableAclProviderTest.php similarity index 94% rename from src/Symfony/Component/Security/Tests/Acl/Dbal/MutableAclProviderTest.php rename to src/Symfony/Component/Security/Acl/Tests/Dbal/MutableAclProviderTest.php index 00a222801237a..8c920cf31effe 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Dbal/MutableAclProviderTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Dbal/MutableAclProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Dbal; +namespace Symfony\Component\Security\Acl\Tests\Dbal; use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; use Symfony\Component\Security\Acl\Model\FieldEntryInterface; @@ -407,6 +407,36 @@ public function testUpdateAclDeletingObjectFieldAcesThrowsDBConstraintViolations $provider->updateAcl($acl); } + public function testUpdateUserSecurityIdentity() + { + $provider = $this->getProvider(); + $acl = $provider->createAcl(new ObjectIdentity(1, 'Foo')); + $sid = new UserSecurityIdentity('johannes', 'FooClass'); + $acl->setEntriesInheriting(!$acl->isEntriesInheriting()); + + $acl->insertObjectAce($sid, 1); + $acl->insertClassAce($sid, 5, 0, false); + $acl->insertObjectAce($sid, 2, 1, true); + $acl->insertClassFieldAce('field', $sid, 2, 0, true); + $provider->updateAcl($acl); + + $newSid = new UserSecurityIdentity('mathieu', 'FooClass'); + $provider->updateUserSecurityIdentity($newSid, 'johannes'); + + $reloadProvider = $this->getProvider(); + $reloadedAcl = $reloadProvider->findAcl(new ObjectIdentity(1, 'Foo')); + + $this->assertNotSame($acl, $reloadedAcl); + $this->assertSame($acl->isEntriesInheriting(), $reloadedAcl->isEntriesInheriting()); + + $aces = $acl->getObjectAces(); + $reloadedAces = $reloadedAcl->getObjectAces(); + $this->assertEquals(count($aces), count($reloadedAces)); + foreach ($reloadedAces as $ace) { + $this->assertTrue($ace->getSecurityIdentity()->equals($newSid)); + } + } + /** * Data must have the following format: * array( @@ -477,9 +507,6 @@ protected function callMethod($object, $method, array $args) protected function setUp() { - if (!class_exists('Doctrine\DBAL\DriverManager')) { - $this->markTestSkipped('The Doctrine2 DBAL is required for this test'); - } if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers())) { self::markTestSkipped('This test requires SQLite support in your environment'); } diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/AclTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/AclTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Acl/Domain/AclTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/AclTest.php index 4b67e625e352c..2034c214de9ba 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/AclTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/AclTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; @@ -511,11 +511,4 @@ protected function getAcl() { return new Acl(1, new ObjectIdentity(1, 'foo'), new PermissionGrantingStrategy(), array(), true); } - - protected function setUp() - { - if (!class_exists('Doctrine\DBAL\DriverManager')) { - $this->markTestSkipped('The Doctrine2 DBAL is required for this test'); - } - } } diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/AuditLoggerTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/AuditLoggerTest.php similarity index 95% rename from src/Symfony/Component/Security/Tests/Acl/Domain/AuditLoggerTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/AuditLoggerTest.php index a0f38eb42cb45..15538d346245a 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/AuditLoggerTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/AuditLoggerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; class AuditLoggerTest extends \PHPUnit_Framework_TestCase { @@ -26,12 +26,12 @@ public function testLogIfNeeded($granting, $audit) ->expects($this->once()) ->method('isAuditSuccess') ->will($this->returnValue($audit)) - ; + ; - $ace + $ace ->expects($this->never()) ->method('isAuditFailure') - ; + ; } else { $ace ->expects($this->never()) diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/DoctrineAclCacheTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/DoctrineAclCacheTest.php similarity index 92% rename from src/Symfony/Component/Security/Tests/Acl/Domain/DoctrineAclCacheTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/DoctrineAclCacheTest.php index 99eb27e1f7b6a..128f2c84836bd 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/DoctrineAclCacheTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/DoctrineAclCacheTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; use Symfony\Component\Security\Acl\Domain\ObjectIdentity; @@ -98,11 +98,4 @@ protected function getCache($cacheDriver = null, $prefix = DoctrineAclCache::PRE return new DoctrineAclCache($cacheDriver, $this->getPermissionGrantingStrategy(), $prefix); } - - protected function setUp() - { - if (!class_exists('Doctrine\DBAL\DriverManager')) { - $this->markTestSkipped('The Doctrine2 DBAL is required for this test'); - } - } } diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/EntryTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/EntryTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Acl/Domain/EntryTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/EntryTest.php index 55c8f0a429a97..6a2aac0d80d33 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/EntryTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/EntryTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\Entry; diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/FieldEntryTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/FieldEntryTest.php similarity index 97% rename from src/Symfony/Component/Security/Tests/Acl/Domain/FieldEntryTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/FieldEntryTest.php index 7f0cbc0760425..735e2e86da75d 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/FieldEntryTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/FieldEntryTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\FieldEntry; diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/ObjectIdentityRetrievalStrategyTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/ObjectIdentityRetrievalStrategyTest.php similarity index 95% rename from src/Symfony/Component/Security/Tests/Acl/Domain/ObjectIdentityRetrievalStrategyTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/ObjectIdentityRetrievalStrategyTest.php index e89e1ef125df9..59fc3bdb7e37c 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/ObjectIdentityRetrievalStrategyTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/ObjectIdentityRetrievalStrategyTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\ObjectIdentityRetrievalStrategy; diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/ObjectIdentityTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/ObjectIdentityTest.php similarity index 80% rename from src/Symfony/Component/Security/Tests/Acl/Domain/ObjectIdentityTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/ObjectIdentityTest.php index 7aa3cf21bbe6c..4eab7b2a7fdcc 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/ObjectIdentityTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/ObjectIdentityTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain +namespace Symfony\Component\Security\Acl\Tests\Domain { use Symfony\Component\Security\Acl\Domain\ObjectIdentity; @@ -26,10 +26,10 @@ public function testConstructor() // Test that constructor never changes passed type, even with proxies public function testConstructorWithProxy() { - $id = new ObjectIdentity('fooid', 'Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Acl\Domain\TestDomainObject'); + $id = new ObjectIdentity('fooid', 'Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Acl\Tests\Domain\TestDomainObject'); $this->assertEquals('fooid', $id->getIdentifier()); - $this->assertEquals('Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Acl\Domain\TestDomainObject', $id->getType()); + $this->assertEquals('Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Acl\Tests\Domain\TestDomainObject', $id->getType()); } public function testFromDomainObjectPrefersInterfaceOverGetId() @@ -54,14 +54,14 @@ public function testFromDomainObjectWithoutInterface() { $id = ObjectIdentity::fromDomainObject(new TestDomainObject()); $this->assertEquals('getId()', $id->getIdentifier()); - $this->assertEquals('Symfony\Component\Security\Tests\Acl\Domain\TestDomainObject', $id->getType()); + $this->assertEquals('Symfony\Component\Security\Acl\Tests\Domain\TestDomainObject', $id->getType()); } public function testFromDomainObjectWithProxy() { - $id = ObjectIdentity::fromDomainObject(new \Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Acl\Domain\TestDomainObject()); + $id = ObjectIdentity::fromDomainObject(new \Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Acl\Tests\Domain\TestDomainObject()); $this->assertEquals('getId()', $id->getIdentifier()); - $this->assertEquals('Symfony\Component\Security\Tests\Acl\Domain\TestDomainObject', $id->getType()); + $this->assertEquals('Symfony\Component\Security\Acl\Tests\Domain\TestDomainObject', $id->getType()); } /** @@ -85,13 +85,6 @@ public function getCompareData() array(new ObjectIdentity('1', 'bla'), new ObjectIdentity('1', 'blub'), false), ); } - - protected function setUp() - { - if (!class_exists('Doctrine\DBAL\DriverManager')) { - $this->markTestSkipped('The Doctrine2 DBAL is required for this test'); - } - } } class TestDomainObject @@ -108,9 +101,9 @@ public function getId() } } -namespace Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Acl\Domain +namespace Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Acl\Tests\Domain { - class TestDomainObject extends \Symfony\Component\Security\Tests\Acl\Domain\TestDomainObject + class TestDomainObject extends \Symfony\Component\Security\Acl\Tests\Domain\TestDomainObject { } } diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/PermissionGrantingStrategyTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/PermissionGrantingStrategyTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Acl/Domain/PermissionGrantingStrategyTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/PermissionGrantingStrategyTest.php index d200d2b0eb24f..490d2563386b2 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/PermissionGrantingStrategyTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/PermissionGrantingStrategyTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\ObjectIdentity; use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; @@ -182,11 +182,4 @@ protected function getAcl($strategy) return new Acl($id++, new ObjectIdentity(1, 'Foo'), $strategy, array(), true); } - - protected function setUp() - { - if (!class_exists('Doctrine\DBAL\DriverManager')) { - $this->markTestSkipped('The Doctrine2 DBAL is required for this test'); - } - } } diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/RoleSecurityIdentityTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/RoleSecurityIdentityTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Acl/Domain/RoleSecurityIdentityTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/RoleSecurityIdentityTest.php index 341f33ca48167..ad5f23639f17d 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/RoleSecurityIdentityTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/RoleSecurityIdentityTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/SecurityIdentityRetrievalStrategyTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/SecurityIdentityRetrievalStrategyTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Acl/Domain/SecurityIdentityRetrievalStrategyTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/SecurityIdentityRetrievalStrategyTest.php index f649653db0f01..02fbe679f1975 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/SecurityIdentityRetrievalStrategyTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/SecurityIdentityRetrievalStrategyTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; @@ -88,7 +88,7 @@ public function getSecurityIdentityRetrievalTests() new RoleSecurityIdentity('IS_AUTHENTICATED_ANONYMOUSLY'), )), array(new CustomUserImpl('johannes'), array('ROLE_FOO'), 'fullFledged', array( - new UserSecurityIdentity('johannes', 'Symfony\Component\Security\Tests\Acl\Domain\CustomUserImpl'), + new UserSecurityIdentity('johannes', 'Symfony\Component\Security\Acl\Tests\Domain\CustomUserImpl'), new RoleSecurityIdentity('ROLE_FOO'), new RoleSecurityIdentity('IS_AUTHENTICATED_FULLY'), new RoleSecurityIdentity('IS_AUTHENTICATED_REMEMBERED'), diff --git a/src/Symfony/Component/Security/Tests/Acl/Domain/UserSecurityIdentityTest.php b/src/Symfony/Component/Security/Acl/Tests/Domain/UserSecurityIdentityTest.php similarity index 93% rename from src/Symfony/Component/Security/Tests/Acl/Domain/UserSecurityIdentityTest.php rename to src/Symfony/Component/Security/Acl/Tests/Domain/UserSecurityIdentityTest.php index 1d6a3c5bcab64..09d3f0d560ffe 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Domain/UserSecurityIdentityTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Domain/UserSecurityIdentityTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Domain; +namespace Symfony\Component\Security\Acl\Tests\Domain; use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; @@ -27,10 +27,10 @@ public function testConstructor() // Test that constructor never changes the type, even for proxies public function testConstructorWithProxy() { - $id = new UserSecurityIdentity('foo', 'Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Acl\Domain\Foo'); + $id = new UserSecurityIdentity('foo', 'Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Acl\Tests\Domain\Foo'); $this->assertEquals('foo', $id->getUsername()); - $this->assertEquals('Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Acl\Domain\Foo', $id->getClass()); + $this->assertEquals('Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Acl\Tests\Domain\Foo', $id->getClass()); } /** diff --git a/src/Symfony/Component/Security/Tests/Acl/Permission/BasicPermissionMapTest.php b/src/Symfony/Component/Security/Acl/Tests/Permission/BasicPermissionMapTest.php similarity index 90% rename from src/Symfony/Component/Security/Tests/Acl/Permission/BasicPermissionMapTest.php rename to src/Symfony/Component/Security/Acl/Tests/Permission/BasicPermissionMapTest.php index 743634ba597b0..2afe588f038af 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Permission/BasicPermissionMapTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Permission/BasicPermissionMapTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Permission; +namespace Symfony\Component\Security\Acl\Tests\Permission; use Symfony\Component\Security\Acl\Permission\BasicPermissionMap; diff --git a/src/Symfony/Component/Security/Tests/Acl/Permission/MaskBuilderTest.php b/src/Symfony/Component/Security/Acl/Tests/Permission/MaskBuilderTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Acl/Permission/MaskBuilderTest.php rename to src/Symfony/Component/Security/Acl/Tests/Permission/MaskBuilderTest.php index dc86755c449a1..824566918025b 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Permission/MaskBuilderTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Permission/MaskBuilderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Permission; +namespace Symfony\Component\Security\Acl\Tests\Permission; use Symfony\Component\Security\Acl\Permission\MaskBuilder; @@ -76,7 +76,7 @@ public function testAddAndRemove() public function testGetPattern() { - $builder = new MaskBuilder; + $builder = new MaskBuilder(); $this->assertEquals(MaskBuilder::ALL_OFF, $builder->getPattern()); $builder->add('view'); diff --git a/src/Symfony/Component/Security/Tests/Acl/Voter/AclVoterTest.php b/src/Symfony/Component/Security/Acl/Tests/Voter/AclVoterTest.php similarity index 99% rename from src/Symfony/Component/Security/Tests/Acl/Voter/AclVoterTest.php rename to src/Symfony/Component/Security/Acl/Tests/Voter/AclVoterTest.php index 2474515b5c5a2..6bec23124cbab 100644 --- a/src/Symfony/Component/Security/Tests/Acl/Voter/AclVoterTest.php +++ b/src/Symfony/Component/Security/Acl/Tests/Voter/AclVoterTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Acl\Voter; +namespace Symfony\Component\Security\Acl\Tests\Voter; use Symfony\Component\Security\Acl\Exception\NoAceFoundException; use Symfony\Component\Security\Acl\Voter\FieldVote; diff --git a/src/Symfony/Component/Security/Acl/composer.json b/src/Symfony/Component/Security/Acl/composer.json new file mode 100644 index 0000000000000..5f5787fcc69f7 --- /dev/null +++ b/src/Symfony/Component/Security/Acl/composer.json @@ -0,0 +1,42 @@ +{ + "name": "symfony/security-acl", + "type": "library", + "description": "Symfony Security Component - ACL (Access Control List)", + "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.3", + "symfony/security-core": "~2.4" + }, + "require-dev": { + "doctrine/common": "~2.2", + "doctrine/dbal": "~2.2", + "psr/log": "~1.0" + }, + "suggest": { + "symfony/class-loader": "For using the ACL generateSql script", + "symfony/finder": "For using the ACL generateSql script", + "doctrine/dbal": "For using the built-in ACL implementation" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\Security\\Acl\\": "" } + }, + "target-dir": "Symfony/Component/Security/Acl", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + } +} diff --git a/src/Symfony/Component/Security/Acl/phpunit.xml.dist b/src/Symfony/Component/Security/Acl/phpunit.xml.dist new file mode 100644 index 0000000000000..65209485df1eb --- /dev/null +++ b/src/Symfony/Component/Security/Acl/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./vendor + ./Tests + + + + diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 92f807366e27d..9ab61d0bb4a1a 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +2.4.0 +----- + + * The switch user listener now preserves the query string when switching a user + * The remember-me cookie hashes now use HMAC, which means that current cookies will be invalidated + * added simpler customization options + * structured component into three sub-components Acl, Core and Http + * added Csrf sub-component + * changed Http sub-component to depend on Csrf sub-component instead of the Form component + 2.3.0 ----- diff --git a/src/Symfony/Component/Security/Core/.gitignore b/src/Symfony/Component/Security/Core/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Security/Core/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php index 3affd7805a404..f4d09590ef413 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php @@ -47,23 +47,22 @@ public function __construct(UserProviderInterface $userProvider, UserCheckerInte $this->providerKey = $providerKey; } - /** - * {@inheritdoc} - */ - public function authenticate(TokenInterface $token) - { - if (!$this->supports($token)) { - return null; - } - + /** + * {@inheritdoc} + */ + public function authenticate(TokenInterface $token) + { + if (!$this->supports($token)) { + return null; + } if (!$user = $token->getUser()) { throw new BadCredentialsException('No pre-authenticated principal found in request.'); } -/* + /* if (null === $token->getCredentials()) { throw new BadCredentialsException('No pre-authenticated credentials found in request.'); } -*/ + */ $user = $this->userProvider->loadUserByUsername($user); $this->userChecker->checkPostAuth($user); diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/SimpleAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/SimpleAuthenticationProvider.php new file mode 100644 index 0000000000000..ffbc72c055a0f --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/SimpleAuthenticationProvider.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\Security\Core\Authentication\Provider; + +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\SimpleAuthenticatorInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; + +/** + * @author Jordi Boggiano + */ +class SimpleAuthenticationProvider implements AuthenticationProviderInterface +{ + private $simpleAuthenticator; + private $userProvider; + private $providerKey; + + public function __construct(SimpleAuthenticatorInterface $simpleAuthenticator, UserProviderInterface $userProvider, $providerKey) + { + $this->simpleAuthenticator = $simpleAuthenticator; + $this->userProvider = $userProvider; + $this->providerKey = $providerKey; + } + + public function authenticate(TokenInterface $token) + { + $authToken = $this->simpleAuthenticator->authenticateToken($token, $this->userProvider, $this->providerKey); + + if ($authToken instanceof TokenInterface) { + return $authToken; + } + + throw new AuthenticationException('Simple authenticator failed to return an authenticated token.'); + } + + public function supports(TokenInterface $token) + { + return $this->simpleAuthenticator->supportsToken($token, $this->providerKey); + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/SimpleAuthenticatorInterface.php b/src/Symfony/Component/Security/Core/Authentication/SimpleAuthenticatorInterface.php new file mode 100644 index 0000000000000..868d072714834 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/SimpleAuthenticatorInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +/** + * @author Jordi Boggiano + */ +interface SimpleAuthenticatorInterface +{ + public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey); + + public function supportsToken(TokenInterface $token, $providerKey); +} diff --git a/src/Symfony/Component/Security/Core/Authentication/SimpleFormAuthenticatorInterface.php b/src/Symfony/Component/Security/Core/Authentication/SimpleFormAuthenticatorInterface.php new file mode 100644 index 0000000000000..95ee881c18d82 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/SimpleFormAuthenticatorInterface.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\Security\Core\Authentication; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Jordi Boggiano + */ +interface SimpleFormAuthenticatorInterface extends SimpleAuthenticatorInterface +{ + public function createToken(Request $request, $username, $password, $providerKey); +} diff --git a/src/Symfony/Component/Security/Core/Authentication/SimplePreAuthenticatorInterface.php b/src/Symfony/Component/Security/Core/Authentication/SimplePreAuthenticatorInterface.php new file mode 100644 index 0000000000000..6164e7d9860a7 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/SimplePreAuthenticatorInterface.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\Security\Core\Authentication; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Jordi Boggiano + */ +interface SimplePreAuthenticatorInterface extends SimpleAuthenticatorInterface +{ + public function createToken(Request $request, $providerKey); +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php index e4c46d5900fd9..5160fc5613a0e 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php @@ -26,9 +26,9 @@ abstract class AbstractToken implements TokenInterface { private $user; - private $roles; - private $authenticated; - private $attributes; + private $roles = array(); + private $authenticated = false; + private $attributes = array(); /** * Constructor. @@ -39,10 +39,6 @@ abstract class AbstractToken implements TokenInterface */ public function __construct(array $roles = array()) { - $this->authenticated = false; - $this->attributes = array(); - - $this->roles = array(); foreach ($roles as $role) { if (is_string($role)) { $role = new Role($role); @@ -218,7 +214,7 @@ public function getAttribute($name) } /** - * Sets a attribute. + * Sets an attribute. * * @param string $name The attribute name * @param mixed $value The attribute value diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php index 6028c42c4b60c..18c3569aa94be 100644 --- a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php +++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php @@ -43,8 +43,13 @@ public function __construct(array $voters, $strategy = 'affirmative', $allowIfAl throw new \InvalidArgumentException('You must at least add one voter.'); } + $strategyMethod = 'decide'.ucfirst($strategy); + if (!is_callable(array($this, $strategyMethod))) { + throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy)); + } + $this->voters = $voters; - $this->strategy = 'decide'.ucfirst($strategy); + $this->strategy = $strategyMethod; $this->allowIfAllAbstainDecisions = (Boolean) $allowIfAllAbstainDecisions; $this->allowIfEqualGrantedDeniedDecisions = (Boolean) $allowIfEqualGrantedDeniedDecisions; } diff --git a/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguage.php b/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguage.php new file mode 100644 index 0000000000000..f9012b73a86a6 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguage.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\Security\Core\Authorization; + +use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; + +/** + * Adds some function to the default ExpressionLanguage. + * + * @author Fabien Potencier + */ +class ExpressionLanguage extends BaseExpressionLanguage +{ + protected function registerFunctions() + { + parent::registerFunctions(); + + $this->register('is_anonymous', function () { + return '$trust_resolver->isAnonymous($token)'; + }, function (array $variables) { + return $variables['trust_resolver']->isAnonymous($variables['token']); + }); + + $this->register('is_authenticated', function () { + return '$token && !$trust_resolver->isAnonymous($token)'; + }, function (array $variables) { + return $variables['token'] && !$variables['trust_resolver']->isAnonymous($variables['token']); + }); + + $this->register('is_fully_authenticated', function () { + return '$trust_resolver->isFullFledged($token)'; + }, function (array $variables) { + return $variables['trust_resolver']->isFullFledged($variables['token']); + }); + + $this->register('is_remember_me', function () { + return '$trust_resolver->isRememberMe($token)'; + }, function (array $variables) { + return $variables['trust_resolver']->isRememberMe($variables['token']); + }); + + $this->register('has_role', function ($role) { + return sprintf('in_array(%s, $roles)', $role); + }, function (array $variables, $role) { + return in_array($role, $variables['roles']); + }); + } +} diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php new file mode 100644 index 0000000000000..bf6683d193007 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization\Voter; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; +use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpFoundation\Request; + +/** + * ExpressionVoter votes based on the evaluation of an expression. + * + * @author Fabien Potencier + */ +class ExpressionVoter implements VoterInterface +{ + private $expressionLanguage; + private $trustResolver; + private $roleHierarchy; + + /** + * Constructor. + * + * @param ExpressionLanguage $expressionLanguage + * @param AuthenticationTrustResolverInterface $trustResolver + * @param RoleHierarchyInterface $roleHierarchy + */ + public function __construct(ExpressionLanguage $expressionLanguage, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null) + { + $this->expressionLanguage = $expressionLanguage; + $this->trustResolver = $trustResolver; + $this->roleHierarchy = $roleHierarchy; + } + + /** + * {@inheritdoc} + */ + public function supportsAttribute($attribute) + { + return $attribute instanceof Expression; + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function vote(TokenInterface $token, $object, array $attributes) + { + $result = VoterInterface::ACCESS_ABSTAIN; + $variables = null; + foreach ($attributes as $attribute) { + if (!$this->supportsAttribute($attribute)) { + continue; + } + + if (null === $variables) { + $variables = $this->getVariables($token, $object); + } + + $result = VoterInterface::ACCESS_DENIED; + if ($this->expressionLanguage->evaluate($attribute, $variables)) { + return VoterInterface::ACCESS_GRANTED; + } + } + + return $result; + } + + private function getVariables(TokenInterface $token, $object) + { + if (null !== $this->roleHierarchy) { + $roles = $this->roleHierarchy->getReachableRoles($token->getRoles()); + } else { + $roles = $token->getRoles(); + } + + $variables = array( + 'token' => $token, + 'user' => $token->getUser(), + 'object' => $object, + 'roles' => array_map(function ($role) { return $role->getRole(); }, $roles), + 'trust_resolver' => $this->trustResolver, + ); + + // this is mainly to propose a better experience when the expression is used + // in an access control rule, as the developer does not know that it's going + // to be handled by this voter + if ($object instanceof Request) { + $variables['request'] = $object; + } + + return $variables; + } +} diff --git a/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php b/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php index b83eb3050dcb9..aa298767118f8 100644 --- a/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php +++ b/src/Symfony/Component/Security/Core/Encoder/BasePasswordEncoder.php @@ -89,6 +89,8 @@ protected function comparePasswords($password1, $password2) /** * Checks if the password is too long. * + * @param string $password The password + * * @return Boolean true if the password is too long, false otherwise */ protected function isPasswordTooLong($password) diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderAwareInterface.php b/src/Symfony/Component/Security/Core/Encoder/EncoderAwareInterface.php new file mode 100644 index 0000000000000..22ae820cce394 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderAwareInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Encoder; + +/** + * @author Christophe Coevoet + */ +interface EncoderAwareInterface +{ + /** + * Gets the name of the encoder used to encode the password. + * + * If the method returns null, the standard way to retrieve the encoder + * will be used instead. + * + * @return string + */ + public function getEncoderName(); +} diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index 8bad61fabfda8..bb5ba912b36d6 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -30,19 +30,32 @@ public function __construct(array $encoders) */ public function getEncoder($user) { - foreach ($this->encoders as $class => $encoder) { - if ((is_object($user) && !$user instanceof $class) || (!is_object($user) && !is_subclass_of($user, $class) && $user != $class)) { - continue; + $encoderKey = null; + + if ($user instanceof EncoderAwareInterface && (null !== $encoderName = $user->getEncoderName())) { + if (!array_key_exists($encoderName, $this->encoders)) { + throw new \RuntimeException(sprintf('The encoder "%s" was not configured.', $encoderName)); } - if (!$encoder instanceof PasswordEncoderInterface) { - return $this->encoders[$class] = $this->createEncoder($encoder); + $encoderKey = $encoderName; + } else { + foreach ($this->encoders as $class => $encoder) { + if ((is_object($user) && $user instanceof $class) || (!is_object($user) && (is_subclass_of($user, $class) || $user == $class))) { + $encoderKey = $class; + break; + } } + } + + if (null === $encoderKey) { + throw new \RuntimeException(sprintf('No encoder has been configured for account "%s".', is_object($user) ? get_class($user) : $user)); + } - return $this->encoders[$class]; + if (!$this->encoders[$encoderKey] instanceof PasswordEncoderInterface) { + $this->encoders[$encoderKey] = $this->createEncoder($this->encoders[$encoderKey]); } - throw new \RuntimeException(sprintf('No encoder has been configured for account "%s".', is_object($user) ? get_class($user) : $user)); + return $this->encoders[$encoderKey]; } /** diff --git a/src/Symfony/Component/Security/Core/Exception/ExceptionInterface.php b/src/Symfony/Component/Security/Core/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..5000d0278083b --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/ExceptionInterface.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\Security\Core\Exception; + +/** + * Base ExceptionInterface for the Security component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Security/Core/Exception/InvalidArgumentException.php b/src/Symfony/Component/Security/Core/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..6f85e9500f19d --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/InvalidArgumentException.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\Security\Core\Exception; + +/** + * Base InvalidArgumentException for the Security component. + * + * @author Bernhard Schussek + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Security/Core/Exception/RuntimeException.php b/src/Symfony/Component/Security/Core/Exception/RuntimeException.php new file mode 100644 index 0000000000000..95edec8ee9dd3 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/RuntimeException.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\Security\Core\Exception; + +/** + * Base RuntimeException for the Security component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Security/Core/LICENSE b/src/Symfony/Component/Security/Core/LICENSE new file mode 100644 index 0000000000000..0b3292cf90235 --- /dev/null +++ b/src/Symfony/Component/Security/Core/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2014 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Security/Core/README.md b/src/Symfony/Component/Security/Core/README.md new file mode 100644 index 0000000000000..4585a5d675332 --- /dev/null +++ b/src/Symfony/Component/Security/Core/README.md @@ -0,0 +1,23 @@ +Security Component - Core +========================= + +Security provides an infrastructure for sophisticated authorization systems, +which makes it possible to easily separate the actual authorization logic from +so called user providers that hold the users credentials. It is inspired by +the Java Spring framework. + +Resources +--------- + +Documentation: + +http://symfony.com/doc/2.5/book/security.html + +Tests +----- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Security/Core/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ar.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ar.xlf new file mode 100644 index 0000000000000..fd18ee6ad9faf --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ar.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + حدث خطأ اثناء الدخول. + + + Authentication credentials could not be found. + لم استطع العثور على معلومات الدخول. + + + Authentication request could not be processed due to a system problem. + لم يكتمل طلب الدخول نتيجه عطل فى النظام. + + + Invalid credentials. + معلومات الدخول خاطئة. + + + Cookie has already been used by someone else. + ملفات تعريف الارتباط(cookies) تم استخدامها من قبل شخص اخر. + + + Not privileged to request the resource. + ليست لديك الصلاحيات الكافية لهذا الطلب. + + + Invalid CSRF token. + رمز الموقع غير صحيح. + + + Digest nonce has expired. + انتهت صلاحية(digest nonce). + + + No authentication provider found to support the authentication token. + لا يوجد معرف للدخول يدعم الرمز المستخدم للدخول. + + + No session available, it either timed out or cookies are not enabled. + لا يوجد صلة بينك و بين الموقع اما انها انتهت او ان متصفحك لا يدعم خاصية ملفات تعريف الارتباط (cookies). + + + No token could be found. + لم استطع العثور على الرمز. + + + Username could not be found. + لم استطع العثور على اسم الدخول. + + + Account has expired. + انتهت صلاحية الحساب. + + + Credentials have expired. + انتهت صلاحية معلومات الدخول. + + + Account is disabled. + الحساب موقوف. + + + Account is locked. + الحساب مغلق. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf new file mode 100644 index 0000000000000..7ece2603ae477 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ca.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Ha succeït un error d'autenticació. + + + Authentication credentials could not be found. + No s'han trobat les credencials d'autenticació. + + + Authentication request could not be processed due to a system problem. + La solicitud d'autenticació no s'ha pogut processar per un problema del sistema. + + + Invalid credentials. + Credencials no vàlides. + + + Cookie has already been used by someone else. + La cookie ja ha estat utilitzada per una altra persona. + + + Not privileged to request the resource. + No té privilegis per solicitar el recurs. + + + Invalid CSRF token. + Token CSRF no vàlid. + + + Digest nonce has expired. + El vector d'inicialització (digest nonce) ha expirat. + + + No authentication provider found to support the authentication token. + No s'ha trobat un proveïdor d'autenticació que suporti el token d'autenticació. + + + No session available, it either timed out or cookies are not enabled. + No hi ha sessió disponible, ha expirat o les cookies no estan habilitades. + + + No token could be found. + No s'ha trobat cap token. + + + Username could not be found. + No s'ha trobat el nom d'usuari. + + + Account has expired. + El compte ha expirat. + + + Credentials have expired. + Les credencials han expirat. + + + Account is disabled. + El compte està deshabilitat. + + + Account is locked. + El compte està bloquejat. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.cs.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.cs.xlf new file mode 100644 index 0000000000000..bd146c68049cb --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.cs.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Při ověřování došlo k chybě. + + + Authentication credentials could not be found. + Ověřovací údaje nebyly nalezeny. + + + Authentication request could not be processed due to a system problem. + Požadavek na ověření nemohl být zpracován kvůli systémové chybě. + + + Invalid credentials. + Neplatné přihlašovací údaje. + + + Cookie has already been used by someone else. + Cookie již bylo použité někým jiným. + + + Not privileged to request the resource. + Nemáte oprávnění přistupovat k prostředku. + + + Invalid CSRF token. + Neplatný CSRF token. + + + Digest nonce has expired. + Platnost inicializačního vektoru (digest nonce) vypršela. + + + No authentication provider found to support the authentication token. + Poskytovatel pro ověřovací token nebyl nalezen. + + + No session available, it either timed out or cookies are not enabled. + Session není k dispozici, vypršela její platnost, nebo jsou zakázané cookies. + + + No token could be found. + Token nebyl nalezen. + + + Username could not be found. + Přihlašovací jméno nebylo nalezeno. + + + Account has expired. + Platnost účtu vypršela. + + + Credentials have expired. + Platnost přihlašovacích údajů vypršela. + + + Account is disabled. + Účet je zakázaný. + + + Account is locked. + Účet je zablokovaný. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.da.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.da.xlf new file mode 100644 index 0000000000000..9c7b8866efb14 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.da.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + En fejl indtraf ved godkendelse. + + + Authentication credentials could not be found. + Loginoplysninger kan findes. + + + Authentication request could not be processed due to a system problem. + Godkendelsesanmodning kan ikke behandles på grund af et systemfejl. + + + Invalid credentials. + Ugyldige loginoplysninger. + + + Cookie has already been used by someone else. + Cookie er allerede brugt af en anden. + + + Not privileged to request the resource. + Ingen tilladselese at anvende kilden. + + + Invalid CSRF token. + Ugyldigt CSRF token. + + + Digest nonce has expired. + Digest nonce er udløbet. + + + No authentication provider found to support the authentication token. + Ingen godkendelsesudbyder er fundet til understøttelsen af godkendelsestoken. + + + No session available, it either timed out or cookies are not enabled. + Ingen session tilgængelig, sessionen er enten udløbet eller cookies er ikke aktiveret. + + + No token could be found. + Ingen token kan findes. + + + Username could not be found. + Brugernavn kan ikke findes. + + + Account has expired. + Brugerkonto er udløbet. + + + Credentials have expired. + Loginoplysninger er udløbet. + + + Account is disabled. + Brugerkonto er deaktiveret. + + + Account is locked. + Brugerkonto er låst. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.de.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.de.xlf new file mode 100644 index 0000000000000..e5946ed4aa42d --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.de.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Es ist ein Fehler bei der Authentifikation aufgetreten. + + + Authentication credentials could not be found. + Es konnten keine Zugangsdaten gefunden werden. + + + Authentication request could not be processed due to a system problem. + Die Authentifikation konnte wegen eines Systemproblems nicht bearbeitet werden. + + + Invalid credentials. + Fehlerhafte Zugangsdaten. + + + Cookie has already been used by someone else. + Cookie wurde bereits von jemand anderem verwendet. + + + Not privileged to request the resource. + Keine Rechte, um die Ressource anzufragen. + + + Invalid CSRF token. + Ungültiges CSRF-Token. + + + Digest nonce has expired. + Digest nonce ist abgelaufen. + + + No authentication provider found to support the authentication token. + Es wurde kein Authentifizierungs-Provider gefunden, der das Authentifizierungs-Token unterstützt. + + + No session available, it either timed out or cookies are not enabled. + Keine Session verfügbar, entweder ist diese abgelaufen oder Cookies sind nicht aktiviert. + + + No token could be found. + Es wurde kein Token gefunden. + + + Username could not be found. + Der Benutzername wurde nicht gefunden. + + + Account has expired. + Der Account ist abgelaufen. + + + Credentials have expired. + Die Zugangsdaten sind abgelaufen. + + + Account is disabled. + Der Account ist deaktiviert. + + + Account is locked. + Der Account ist gesperrt. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.el.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.el.xlf new file mode 100644 index 0000000000000..07eabe7ed29e2 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.el.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Συνέβη ένα σφάλμα πιστοποίησης. + + + Authentication credentials could not be found. + Τα στοιχεία πιστοποίησης δε βρέθηκαν. + + + Authentication request could not be processed due to a system problem. + Το αίτημα πιστοποίησης δε μπορεί να επεξεργαστεί λόγω σφάλματος του συστήματος. + + + Invalid credentials. + Λανθασμένα στοιχεία σύνδεσης. + + + Cookie has already been used by someone else. + Το Cookie έχει ήδη χρησιμοποιηθεί από κάποιον άλλο. + + + Not privileged to request the resource. + Δεν είστε εξουσιοδοτημένος για πρόσβαση στο συγκεκριμένο περιεχόμενο. + + + Invalid CSRF token. + Μη έγκυρο CSRF token. + + + Digest nonce has expired. + Το digest nonce έχει λήξει. + + + No authentication provider found to support the authentication token. + Δε βρέθηκε κάποιος πάροχος πιστοποίησης που να υποστηρίζει το token πιστοποίησης. + + + No session available, it either timed out or cookies are not enabled. + Δεν υπάρχει ενεργή σύνοδος (session), είτε έχει λήξει ή τα cookies δεν είναι ενεργοποιημένα. + + + No token could be found. + Δεν ήταν δυνατόν να βρεθεί κάποιο token. + + + Username could not be found. + Το Username δε βρέθηκε. + + + Account has expired. + Ο λογαριασμός έχει λήξει. + + + Credentials have expired. + Τα στοιχεία σύνδεσης έχουν λήξει. + + + Account is disabled. + Ο λογαριασμός είναι απενεργοποιημένος. + + + Account is locked. + Ο λογαριασμός είναι κλειδωμένος. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.en.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.en.xlf new file mode 100644 index 0000000000000..3640698ce9fb3 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.en.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + An authentication exception occurred. + + + Authentication credentials could not be found. + Authentication credentials could not be found. + + + Authentication request could not be processed due to a system problem. + Authentication request could not be processed due to a system problem. + + + Invalid credentials. + Invalid credentials. + + + Cookie has already been used by someone else. + Cookie has already been used by someone else. + + + Not privileged to request the resource. + Not privileged to request the resource. + + + Invalid CSRF token. + Invalid CSRF token. + + + Digest nonce has expired. + Digest nonce has expired. + + + No authentication provider found to support the authentication token. + No authentication provider found to support the authentication token. + + + No session available, it either timed out or cookies are not enabled. + No session available, it either timed out or cookies are not enabled. + + + No token could be found. + No token could be found. + + + Username could not be found. + Username could not be found. + + + Account has expired. + Account has expired. + + + Credentials have expired. + Credentials have expired. + + + Account is disabled. + Account is disabled. + + + Account is locked. + Account is locked. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.es.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.es.xlf new file mode 100644 index 0000000000000..00cefbb2dad67 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.es.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Ocurrió un error de autenticación. + + + Authentication credentials could not be found. + No se encontraron las credenciales de autenticación. + + + Authentication request could not be processed due to a system problem. + La solicitud de autenticación no se pudo procesar debido a un problema del sistema. + + + Invalid credentials. + Credenciales no válidas. + + + Cookie has already been used by someone else. + La cookie ya ha sido usada por otra persona. + + + Not privileged to request the resource. + No tiene privilegios para solicitar el recurso. + + + Invalid CSRF token. + Token CSRF no válido. + + + Digest nonce has expired. + El vector de inicialización (digest nonce) ha expirado. + + + No authentication provider found to support the authentication token. + No se encontró un proveedor de autenticación que soporte el token de autenticación. + + + No session available, it either timed out or cookies are not enabled. + No hay ninguna sesión disponible, ha expirado o las cookies no están habilitados. + + + No token could be found. + No se encontró ningún token. + + + Username could not be found. + No se encontró el nombre de usuario. + + + Account has expired. + La cuenta ha expirado. + + + Credentials have expired. + Las credenciales han expirado. + + + Account is disabled. + La cuenta está deshabilitada. + + + Account is locked. + La cuenta está bloqueada. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.fa.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.fa.xlf new file mode 100644 index 0000000000000..0b7629078063c --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.fa.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + خطایی هنگام تعیین اعتبار اتفاق افتاد. + + + Authentication credentials could not be found. + شرایط تعیین اعتبار پیدا نشد. + + + Authentication request could not be processed due to a system problem. + درخواست تعیین اعتبار به دلیل مشکل سیستم قابل بررسی نیست. + + + Invalid credentials. + شرایط نامعتبر. + + + Cookie has already been used by someone else. + کوکی قبلا برای شخص دیگری استفاده شده است. + + + Not privileged to request the resource. + دسترسی لازم برای درخواست این منبع را ندارید. + + + Invalid CSRF token. + توکن CSRF معتبر نیست. + + + Digest nonce has expired. + Digest nonce منقضی شده است. + + + No authentication provider found to support the authentication token. + هیچ ارایه کننده تعیین اعتباری برای ساپورت توکن تعیین اعتبار پیدا نشد. + + + No session available, it either timed out or cookies are not enabled. + جلسه‌ای در دسترس نیست. این میتواند یا به دلیل پایان یافتن زمان باشد یا اینکه کوکی ها فعال نیستند. + + + No token could be found. + هیچ توکنی پیدا نشد. + + + Username could not be found. + نام ‌کاربری پیدا نشد. + + + Account has expired. + حساب کاربری منقضی شده است. + + + Credentials have expired. + پارامترهای تعیین اعتبار منقضی شده‌اند. + + + Account is disabled. + حساب کاربری غیرفعال است. + + + Account is locked. + حساب کاربری قفل شده است. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.fr.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.fr.xlf new file mode 100644 index 0000000000000..f3965d3fb33ec --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.fr.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Une exception d'authentification s'est produite. + + + Authentication credentials could not be found. + Les droits d'authentification n'ont pas pu être trouvés. + + + Authentication request could not be processed due to a system problem. + La requête d'authentification n'a pas pu être executée à cause d'un problème système. + + + Invalid credentials. + Droits invalides. + + + Cookie has already been used by someone else. + Le cookie a déjà été utilisé par quelqu'un d'autre. + + + Not privileged to request the resource. + Pas de privilèges pour accéder à la ressource. + + + Invalid CSRF token. + Jeton CSRF invalide. + + + Digest nonce has expired. + Le digest nonce a expiré. + + + No authentication provider found to support the authentication token. + Aucun fournisseur d'authentification n'a été trouvé pour supporter le jeton d'authentification. + + + No session available, it either timed out or cookies are not enabled. + Pas de session disponible, celle-ci a expiré ou les cookies ne sont pas activés. + + + No token could be found. + Aucun jeton n'a pu être trouvé. + + + Username could not be found. + Le nom d'utilisateur ne peut pas être trouvé. + + + Account has expired. + Le compte a expiré. + + + Credentials have expired. + Les droits ont expirés. + + + Account is disabled. + Le compte est désactivé. + + + Account is locked. + Le compte est bloqué. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.gl.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.gl.xlf new file mode 100644 index 0000000000000..ed6491f7ef97a --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.gl.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Ocorreu un erro de autenticación. + + + Authentication credentials could not be found. + Non se atoparon as credenciais de autenticación. + + + Authentication request could not be processed due to a system problem. + A solicitude de autenticación no puido ser procesada debido a un problema do sistema. + + + Invalid credentials. + Credenciais non válidas. + + + Cookie has already been used by someone else. + A cookie xa foi empregado por outro usuario. + + + Not privileged to request the resource. + Non ten privilexios para solicitar o recurso. + + + Invalid CSRF token. + Token CSRF non válido. + + + Digest nonce has expired. + O vector de inicialización (digest nonce) expirou. + + + No authentication provider found to support the authentication token. + Non se atopou un provedor de autenticación que soporte o token de autenticación. + + + No session available, it either timed out or cookies are not enabled. + Non hai ningunha sesión dispoñible, expirou ou as cookies non están habilitadas. + + + No token could be found. + Non se atopou ningún token. + + + Username could not be found. + Non se atopou o nome de usuario. + + + Account has expired. + A conta expirou. + + + Credentials have expired. + As credenciais expiraron. + + + Account is disabled. + A conta está deshabilitada. + + + Account is locked. + A conta está bloqueada. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.hu.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.hu.xlf new file mode 100644 index 0000000000000..724397038cb66 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.hu.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Hitelesítési hiba lépett fel. + + + Authentication credentials could not be found. + Nem találhatók hitelesítési információk. + + + Authentication request could not be processed due to a system problem. + A hitelesítési kérést rendszerhiba miatt nem lehet feldolgozni. + + + Invalid credentials. + Érvénytelen hitelesítési információk. + + + Cookie has already been used by someone else. + Ezt a sütit valaki más már felhasználta. + + + Not privileged to request the resource. + Nem rendelkezik az erőforrás eléréséhez szükséges jogosultsággal. + + + Invalid CSRF token. + Érvénytelen CSRF token. + + + Digest nonce has expired. + A kivonat bélyege (nonce) lejárt. + + + No authentication provider found to support the authentication token. + Nem található a hitelesítési tokent támogató hitelesítési szolgáltatás. + + + No session available, it either timed out or cookies are not enabled. + Munkamenet nem áll rendelkezésre, túllépte az időkeretet vagy a sütik le vannak tiltva. + + + No token could be found. + Nem található token. + + + Username could not be found. + A felhasználónév nem található. + + + Account has expired. + A fiók lejárt. + + + Credentials have expired. + A hitelesítési információk lejártak. + + + Account is disabled. + Felfüggesztett fiók. + + + Account is locked. + Zárolt fiók. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.it.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.it.xlf new file mode 100644 index 0000000000000..75d81cc8d9312 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.it.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Si è verificato un errore di autenticazione. + + + Authentication credentials could not be found. + Impossibile trovare le credenziali di autenticazione. + + + Authentication request could not be processed due to a system problem. + La richiesta di autenticazione non può essere processata a causa di un errore di sistema. + + + Invalid credentials. + Credenziali non valide. + + + Cookie has already been used by someone else. + Il cookie è già stato usato da qualcun altro. + + + Not privileged to request the resource. + Non hai i privilegi per richiedere questa risorsa. + + + Invalid CSRF token. + CSRF token non valido. + + + Digest nonce has expired. + Il numero di autenticazione è scaduto. + + + No authentication provider found to support the authentication token. + Non è stato trovato un valido fornitore di autenticazione per supportare il token. + + + No session available, it either timed out or cookies are not enabled. + Nessuna sessione disponibile, può essere scaduta o i cookie non sono abilitati. + + + No token could be found. + Nessun token trovato. + + + Username could not be found. + Username non trovato. + + + Account has expired. + Account scaduto. + + + Credentials have expired. + Credenziali scadute. + + + Account is disabled. + L'account è disabilitato. + + + Account is locked. + L'account è bloccato. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.lb.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.lb.xlf new file mode 100644 index 0000000000000..3dc76d5486883 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.lb.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Bei der Authentifikatioun ass e Feeler opgetrueden. + + + Authentication credentials could not be found. + Et konnte keng Zouganksdate fonnt ginn. + + + Authentication request could not be processed due to a system problem. + D'Ufro fir eng Authentifikatioun konnt wéinst engem Problem vum System net beaarbecht ginn. + + + Invalid credentials. + Ongëlteg Zouganksdaten. + + + Cookie has already been used by someone else. + De Cookie gouf scho vun engem anere benotzt. + + + Not privileged to request the resource. + Keng Rechter fir d'Ressource unzefroen. + + + Invalid CSRF token. + Ongëltegen CSRF-Token. + + + Digest nonce has expired. + Den eemolege Schlëssel ass ofgelaf. + + + No authentication provider found to support the authentication token. + Et gouf keen Authentifizéierungs-Provider fonnt deen den Authentifizéierungs-Token ënnerstëtzt. + + + No session available, it either timed out or cookies are not enabled. + Keng Sëtzung disponibel. Entweder ass se ofgelaf oder Cookies sinn net aktivéiert. + + + No token could be found. + Et konnt keen Token fonnt ginn. + + + Username could not be found. + De Benotzernumm konnt net fonnt ginn. + + + Account has expired. + Den Account ass ofgelaf. + + + Credentials have expired. + D'Zouganksdate sinn ofgelaf. + + + Account is disabled. + De Konto ass deaktivéiert. + + + Account is locked. + De Konto ass gespaart. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.nl.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.nl.xlf new file mode 100644 index 0000000000000..8969e9ef8ca69 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.nl.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Er heeft zich een authenticatieprobleem voorgedaan. + + + Authentication credentials could not be found. + Authenticatiegegevens konden niet worden gevonden. + + + Authentication request could not be processed due to a system problem. + Authenticatieaanvraag kon niet worden verwerkt door een technisch probleem. + + + Invalid credentials. + Ongeldige inloggegevens. + + + Cookie has already been used by someone else. + Cookie is al door een ander persoon gebruikt. + + + Not privileged to request the resource. + Onvoldoende rechten om de aanvraag te verwerken. + + + Invalid CSRF token. + CSRF-code is ongeldig. + + + Digest nonce has expired. + Serverauthenticatiesleutel (digest nonce) is verlopen. + + + No authentication provider found to support the authentication token. + Geen authenticatieprovider gevonden die de authenticatietoken ondersteunt. + + + No session available, it either timed out or cookies are not enabled. + Geen sessie beschikbaar, mogelijk is deze verlopen of cookies zijn uitgeschakeld. + + + No token could be found. + Er kon geen authenticatietoken worden gevonden. + + + Username could not be found. + Gebruikersnaam kon niet worden gevonden. + + + Account has expired. + Account is verlopen. + + + Credentials have expired. + Authenticatiegegevens zijn verlopen. + + + Account is disabled. + Account is gedeactiveerd. + + + Account is locked. + Account is geblokkeerd. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.no.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.no.xlf new file mode 100644 index 0000000000000..3857ab4693ccb --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.no.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + En autentiserings feil har skjedd. + + + Authentication credentials could not be found. + Påloggingsinformasjonen kunne ikke bli funnet. + + + Authentication request could not be processed due to a system problem. + Autentiserings forespørselen kunne ikke bli prosessert grunnet en system feil. + + + Invalid credentials. + Ugyldig påloggingsinformasjonen. + + + Cookie has already been used by someone else. + Cookie har allerede blitt brukt av noen andre. + + + Not privileged to request the resource. + Ingen tilgang til å be om gitt kilde. + + + Invalid CSRF token. + Ugyldig CSRF token. + + + Digest nonce has expired. + Digest nonce er utløpt. + + + No authentication provider found to support the authentication token. + Ingen autentiserings tilbyder funnet som støtter gitt autentiserings token. + + + No session available, it either timed out or cookies are not enabled. + Ingen sesjon tilgjengelig, sesjonen er enten utløpt eller cookies ikke skrudd på. + + + No token could be found. + Ingen token kunne bli funnet. + + + Username could not be found. + Brukernavn kunne ikke bli funnet. + + + Account has expired. + Brukerkonto har utgått. + + + Credentials have expired. + Påloggingsinformasjon har utløpt. + + + Account is disabled. + Brukerkonto er deaktivert. + + + Account is locked. + Brukerkonto er sperret. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.pl.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.pl.xlf new file mode 100644 index 0000000000000..8d563d21206a9 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.pl.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Wystąpił błąd uwierzytelniania. + + + Authentication credentials could not be found. + Dane uwierzytelniania nie zostały znalezione. + + + Authentication request could not be processed due to a system problem. + Żądanie uwierzytelniania nie mogło zostać pomyślnie zakończone z powodu problemu z systemem. + + + Invalid credentials. + Nieprawidłowe dane. + + + Cookie has already been used by someone else. + To ciasteczko jest używane przez kogoś innego. + + + Not privileged to request the resource. + Brak uprawnień dla żądania wskazanego zasobu. + + + Invalid CSRF token. + Nieprawidłowy token CSRF. + + + Digest nonce has expired. + Kod dostępu wygasł. + + + No authentication provider found to support the authentication token. + Nie znaleziono mechanizmu uwierzytelniania zdolnego do obsługi przesłanego tokenu. + + + No session available, it either timed out or cookies are not enabled. + Brak danych sesji, sesja wygasła lub ciasteczka nie są włączone. + + + No token could be found. + Nie znaleziono tokenu. + + + Username could not be found. + Użytkownik o podanej nazwie nie istnieje. + + + Account has expired. + Konto wygasło. + + + Credentials have expired. + Dane uwierzytelniania wygasły. + + + Account is disabled. + Konto jest wyłączone. + + + Account is locked. + Konto jest zablokowane. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.pt_BR.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.pt_BR.xlf new file mode 100644 index 0000000000000..846fd49e6402b --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.pt_BR.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Uma exceção ocorreu durante a autenticação. + + + Authentication credentials could not be found. + As credenciais de autenticação não foram encontradas. + + + Authentication request could not be processed due to a system problem. + A autenticação não pôde ser concluída devido a um problema no sistema. + + + Invalid credentials. + Credenciais inválidas. + + + Cookie has already been used by someone else. + Este cookie já esta em uso. + + + Not privileged to request the resource. + Não possui privilégios o bastante para requisitar este recurso. + + + Invalid CSRF token. + Token CSRF inválido. + + + Digest nonce has expired. + Digest nonce expirado. + + + No authentication provider found to support the authentication token. + Nenhum provedor de autenticação encontrado para suportar o token de autenticação. + + + No session available, it either timed out or cookies are not enabled. + Nenhuma sessão disponível, ela expirou ou cookies estão desativados. + + + No token could be found. + Nenhum token foi encontrado. + + + Username could not be found. + Nome de usuário não encontrado. + + + Account has expired. + A conta esta expirada. + + + Credentials have expired. + As credenciais estão expiradas. + + + Account is disabled. + Conta desativada. + + + Account is locked. + A conta esta travada. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.pt_PT.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.pt_PT.xlf new file mode 100644 index 0000000000000..e661000148273 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.pt_PT.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Ocorreu um excepção durante a autenticação. + + + Authentication credentials could not be found. + As credenciais de autenticação não foram encontradas. + + + Authentication request could not be processed due to a system problem. + O pedido de autenticação não foi concluído devido a um problema no sistema. + + + Invalid credentials. + Credenciais inválidas. + + + Cookie has already been used by someone else. + Este cookie já esta em uso. + + + Not privileged to request the resource. + Não possui privilégios para aceder a este recurso. + + + Invalid CSRF token. + Token CSRF inválido. + + + Digest nonce has expired. + Digest nonce expirado. + + + No authentication provider found to support the authentication token. + Nenhum fornecedor de autenticação encontrado para suportar o token de autenticação. + + + No session available, it either timed out or cookies are not enabled. + Não existe sessão disponível, esta expirou ou os cookies estão desativados. + + + No token could be found. + O token não foi encontrado. + + + Username could not be found. + Nome de utilizador não encontrado. + + + Account has expired. + A conta expirou. + + + Credentials have expired. + As credenciais expiraram. + + + Account is disabled. + Conta desativada. + + + Account is locked. + A conta esta trancada. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf new file mode 100644 index 0000000000000..440f11036770d --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + A apărut o eroare de autentificare. + + + Authentication credentials could not be found. + Informațiile de autentificare nu au fost găsite. + + + Authentication request could not be processed due to a system problem. + Sistemul nu a putut procesa cererea de autentificare din cauza unei erori. + + + Invalid credentials. + Date de autentificare invalide. + + + Cookie has already been used by someone else. + Cookieul este folosit deja de altcineva. + + + Not privileged to request the resource. + Permisiuni insuficiente pentru resursa cerută. + + + Invalid CSRF token. + Tokenul CSRF este invalid. + + + Digest nonce has expired. + Tokenul temporar a expirat. + + + No authentication provider found to support the authentication token. + Nu a fost găsit nici un agent de autentificare pentru tokenul specificat. + + + No session available, it either timed out or cookies are not enabled. + Sesiunea nu mai este disponibilă, a expirat sau suportul pentru cookieuri nu este activat. + + + No token could be found. + Tokenul nu a putut fi găsit. + + + Username could not be found. + Numele de utilizator nu a fost găsit. + + + Account has expired. + Contul a expirat. + + + Credentials have expired. + Datele de autentificare au expirat. + + + Account is disabled. + Contul este dezactivat. + + + Account is locked. + Contul este blocat. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ru.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ru.xlf new file mode 100644 index 0000000000000..1964f95e09a64 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ru.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Ошибка аутентификации. + + + Authentication credentials could not be found. + Аутентификационные данные не найдены. + + + Authentication request could not be processed due to a system problem. + Запрос аутентификации не может быть обработан в связи с проблемой в системе. + + + Invalid credentials. + Недействительные аутентификационные данные. + + + Cookie has already been used by someone else. + Cookie уже был использован кем-то другим. + + + Not privileged to request the resource. + Отсутствуют права на запрос этого ресурса. + + + Invalid CSRF token. + Недействительный токен CSRF. + + + Digest nonce has expired. + Время действия одноразового ключа дайджеста истекло. + + + No authentication provider found to support the authentication token. + Не найден провайдер аутентификации, поддерживающий токен аутентификации. + + + No session available, it either timed out or cookies are not enabled. + Сессия не найдена, ее время истекло, либо cookies не включены. + + + No token could be found. + Токен не найден. + + + Username could not be found. + Имя пользователя не найдено. + + + Account has expired. + Время действия учетной записи истекло. + + + Credentials have expired. + Время действия аутентификационных данных истекло. + + + Account is disabled. + Учетная запись отключена. + + + Account is locked. + Учетная запись заблокирована. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.sk.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.sk.xlf new file mode 100644 index 0000000000000..e6552a6a0914e --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.sk.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Pri overovaní došlo k chybe. + + + Authentication credentials could not be found. + Overovacie údaje neboli nájdené. + + + Authentication request could not be processed due to a system problem. + Požiadavok na overenie nemohol byť spracovaný kvôli systémovej chybe. + + + Invalid credentials. + Neplatné prihlasovacie údaje. + + + Cookie has already been used by someone else. + Cookie už bolo použité niekým iným. + + + Not privileged to request the resource. + Nemáte oprávnenie pristupovať k prostriedku. + + + Invalid CSRF token. + Neplatný CSRF token. + + + Digest nonce has expired. + Platnosť inicializačného vektoru (digest nonce) skončila. + + + No authentication provider found to support the authentication token. + Poskytovateľ pre overovací token nebol nájdený. + + + No session available, it either timed out or cookies are not enabled. + Session nie je k dispozíci, vypršala jej platnosť, alebo sú zakázané cookies. + + + No token could be found. + Token nebol nájdený. + + + Username could not be found. + Prihlasovacie meno nebolo nájdené. + + + Account has expired. + Platnosť účtu skončila. + + + Credentials have expired. + Platnosť prihlasovacích údajov skončila. + + + Account is disabled. + Účet je zakázaný. + + + Account is locked. + Účet je zablokovaný. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.sl.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.sl.xlf new file mode 100644 index 0000000000000..ee70c9aaa4af0 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.sl.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Prišlo je do izjeme pri preverjanju avtentikacije. + + + Authentication credentials could not be found. + Poverilnic za avtentikacijo ni bilo mogoče najti. + + + Authentication request could not be processed due to a system problem. + Zahteve za avtentikacijo ni bilo mogoče izvesti zaradi sistemske težave. + + + Invalid credentials. + Neveljavne pravice. + + + Cookie has already been used by someone else. + Piškotek je uporabil že nekdo drug. + + + Not privileged to request the resource. + Nimate privilegijev za zahtevani vir. + + + Invalid CSRF token. + Neveljaven CSRF žeton. + + + Digest nonce has expired. + Začasni žeton je potekel. + + + No authentication provider found to support the authentication token. + Ponudnika avtentikacije za podporo prijavnega žetona ni bilo mogoče najti. + + + No session available, it either timed out or cookies are not enabled. + Seja ni na voljo, ali je potekla ali pa piškotki niso omogočeni. + + + No token could be found. + Žetona ni bilo mogoče najti. + + + Username could not be found. + Uporabniškega imena ni bilo mogoče najti. + + + Account has expired. + Račun je potekel. + + + Credentials have expired. + Poverilnice so potekle. + + + Account is disabled. + Račun je onemogočen. + + + Account is locked. + Račun je zaklenjen. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.sr_Cyrl.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.sr_Cyrl.xlf new file mode 100644 index 0000000000000..35e4ddf29b28c --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.sr_Cyrl.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Изузетак при аутентификацији. + + + Authentication credentials could not be found. + Аутентификациони подаци нису пронађени. + + + Authentication request could not be processed due to a system problem. + Захтев за аутентификацију не може бити обрађен због системских проблема. + + + Invalid credentials. + Невалидни подаци за аутентификацију. + + + Cookie has already been used by someone else. + Колачић је већ искоришћен од стране неког другог. + + + Not privileged to request the resource. + Немате права приступа овом ресурсу. + + + Invalid CSRF token. + Невалидан CSRF токен. + + + Digest nonce has expired. + Време криптографског кључа је истекло. + + + No authentication provider found to support the authentication token. + Аутентификациони провајдер за подршку токена није пронађен. + + + No session available, it either timed out or cookies are not enabled. + Сесија није доступна, истекла је или су колачићи искључени. + + + No token could be found. + Токен не може бити пронађен. + + + Username could not be found. + Корисничко име не може бити пронађено. + + + Account has expired. + Налог је истекао. + + + Credentials have expired. + Подаци за аутентификацију су истекли. + + + Account is disabled. + Налог је онемогућен. + + + Account is locked. + Налог је закључан. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.sr_Latn.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.sr_Latn.xlf new file mode 100644 index 0000000000000..ddc48076a2a6e --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.sr_Latn.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Izuzetak pri autentifikaciji. + + + Authentication credentials could not be found. + Autentifikacioni podaci nisu pronađeni. + + + Authentication request could not be processed due to a system problem. + Zahtev za autentifikaciju ne može biti obrađen zbog sistemskih problema. + + + Invalid credentials. + Nevalidni podaci za autentifikaciju. + + + Cookie has already been used by someone else. + Kolačić je već iskorišćen od strane nekog drugog. + + + Not privileged to request the resource. + Nemate prava pristupa ovom resursu. + + + Invalid CSRF token. + Nevalidan CSRF token. + + + Digest nonce has expired. + Vreme kriptografskog ključa je isteklo. + + + No authentication provider found to support the authentication token. + Autentifikacioni provajder za podršku tokena nije pronađen. + + + No session available, it either timed out or cookies are not enabled. + Sesija nije dostupna, istekla je ili su kolačići isključeni. + + + No token could be found. + Token ne može biti pronađen. + + + Username could not be found. + Korisničko ime ne može biti pronađeno. + + + Account has expired. + Nalog je istekao. + + + Credentials have expired. + Podaci za autentifikaciju su istekli. + + + Account is disabled. + Nalog je onemogućen. + + + Account is locked. + Nalog je zaključan. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.sv.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.sv.xlf new file mode 100644 index 0000000000000..b5f62092365fa --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.sv.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Ett autentiseringsfel har inträffat. + + + Authentication credentials could not be found. + Uppgifterna för autentisering kunde inte hittas. + + + Authentication request could not be processed due to a system problem. + Autentiseringen kunde inte genomföras på grund av systemfel. + + + Invalid credentials. + Felaktiga uppgifter. + + + Cookie has already been used by someone else. + Cookien har redan använts av någon annan. + + + Not privileged to request the resource. + Saknar rättigheter för resursen. + + + Invalid CSRF token. + Ogiltig CSRF-token. + + + Digest nonce has expired. + Förfallen digest nonce. + + + No authentication provider found to support the authentication token. + Ingen leverantör för autentisering hittades för angiven autentiseringstoken. + + + No session available, it either timed out or cookies are not enabled. + Ingen session finns tillgänglig, antingen har den förfallit eller är cookies inte aktiverat. + + + No token could be found. + Ingen token kunde hittas. + + + Username could not be found. + Användarnamnet kunde inte hittas. + + + Account has expired. + Kontot har förfallit. + + + Credentials have expired. + Uppgifterna har förfallit. + + + Account is disabled. + Kontot är inaktiverat. + + + Account is locked. + Kontot är låst. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.tr.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.tr.xlf new file mode 100644 index 0000000000000..fbf9b260b05c0 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.tr.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Bir yetkilendirme istisnası oluştu. + + + Authentication credentials could not be found. + Yetkilendirme girdileri bulunamadı. + + + Authentication request could not be processed due to a system problem. + Bir sistem hatası nedeniyle yetkilendirme isteği işleme alınamıyor. + + + Invalid credentials. + Geçersiz girdiler. + + + Cookie has already been used by someone else. + Çerez bir başkası tarafından zaten kullanılmıştı. + + + Not privileged to request the resource. + Kaynak talebi için imtiyaz bulunamadı. + + + Invalid CSRF token. + Geçersiz CSRF fişi. + + + Digest nonce has expired. + Derleme zaman aşımı gerçekleşti. + + + No authentication provider found to support the authentication token. + Yetkilendirme fişini destekleyecek yetkilendirme sağlayıcısı bulunamadı. + + + No session available, it either timed out or cookies are not enabled. + Oturum bulunamadı, zaman aşımına uğradı veya çerezler etkin değil. + + + No token could be found. + Bilet bulunamadı. + + + Username could not be found. + Kullanıcı adı bulunamadı. + + + Account has expired. + Hesap zaman aşımına uğradı. + + + Credentials have expired. + Girdiler zaman aşımına uğradı. + + + Account is disabled. + Hesap devre dışı bırakılmış. + + + Account is locked. + Hesap kilitlenmiş. + + + + diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ua.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ua.xlf new file mode 100644 index 0000000000000..79721212068db --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ua.xlf @@ -0,0 +1,71 @@ + + + + + + An authentication exception occurred. + Помилка автентифікації. + + + Authentication credentials could not be found. + Автентифікаційні дані не знайдено. + + + Authentication request could not be processed due to a system problem. + Запит на автентифікацію не може бути опрацьовано у зв’язку з проблемою в системі. + + + Invalid credentials. + Невірні автентифікаційні дані. + + + Cookie has already been used by someone else. + Хтось інший вже використав цей сookie. + + + Not privileged to request the resource. + Відсутні права на запит цього ресурсу. + + + Invalid CSRF token. + Невірний токен CSRF. + + + Digest nonce has expired. + Закінчився термін дії одноразового ключа дайджесту. + + + No authentication provider found to support the authentication token. + Не знайдено провайдера автентифікації, що підтримує токен автентифікаціії. + + + No session available, it either timed out or cookies are not enabled. + Сесія недоступна, її час вийшов, або cookies вимкнено. + + + No token could be found. + Токен не знайдено. + + + Username could not be found. + Ім’я користувача не знайдено. + + + Account has expired. + Термін дії облікового запису вичерпано. + + + Credentials have expired. + Термін дії автентифікаційних даних вичерпано. + + + Account is disabled. + Обліковий запис відключено. + + + Account is locked. + Обліковий запис заблоковано. + + + + diff --git a/src/Symfony/Component/Security/Core/Role/Role.php b/src/Symfony/Component/Security/Core/Role/Role.php index 5b50981fe1a78..511ba02755932 100644 --- a/src/Symfony/Component/Security/Core/Role/Role.php +++ b/src/Symfony/Component/Security/Core/Role/Role.php @@ -38,4 +38,14 @@ public function getRole() { return $this->role; } + + /** + * Casts the role to string + * + * @return string + */ + public function __toString() + { + return $this->role; + } } diff --git a/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php b/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php index 2e7df0ebcfaf3..793007e7f5420 100644 --- a/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php +++ b/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php @@ -19,7 +19,7 @@ class RoleHierarchy implements RoleHierarchyInterface { private $hierarchy; - private $map; + protected $map; /** * Constructor. @@ -52,7 +52,7 @@ public function getReachableRoles(array $roles) return $reachableRoles; } - private function buildRoleMap() + protected function buildRoleMap() { $this->map = array(); foreach ($this->hierarchy as $main => $roles) { diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/AuthenticationProviderManagerTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationProviderManagerTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Core/Authentication/AuthenticationProviderManagerTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationProviderManagerTest.php index 4120995ecbbea..b053d73b8fbf7 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/AuthenticationProviderManagerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationProviderManagerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication; +namespace Symfony\Component\Security\Core\Tests\Authentication; use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/AuthenticationTrustResolverTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationTrustResolverTest.php similarity index 97% rename from src/Symfony/Component/Security/Tests/Core/Authentication/AuthenticationTrustResolverTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationTrustResolverTest.php index c3b15852e6c1e..07ce08b6da351 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/AuthenticationTrustResolverTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationTrustResolverTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication; +namespace Symfony\Component\Security\Core\Tests\Authentication; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/AnonymousAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/AnonymousAuthenticationProviderTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Provider/AnonymousAuthenticationProviderTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Provider/AnonymousAuthenticationProviderTest.php index d0da14724e3c1..5a189b0ec6450 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/AnonymousAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/AnonymousAuthenticationProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Provider; +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use Symfony\Component\Security\Core\Authentication\Provider\AnonymousAuthenticationProvider; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/DaoAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php similarity index 99% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Provider/DaoAuthenticationProviderTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php index 8b270615dc1d3..ed4fe10634d88 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/DaoAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/DaoAuthenticationProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Provider; +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php index f7ffb1e509b79..522edb462bc78 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Provider; +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use Symfony\Component\Security\Core\Authentication\Provider\PreAuthenticatedAuthenticationProvider; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/RememberMeAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Provider/RememberMeAuthenticationProviderTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php index 5e250e0d0791e..43da274d5b476 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/RememberMeAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Provider; +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use Symfony\Component\Security\Core\Authentication\Provider\RememberMeAuthenticationProvider; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/UserAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php similarity index 99% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Provider/UserAuthenticationProviderTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php index 22a7e5d1f4d13..db47589987e91 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Provider/UserAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Provider; +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/RememberMe/InMemoryTokenProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Core/Authentication/RememberMe/InMemoryTokenProviderTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php index 1739714f2baeb..3bdf38cff6fd9 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/RememberMe/InMemoryTokenProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/InMemoryTokenProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\RememberMe; +namespace Symfony\Component\Security\Core\Tests\Authentication\RememberMe; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; use Symfony\Component\Security\Core\Authentication\RememberMe\InMemoryTokenProvider; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/RememberMe/PersistentTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php similarity index 93% rename from src/Symfony/Component/Security/Tests/Core/Authentication/RememberMe/PersistentTokenTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php index 3903591c752e4..903c0309fc541 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/RememberMe/PersistentTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/PersistentTokenTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\RememberMe; +namespace Symfony\Component\Security\Core\Tests\Authentication\RememberMe; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/AbstractTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AbstractTokenTest.php similarity index 99% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Token/AbstractTokenTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Token/AbstractTokenTest.php index 5683b782cbccd..098017e767576 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/AbstractTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AbstractTokenTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Token; +namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/AnonymousTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AnonymousTokenTest.php similarity index 94% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Token/AnonymousTokenTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Token/AnonymousTokenTest.php index 135397bff37be..b5cf00683aa5f 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/AnonymousTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AnonymousTokenTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Token; +namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/PreAuthenticatedTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/PreAuthenticatedTokenTest.php similarity index 95% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Token/PreAuthenticatedTokenTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Token/PreAuthenticatedTokenTest.php index 59a533a5b72a2..77d2608996430 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/PreAuthenticatedTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/PreAuthenticatedTokenTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Token; +namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/UsernamePasswordTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UsernamePasswordTokenTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Core/Authentication/Token/UsernamePasswordTokenTest.php rename to src/Symfony/Component/Security/Core/Tests/Authentication/Token/UsernamePasswordTokenTest.php index 67f431f671ffa..0297eff939ab6 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authentication/Token/UsernamePasswordTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UsernamePasswordTokenTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authentication\Token; +namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Core/Authorization/AccessDecisionManagerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php similarity index 94% rename from src/Symfony/Component/Security/Tests/Core/Authorization/AccessDecisionManagerTest.php rename to src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php index b99423f767ca8..b0557a3dcb1e6 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authorization/AccessDecisionManagerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authorization; +namespace Symfony\Component\Security\Core\Tests\Authorization; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; @@ -54,6 +54,14 @@ public function testSetVotersEmpty() $manager = new AccessDecisionManager(array()); } + /** + * @expectedException \InvalidArgumentException + */ + public function testSetUnsupportedStrategy() + { + new AccessDecisionManager(array($this->getVoter(VoterInterface::ACCESS_GRANTED)), 'fooBar'); + } + /** * @dataProvider getStrategyTests */ diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php new file mode 100644 index 0000000000000..5b4aca610a840 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Tests\Authorization; + +use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\User; + +class ExpressionLanguageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider provider + */ + public function testIsAuthenticated($token, $expression, $result, array $roles = array()) + { + $anonymousTokenClass = 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken'; + $rememberMeTokenClass = 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\RememberMeToken'; + $expressionLanguage = new ExpressionLanguage(); + $trustResolver = new AuthenticationTrustResolver($anonymousTokenClass, $rememberMeTokenClass); + + $context = array(); + $context['trust_resolver'] = $trustResolver; + $context['token'] = $token; + $context['roles'] = $roles; + + $this->assertEquals($result, $expressionLanguage->evaluate($expression, $context)); + } + + public function provider() + { + $roles = array('ROLE_USER', 'ROLE_ADMIN'); + $user = new User('username', 'password', $roles); + + $noToken = null; + $anonymousToken = new AnonymousToken('firewall', 'anon.'); + $rememberMeToken = new RememberMeToken($user, 'providerkey', 'firewall'); + $usernamePasswordToken = new UsernamePasswordToken('username', 'password', 'providerkey', $roles); + + return array( + array($noToken, 'is_anonymous()', false), + array($noToken, 'is_authenticated()', false), + array($noToken, 'is_fully_authenticated()', false), + array($noToken, 'is_remember_me()', false), + array($noToken, "has_role('ROLE_USER')", false), + + array($anonymousToken, 'is_anonymous()', true), + array($anonymousToken, 'is_authenticated()', false), + array($anonymousToken, 'is_fully_authenticated()', false), + array($anonymousToken, 'is_remember_me()', false), + array($anonymousToken, "has_role('ROLE_USER')", false), + + array($rememberMeToken, 'is_anonymous()', false), + array($rememberMeToken, 'is_authenticated()', true), + array($rememberMeToken, 'is_fully_authenticated()', false), + array($rememberMeToken, 'is_remember_me()', true), + array($rememberMeToken, "has_role('ROLE_FOO')", false, $roles), + array($rememberMeToken, "has_role('ROLE_USER')", true, $roles), + + array($usernamePasswordToken, 'is_anonymous()', false), + array($usernamePasswordToken, 'is_authenticated()', true), + array($usernamePasswordToken, 'is_fully_authenticated()', true), + array($usernamePasswordToken, 'is_remember_me()', false), + array($usernamePasswordToken, "has_role('ROLE_FOO')", false, $roles), + array($usernamePasswordToken, "has_role('ROLE_USER')", true, $roles), + ); + } +} diff --git a/src/Symfony/Component/Security/Tests/Core/Authorization/Voter/AuthenticatedVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Core/Authorization/Voter/AuthenticatedVoterTest.php rename to src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php index b077712bc964a..4679c0ff0e5ea 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authorization/Voter/AuthenticatedVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authorization\Voter; +namespace Symfony\Component\Security\Core\Tests\Authorization\Voter; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php new file mode 100644 index 0000000000000..dc8ea7960e960 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.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\Security\Core\Tests\Authorization\Voter; + +use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; +use Symfony\Component\Security\Core\Role\Role; + +class ExpressionVoterTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportsAttribute() + { + $expression = $this->createExpression(); + $expressionLanguage = $this->getMock('Symfony\Component\Security\Core\Authorization\ExpressionLanguage'); + $voter = new ExpressionVoter($expressionLanguage, $this->createTrustResolver(), $this->createRoleHierarchy()); + + $this->assertTrue($voter->supportsAttribute($expression)); + } + + /** + * @dataProvider getVoteTests + */ + public function testVote($roles, $attributes, $expected, $tokenExpectsGetRoles = true, $expressionLanguageExpectsEvaluate = true) + { + $voter = new ExpressionVoter($this->createExpressionLanguage($expressionLanguageExpectsEvaluate), $this->createTrustResolver()); + + $this->assertSame($expected, $voter->vote($this->getToken($roles, $tokenExpectsGetRoles), null, $attributes)); + } + + public function getVoteTests() + { + return array( + array(array(), array(), VoterInterface::ACCESS_ABSTAIN, false, false), + array(array(), array('FOO'), VoterInterface::ACCESS_ABSTAIN, false, false), + + array(array(), array($this->createExpression()), VoterInterface::ACCESS_DENIED, true, false), + + array(array('ROLE_FOO'), array($this->createExpression(), $this->createExpression()), VoterInterface::ACCESS_GRANTED), + array(array('ROLE_BAR', 'ROLE_FOO'), array($this->createExpression()), VoterInterface::ACCESS_GRANTED), + ); + } + + protected function getToken(array $roles, $tokenExpectsGetRoles = true) + { + foreach ($roles as $i => $role) { + $roles[$i] = new Role($role); + } + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + if ($tokenExpectsGetRoles) { + $token->expects($this->once()) + ->method('getRoles') + ->will($this->returnValue($roles)); + } + + return $token; + } + + protected function createExpressionLanguage($expressionLanguageExpectsEvaluate = true) + { + $mock = $this->getMock('Symfony\Component\Security\Core\Authorization\ExpressionLanguage'); + + if ($expressionLanguageExpectsEvaluate) { + $mock->expects($this->once()) + ->method('evaluate') + ->will($this->returnValue(true)); + } + + return $mock; + } + + protected function createTrustResolver() + { + return $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface'); + } + + protected function createRoleHierarchy() + { + return $this->getMock('Symfony\Component\Security\Core\Role\RoleHierarchyInterface'); + } + + protected function createExpression() + { + return $this->getMockBuilder('Symfony\Component\ExpressionLanguage\Expression') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/src/Symfony/Component/Security/Tests/Core/Authorization/Voter/RoleHierarchyVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php similarity index 94% rename from src/Symfony/Component/Security/Tests/Core/Authorization/Voter/RoleHierarchyVoterTest.php rename to src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php index a50fa791e053e..c50ecf38c587d 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authorization/Voter/RoleHierarchyVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authorization\Voter; +namespace Symfony\Component\Security\Core\Tests\Authorization\Voter; use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; diff --git a/src/Symfony/Component/Security/Tests/Core/Authorization/Voter/RoleVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Core/Authorization/Voter/RoleVoterTest.php rename to src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php index 63608ebb3b9a8..62e3013254b73 100644 --- a/src/Symfony/Component/Security/Tests/Core/Authorization/Voter/RoleVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Authorization\Voter; +namespace Symfony\Component\Security\Core\Tests\Authorization\Voter; use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/BCryptPasswordEncoderTest.php similarity index 97% rename from src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php rename to src/Symfony/Component/Security/Core/Tests/Encoder/BCryptPasswordEncoderTest.php index dd962fd095858..2213dc5d6b0c6 100644 --- a/src/Symfony/Component/Security/Tests/Core/Encoder/BCryptPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/BCryptPasswordEncoderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Encoder; +namespace Symfony\Component\Security\Core\Tests\Encoder; use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/BasePasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/BasePasswordEncoderTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Core/Encoder/BasePasswordEncoderTest.php rename to src/Symfony/Component/Security/Core/Tests/Encoder/BasePasswordEncoderTest.php index 702efb073c54b..14c488bc109fb 100644 --- a/src/Symfony/Component/Security/Tests/Core/Encoder/BasePasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/BasePasswordEncoderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Encoder; +namespace Symfony\Component\Security\Core\Tests\Encoder; use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder; diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/EncoderFactoryTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php similarity index 56% rename from src/Symfony/Component/Security/Tests/Core/Encoder/EncoderFactoryTest.php rename to src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php index 2e55a4b6f8c69..3d34d0468d58d 100644 --- a/src/Symfony/Component/Security/Tests/Core/Encoder/EncoderFactoryTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php @@ -9,10 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Encoder; +namespace Symfony\Component\Security\Core\Tests\Encoder; use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; use Symfony\Component\Security\Core\Encoder\EncoderFactory; +use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; @@ -52,7 +53,7 @@ public function testGetEncoderWithClassName() 'Symfony\Component\Security\Core\User\UserInterface' => new MessageDigestPasswordEncoder('sha1'), )); - $encoder = $factory->getEncoder('Symfony\Component\Security\Tests\Core\Encoder\SomeChildUser'); + $encoder = $factory->getEncoder('Symfony\Component\Security\Core\Tests\Encoder\SomeChildUser'); $expectedEncoder = new MessageDigestPasswordEncoder('sha1'); $this->assertEquals($expectedEncoder->encodePassword('foo', ''), $encoder->encodePassword('foo', '')); } @@ -71,10 +72,63 @@ public function testGetEncoderConfiguredForConcreteClassWithService() public function testGetEncoderConfiguredForConcreteClassWithClassName() { $factory = new EncoderFactory(array( - 'Symfony\Component\Security\Tests\Core\Encoder\SomeUser' => new MessageDigestPasswordEncoder('sha1'), + 'Symfony\Component\Security\Core\Tests\Encoder\SomeUser' => new MessageDigestPasswordEncoder('sha1'), )); - $encoder = $factory->getEncoder('Symfony\Component\Security\Tests\Core\Encoder\SomeChildUser'); + $encoder = $factory->getEncoder('Symfony\Component\Security\Core\Tests\Encoder\SomeChildUser'); + $expectedEncoder = new MessageDigestPasswordEncoder('sha1'); + $this->assertEquals($expectedEncoder->encodePassword('foo', ''), $encoder->encodePassword('foo', '')); + } + + public function testGetNamedEncoderForEncoderAware() + { + $factory = new EncoderFactory(array( + 'Symfony\Component\Security\Core\Tests\Encoder\EncAwareUser' => new MessageDigestPasswordEncoder('sha256'), + 'encoder_name' => new MessageDigestPasswordEncoder('sha1') + )); + + $encoder = $factory->getEncoder(new EncAwareUser('user', 'pass')); + $expectedEncoder = new MessageDigestPasswordEncoder('sha1'); + $this->assertEquals($expectedEncoder->encodePassword('foo', ''), $encoder->encodePassword('foo', '')); + } + + public function testGetNullNamedEncoderForEncoderAware() + { + $factory = new EncoderFactory(array( + 'Symfony\Component\Security\Core\Tests\Encoder\EncAwareUser' => new MessageDigestPasswordEncoder('sha1'), + 'encoder_name' => new MessageDigestPasswordEncoder('sha256') + )); + + $user = new EncAwareUser('user', 'pass'); + $user->encoderName = null; + $encoder = $factory->getEncoder($user); + $expectedEncoder = new MessageDigestPasswordEncoder('sha1'); + $this->assertEquals($expectedEncoder->encodePassword('foo', ''), $encoder->encodePassword('foo', '')); + } + + /** + * @expectedException RuntimeException + */ + public function testGetInvalidNamedEncoderForEncoderAware() + { + $factory = new EncoderFactory(array( + 'Symfony\Component\Security\Core\Tests\Encoder\EncAwareUser' => new MessageDigestPasswordEncoder('sha1'), + 'encoder_name' => new MessageDigestPasswordEncoder('sha256') + )); + + $user = new EncAwareUser('user', 'pass'); + $user->encoderName = 'invalid_encoder_name'; + $encoder = $factory->getEncoder($user); + } + + public function testGetEncoderForEncoderAwareWithClassName() + { + $factory = new EncoderFactory(array( + 'Symfony\Component\Security\Core\Tests\Encoder\EncAwareUser' => new MessageDigestPasswordEncoder('sha1'), + 'encoder_name' => new MessageDigestPasswordEncoder('sha256') + )); + + $encoder = $factory->getEncoder('Symfony\Component\Security\Core\Tests\Encoder\EncAwareUser'); $expectedEncoder = new MessageDigestPasswordEncoder('sha1'); $this->assertEquals($expectedEncoder->encodePassword('foo', ''), $encoder->encodePassword('foo', '')); } @@ -92,3 +146,13 @@ public function eraseCredentials() {} class SomeChildUser extends SomeUser { } + +class EncAwareUser extends SomeUser implements EncoderAwareInterface +{ + public $encoderName = 'encoder_name'; + + public function getEncoderName() + { + return $this->encoderName; + } +} diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/MessageDigestPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/MessageDigestPasswordEncoderTest.php similarity index 97% rename from src/Symfony/Component/Security/Tests/Core/Encoder/MessageDigestPasswordEncoderTest.php rename to src/Symfony/Component/Security/Core/Tests/Encoder/MessageDigestPasswordEncoderTest.php index f37d3bc4171bc..5189fff01851e 100644 --- a/src/Symfony/Component/Security/Tests/Core/Encoder/MessageDigestPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/MessageDigestPasswordEncoderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Encoder; +namespace Symfony\Component\Security\Core\Tests\Encoder; use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder; diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/Pbkdf2PasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/Pbkdf2PasswordEncoderTest.php similarity index 97% rename from src/Symfony/Component/Security/Tests/Core/Encoder/Pbkdf2PasswordEncoderTest.php rename to src/Symfony/Component/Security/Core/Tests/Encoder/Pbkdf2PasswordEncoderTest.php index ca16f0237e4b7..3e9452b1a7f6c 100644 --- a/src/Symfony/Component/Security/Tests/Core/Encoder/Pbkdf2PasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/Pbkdf2PasswordEncoderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Encoder; +namespace Symfony\Component\Security\Core\Tests\Encoder; use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; diff --git a/src/Symfony/Component/Security/Tests/Core/Encoder/PlaintextPasswordEncoderTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/PlaintextPasswordEncoderTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Core/Encoder/PlaintextPasswordEncoderTest.php rename to src/Symfony/Component/Security/Core/Tests/Encoder/PlaintextPasswordEncoderTest.php index 8b1b888b0c428..c7e0d2ad3840a 100644 --- a/src/Symfony/Component/Security/Tests/Core/Encoder/PlaintextPasswordEncoderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/PlaintextPasswordEncoderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Encoder; +namespace Symfony\Component\Security\Core\Tests\Encoder; use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder; diff --git a/src/Symfony/Component/Security/Tests/Core/Role/RoleHierarchyTest.php b/src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Core/Role/RoleHierarchyTest.php rename to src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php index a98aed617cecc..df1b6a3300ba0 100644 --- a/src/Symfony/Component/Security/Tests/Core/Role/RoleHierarchyTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Role; +namespace Symfony\Component\Security\Core\Tests\Role; use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Core/Role/RoleTest.php b/src/Symfony/Component/Security/Core/Tests/Role/RoleTest.php similarity index 89% rename from src/Symfony/Component/Security/Tests/Core/Role/RoleTest.php rename to src/Symfony/Component/Security/Core/Tests/Role/RoleTest.php index e2e7ca86b637f..02be07b2e0728 100644 --- a/src/Symfony/Component/Security/Tests/Core/Role/RoleTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Role/RoleTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Role; +namespace Symfony\Component\Security\Core\Tests\Role; use Symfony\Component\Security\Core\Role\Role; diff --git a/src/Symfony/Component/Security/Tests/Core/Role/SwitchUserRoleTest.php b/src/Symfony/Component/Security/Core/Tests/Role/SwitchUserRoleTest.php similarity index 93% rename from src/Symfony/Component/Security/Tests/Core/Role/SwitchUserRoleTest.php rename to src/Symfony/Component/Security/Core/Tests/Role/SwitchUserRoleTest.php index bf9b173e3b79f..f0ce468637478 100644 --- a/src/Symfony/Component/Security/Tests/Core/Role/SwitchUserRoleTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Role/SwitchUserRoleTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Role; +namespace Symfony\Component\Security\Core\Tests\Role; use Symfony\Component\Security\Core\Role\SwitchUserRole; diff --git a/src/Symfony/Component/Security/Tests/Core/SecurityContextTest.php b/src/Symfony/Component/Security/Core/Tests/SecurityContextTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Core/SecurityContextTest.php rename to src/Symfony/Component/Security/Core/Tests/SecurityContextTest.php index 124ebf9f44986..dd0e2e3a8896d 100644 --- a/src/Symfony/Component/Security/Tests/Core/SecurityContextTest.php +++ b/src/Symfony/Component/Security/Core/Tests/SecurityContextTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core; +namespace Symfony\Component\Security\Core\Tests; use Symfony\Component\Security\Core\SecurityContext; diff --git a/src/Symfony/Component/Security/Tests/Core/User/ChainUserProviderTest.php b/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php similarity index 99% rename from src/Symfony/Component/Security/Tests/Core/User/ChainUserProviderTest.php rename to src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php index 0fddcd668bff5..57873cfaac63d 100644 --- a/src/Symfony/Component/Security/Tests/Core/User/ChainUserProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\User; +namespace Symfony\Component\Security\Core\Tests\User; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; diff --git a/src/Symfony/Component/Security/Tests/Core/User/UserTest.php b/src/Symfony/Component/Security/Core/Tests/User/UserTest.php similarity index 98% rename from src/Symfony/Component/Security/Tests/Core/User/UserTest.php rename to src/Symfony/Component/Security/Core/Tests/User/UserTest.php index d05f4911ab12e..2fe6daa5e973d 100644 --- a/src/Symfony/Component/Security/Tests/Core/User/UserTest.php +++ b/src/Symfony/Component/Security/Core/Tests/User/UserTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\User; +namespace Symfony\Component\Security\Core\Tests\User; use Symfony\Component\Security\Core\User\User; diff --git a/src/Symfony/Component/Security/Tests/Core/Util/ClassUtilsTest.php b/src/Symfony/Component/Security/Core/Tests/Util/ClassUtilsTest.php similarity index 77% rename from src/Symfony/Component/Security/Tests/Core/Util/ClassUtilsTest.php rename to src/Symfony/Component/Security/Core/Tests/Util/ClassUtilsTest.php index 8359236f6549e..e8f0143eb7b27 100644 --- a/src/Symfony/Component/Security/Tests/Core/Util/ClassUtilsTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Util/ClassUtilsTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Util +namespace Symfony\Component\Security\Core\Tests\Util { use Symfony\Component\Security\Core\Util\ClassUtils; @@ -22,9 +22,9 @@ public static function dataGetClass() array('Symfony\Component\Security\Core\Util\ClassUtils', 'Symfony\Component\Security\Core\Util\ClassUtils'), array('MyProject\Proxies\__CG__\stdClass', 'stdClass'), array('MyProject\Proxies\__CG__\OtherProject\Proxies\__CG__\stdClass', 'stdClass'), - array('MyProject\Proxies\__CG__\Symfony\Component\Security\Tests\Core\Util\ChildObject', 'Symfony\Component\Security\Tests\Core\Util\ChildObject'), - array(new TestObject(), 'Symfony\Component\Security\Tests\Core\Util\TestObject'), - array(new \Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Core\Util\TestObject(), 'Symfony\Component\Security\Tests\Core\Util\TestObject'), + array('MyProject\Proxies\__CG__\Symfony\Component\Security\Core\Tests\Util\ChildObject', 'Symfony\Component\Security\Core\Tests\Util\ChildObject'), + array(new TestObject(), 'Symfony\Component\Security\Core\Tests\Util\TestObject'), + array(new \Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Core\Tests\Util\TestObject(), 'Symfony\Component\Security\Core\Tests\Util\TestObject'), ); } @@ -42,9 +42,9 @@ class TestObject } } -namespace Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Tests\Core\Util +namespace Acme\DemoBundle\Proxy\__CG__\Symfony\Component\Security\Core\Tests\Util { - class TestObject extends \Symfony\Component\Security\Tests\Core\Util\TestObject + class TestObject extends \Symfony\Component\Security\Core\Tests\Util\TestObject { } } diff --git a/src/Symfony/Component/Security/Tests/Core/Util/SecureRandomTest.php b/src/Symfony/Component/Security/Core/Tests/Util/SecureRandomTest.php similarity index 99% rename from src/Symfony/Component/Security/Tests/Core/Util/SecureRandomTest.php rename to src/Symfony/Component/Security/Core/Tests/Util/SecureRandomTest.php index 38e91212f42bc..4cfdb2c91e00e 100644 --- a/src/Symfony/Component/Security/Tests/Core/Util/SecureRandomTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Util/SecureRandomTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Util; +namespace Symfony\Component\Security\Core\Tests\Util; use Symfony\Component\Security\Core\Util\SecureRandom; diff --git a/src/Symfony/Component/Security/Tests/Core/Util/StringUtilsTest.php b/src/Symfony/Component/Security/Core/Tests/Util/StringUtilsTest.php similarity index 90% rename from src/Symfony/Component/Security/Tests/Core/Util/StringUtilsTest.php rename to src/Symfony/Component/Security/Core/Tests/Util/StringUtilsTest.php index aac4139254a43..89da98de66bd5 100644 --- a/src/Symfony/Component/Security/Tests/Core/Util/StringUtilsTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Util/StringUtilsTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Util; +namespace Symfony\Component\Security\Core\Tests\Util; use Symfony\Component\Security\Core\Util\StringUtils; diff --git a/src/Symfony/Component/Security/Tests/Core/Validator/Constraints/UserPasswordValidatorTest.php b/src/Symfony/Component/Security/Core/Tests/Validator/Constraints/UserPasswordValidatorTest.php similarity index 94% rename from src/Symfony/Component/Security/Tests/Core/Validator/Constraints/UserPasswordValidatorTest.php rename to src/Symfony/Component/Security/Core/Tests/Validator/Constraints/UserPasswordValidatorTest.php index c3a8370a7901e..53eeb5fed756e 100644 --- a/src/Symfony/Component/Security/Tests/Core/Validator/Constraints/UserPasswordValidatorTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Validator/Constraints/UserPasswordValidatorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Core\Validator\Constraints; +namespace Symfony\Component\Security\Core\Tests\Validator\Constraints; use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator; @@ -23,10 +23,6 @@ class UserPasswordValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (false === class_exists('Symfony\Component\Validator\Validator')) { - $this->markTestSkipped('The Validator component is required for this test.'); - } - $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); } diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json new file mode 100644 index 0000000000000..249d4c14f3192 --- /dev/null +++ b/src/Symfony/Component/Security/Core/composer.json @@ -0,0 +1,46 @@ +{ + "name": "symfony/security-core", + "type": "library", + "description": "Symfony Security Component - Core Library", + "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.3" + }, + "require-dev": { + "symfony/event-dispatcher": "~2.1", + "symfony/expression-language": "~2.4", + "symfony/http-foundation": "~2.4", + "symfony/validator": "~2.2", + "psr/log": "~1.0", + "ircmaxell/password-compat": "1.0.*" + }, + "suggest": { + "symfony/event-dispatcher": "", + "symfony/http-foundation": "", + "symfony/validator": "For using the user password constraint", + "symfony/expression-language": "For using the expression voter", + "ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\Security\\Core\\": "" } + }, + "target-dir": "Symfony/Component/Security/Core", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + } +} diff --git a/src/Symfony/Component/Security/Core/phpunit.xml.dist b/src/Symfony/Component/Security/Core/phpunit.xml.dist new file mode 100644 index 0000000000000..f085b7255b97c --- /dev/null +++ b/src/Symfony/Component/Security/Core/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./vendor + ./Tests + + + + diff --git a/src/Symfony/Component/Security/Csrf/.gitignore b/src/Symfony/Component/Security/Csrf/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Security/Csrf/CsrfToken.php b/src/Symfony/Component/Security/Csrf/CsrfToken.php new file mode 100644 index 0000000000000..9ccaaebf2df1e --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/CsrfToken.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf; + +/** + * A CSRF token. + * + * @author Bernhard Schussek + */ +class CsrfToken +{ + /** + * @var string + */ + private $id; + + /** + * @var string + */ + private $value; + + /** + * Constructor. + * + * @param string $id The token ID + * @param string $value The actual token value + */ + public function __construct($id, $value) + { + $this->id = (string) $id; + $this->value = (string) $value; + } + + /** + * Returns the ID of the CSRF token. + * + * @return string The token ID + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the value of the CSRF token. + * + * @return string The token value + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns the value of the CSRF token. + * + * @return string The token value + */ + public function __toString() + { + return $this->value; + } +} diff --git a/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php new file mode 100644 index 0000000000000..e1295029acfe7 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/CsrfTokenManager.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\Security\Csrf; + +use Symfony\Component\Security\Core\Util\StringUtils; +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; +use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; +use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; +use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; + +/** + * Default implementation of {@link CsrfTokenManagerInterface}. + * + * @author Bernhard Schussek + */ +class CsrfTokenManager implements CsrfTokenManagerInterface +{ + /** + * @var TokenGeneratorInterface + */ + private $generator; + + /** + * @var TokenStorageInterface + */ + private $storage; + + /** + * Creates a new CSRF provider using PHP's native session storage. + * + * @param TokenGeneratorInterface|null $generator The token generator + * @param TokenStorageInterface|null $storage The storage for storing + * generated CSRF tokens + */ + public function __construct(TokenGeneratorInterface $generator = null, TokenStorageInterface $storage = null) + { + $this->generator = $generator ?: new UriSafeTokenGenerator(); + $this->storage = $storage ?: new NativeSessionTokenStorage(); + } + + /** + * {@inheritdoc} + */ + public function getToken($tokenId) + { + if ($this->storage->hasToken($tokenId)) { + $value = $this->storage->getToken($tokenId); + } else { + $value = $this->generator->generateToken(); + + $this->storage->setToken($tokenId, $value); + } + + return new CsrfToken($tokenId, $value); + } + + /** + * {@inheritdoc} + */ + public function refreshToken($tokenId) + { + $value = $this->generator->generateToken(); + + $this->storage->setToken($tokenId, $value); + + return new CsrfToken($tokenId, $value); + } + + /** + * {@inheritdoc} + */ + public function removeToken($tokenId) + { + return $this->storage->removeToken($tokenId); + } + + /** + * {@inheritdoc} + */ + public function isTokenValid(CsrfToken $token) + { + if (!$this->storage->hasToken($token->getId())) { + return false; + } + + return StringUtils::equals($this->storage->getToken($token->getId()), $token->getValue()); + } +} diff --git a/src/Symfony/Component/Security/Csrf/CsrfTokenManagerInterface.php b/src/Symfony/Component/Security/Csrf/CsrfTokenManagerInterface.php new file mode 100644 index 0000000000000..2b9254b829007 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/CsrfTokenManagerInterface.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf; + +/** + * Manages CSRF tokens. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface CsrfTokenManagerInterface +{ + /** + * Returns a CSRF token for the given ID. + * + * If previously no token existed for the given ID, a new token is + * generated. Otherwise the existing token is returned (with the same value, + * not the same instance). + * + * @param string $tokenId The token ID. You may choose an arbitrary value + * for the ID + * + * @return CsrfToken The CSRF token + */ + public function getToken($tokenId); + + /** + * Generates a new token value for the given ID. + * + * This method will generate a new token for the given token ID, independent + * of whether a token value previously existed or not. It can be used to + * enforce once-only tokens in environments with high security needs. + * + * @param string $tokenId The token ID. You may choose an arbitrary value + * for the ID + * + * @return CsrfToken The CSRF token + */ + public function refreshToken($tokenId); + + /** + * Invalidates the CSRF token with the given ID, if one exists. + * + * @param string $tokenId The token ID + * + * @return string|null Returns the removed token value if one existed, NULL + * otherwise + */ + public function removeToken($tokenId); + + /** + * Returns whether the given CSRF token is valid. + * + * @param CsrfToken $token A CSRF token + * + * @return Boolean Returns true if the token is valid, false otherwise + */ + public function isTokenValid(CsrfToken $token); +} diff --git a/src/Symfony/Component/Security/Csrf/Exception/TokenNotFoundException.php b/src/Symfony/Component/Security/Csrf/Exception/TokenNotFoundException.php new file mode 100644 index 0000000000000..936afdeb113e4 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/Exception/TokenNotFoundException.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\Security\Csrf\Exception; + +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * @author Bernhard Schussek + */ +class TokenNotFoundException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/Security/Csrf/LICENSE b/src/Symfony/Component/Security/Csrf/LICENSE new file mode 100644 index 0000000000000..0b3292cf90235 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2014 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Security/Csrf/README.md b/src/Symfony/Component/Security/Csrf/README.md new file mode 100644 index 0000000000000..95a1062091ed3 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/README.md @@ -0,0 +1,21 @@ +Security Component - CSRF +========================= + +The Security CSRF (cross-site request forgery) component provides a class +`CsrfTokenManager` for generating and validating CSRF tokens. + +Resources +--------- + +Documentation: + +http://symfony.com/doc/2.5/book/security.html + +Tests +----- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Security/Csrf/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php b/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php new file mode 100644 index 0000000000000..3112038eee700 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\Tests; + +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManager; + +/** + * @author Bernhard Schussek + */ +class CsrfTokenManagerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $generator; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storage; + + /** + * @var CsrfTokenManager + */ + private $manager; + + protected function setUp() + { + $this->generator = $this->getMock('Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface'); + $this->storage = $this->getMock('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface'); + $this->manager = new CsrfTokenManager($this->generator, $this->storage); + } + + protected function tearDown() + { + $this->generator = null; + $this->storage = null; + $this->manager = null; + } + + public function testGetNonExistingToken() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(false)); + + $this->generator->expects($this->once()) + ->method('generateToken') + ->will($this->returnValue('TOKEN')); + + $this->storage->expects($this->once()) + ->method('setToken') + ->with('token_id', 'TOKEN'); + + $token = $this->manager->getToken('token_id'); + + $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); + $this->assertSame('token_id', $token->getId()); + $this->assertSame('TOKEN', $token->getValue()); + } + + public function testUseExistingTokenIfAvailable() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(true)); + + $this->storage->expects($this->once()) + ->method('getToken') + ->with('token_id') + ->will($this->returnValue('TOKEN')); + + $token = $this->manager->getToken('token_id'); + + $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); + $this->assertSame('token_id', $token->getId()); + $this->assertSame('TOKEN', $token->getValue()); + } + + public function testRefreshTokenAlwaysReturnsNewToken() + { + $this->storage->expects($this->never()) + ->method('hasToken'); + + $this->generator->expects($this->once()) + ->method('generateToken') + ->will($this->returnValue('TOKEN')); + + $this->storage->expects($this->once()) + ->method('setToken') + ->with('token_id', 'TOKEN'); + + $token = $this->manager->refreshToken('token_id'); + + $this->assertInstanceOf('Symfony\Component\Security\Csrf\CsrfToken', $token); + $this->assertSame('token_id', $token->getId()); + $this->assertSame('TOKEN', $token->getValue()); + } + + public function testMatchingTokenIsValid() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(true)); + + $this->storage->expects($this->once()) + ->method('getToken') + ->with('token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertTrue($this->manager->isTokenValid(new CsrfToken('token_id', 'TOKEN'))); + } + + public function testNonMatchingTokenIsNotValid() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(true)); + + $this->storage->expects($this->once()) + ->method('getToken') + ->with('token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertFalse($this->manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); + } + + public function testNonExistingTokenIsNotValid() + { + $this->storage->expects($this->once()) + ->method('hasToken') + ->with('token_id') + ->will($this->returnValue(false)); + + $this->storage->expects($this->never()) + ->method('getToken'); + + $this->assertFalse($this->manager->isTokenValid(new CsrfToken('token_id', 'FOOBAR'))); + } + + public function testRemoveToken() + { + $this->storage->expects($this->once()) + ->method('removeToken') + ->with('token_id') + ->will($this->returnValue('REMOVED_TOKEN')); + + $this->assertSame('REMOVED_TOKEN', $this->manager->removeToken('token_id')); + } +} diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenGenerator/UriSafeTokenGeneratorTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenGenerator/UriSafeTokenGeneratorTest.php new file mode 100644 index 0000000000000..4fb0c99d3005a --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenGenerator/UriSafeTokenGeneratorTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\Tests\TokenGenerator; + +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; + +/** + * @author Bernhard Schussek + */ +class UriSafeTokenGeneratorTest extends \PHPUnit_Framework_TestCase +{ + const ENTROPY = 1000; + + /** + * A non alpha-numeric byte string + * @var string + */ + private static $bytes; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $random; + + /** + * @var UriSafeTokenGenerator + */ + private $generator; + + public static function setUpBeforeClass() + { + self::$bytes = base64_decode('aMf+Tct/RLn2WQ=='); + } + + protected function setUp() + { + $this->random = $this->getMock('Symfony\Component\Security\Core\Util\SecureRandomInterface'); + $this->generator = new UriSafeTokenGenerator($this->random, self::ENTROPY); + } + + protected function tearDown() + { + $this->random = null; + $this->generator = null; + } + + public function testGenerateToken() + { + $this->random->expects($this->once()) + ->method('nextBytes') + ->with(self::ENTROPY/8) + ->will($this->returnValue(self::$bytes)); + + $token = $this->generator->generateToken(); + + $this->assertTrue(ctype_print($token), 'is printable'); + $this->assertStringNotMatchesFormat('%S+%S', $token, 'is URI safe'); + $this->assertStringNotMatchesFormat('%S/%S', $token, 'is URI safe'); + $this->assertStringNotMatchesFormat('%S=%S', $token, 'is URI safe'); + } +} diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php new file mode 100644 index 0000000000000..724806c881f9f --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/NativeSessionTokenStorageTest.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\Tests\TokenStorage; + +use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage; + +/** + * @author Bernhard Schussek + * + * @runTestsInSeparateProcesses + */ +class NativeSessionTokenStorageTest extends \PHPUnit_Framework_TestCase +{ + const SESSION_NAMESPACE = 'foobar'; + + /** + * @var NativeSessionTokenStorage + */ + private $storage; + + public static function setUpBeforeClass() + { + ini_set('session.save_handler', 'files'); + ini_set('session.save_path', sys_get_temp_dir()); + + parent::setUpBeforeClass(); + } + + protected function setUp() + { + $_SESSION = array(); + + $this->storage = new NativeSessionTokenStorage(self::SESSION_NAMESPACE); + } + + public function testStoreTokenInClosedSession() + { + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION); + } + + public function testStoreTokenInClosedSessionWithExistingSessionId() + { + if (version_compare(PHP_VERSION, '5.4', '<')) { + $this->markTestSkipped('This test requires PHP 5.4 or later.'); + } + + session_id('foobar'); + + $this->assertSame(PHP_SESSION_NONE, session_status()); + + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame(PHP_SESSION_ACTIVE, session_status()); + $this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION); + } + + public function testStoreTokenInActiveSession() + { + session_start(); + + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION); + } + + /** + * @depends testStoreTokenInClosedSession + */ + public function testCheckToken() + { + $this->assertFalse($this->storage->hasToken('token_id')); + + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertTrue($this->storage->hasToken('token_id')); + } + + /** + * @depends testStoreTokenInClosedSession + */ + public function testGetExistingToken() + { + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame('TOKEN', $this->storage->getToken('token_id')); + } + + /** + * @expectedException \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException + */ + public function testGetNonExistingToken() + { + $this->storage->getToken('token_id'); + } + + /** + * @depends testCheckToken + */ + public function testRemoveNonExistingToken() + { + $this->assertNull($this->storage->removeToken('token_id')); + $this->assertFalse($this->storage->hasToken('token_id')); + } + + /** + * @depends testCheckToken + */ + public function testRemoveExistingToken() + { + $this->storage->setToken('token_id', 'TOKEN'); + + $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); + $this->assertFalse($this->storage->hasToken('token_id')); + } +} diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php new file mode 100644 index 0000000000000..4166c1eb4acc4 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php @@ -0,0 +1,262 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\Tests\TokenStorage; + +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; + +/** + * @author Bernhard Schussek + */ +class SessionTokenStorageTest extends \PHPUnit_Framework_TestCase +{ + const SESSION_NAMESPACE = 'foobar'; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $session; + + /** + * @var SessionTokenStorage + */ + private $storage; + + protected function setUp() + { + if (!interface_exists('Symfony\Component\HttpFoundation\Session\SessionInterface')) { + $this->markTestSkipped('The "HttpFoundation" component is not available'); + } + + $this->session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface') + ->disableOriginalConstructor() + ->getMock(); + $this->storage = new SessionTokenStorage($this->session, self::SESSION_NAMESPACE); + } + + public function testStoreTokenInClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('set') + ->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); + + $this->storage->setToken('token_id', 'TOKEN'); + } + + public function testStoreTokenInActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('set') + ->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN'); + + $this->storage->setToken('token_id', 'TOKEN'); + } + + public function testCheckTokenInClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->hasToken('token_id')); + } + + public function testCheckTokenInActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->hasToken('token_id')); + } + + public function testGetExistingTokenFromClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(true)); + + $this->session->expects($this->once()) + ->method('get') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->getToken('token_id')); + } + + public function testGetExistingTokenFromActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(true)); + + $this->session->expects($this->once()) + ->method('get') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->storage->getToken('token_id')); + } + + /** + * @expectedException \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException + */ + public function testGetNonExistingTokenFromClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(false)); + + $this->storage->getToken('token_id'); + } + + /** + * @expectedException \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException + */ + public function testGetNonExistingTokenFromActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('has') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(false)); + + $this->storage->getToken('token_id'); + } + + public function testRemoveNonExistingTokenFromClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(null)); + + $this->assertNull($this->storage->removeToken('token_id')); + } + + public function testRemoveNonExistingTokenFromActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue(null)); + + $this->assertNull($this->storage->removeToken('token_id')); + } + + public function testRemoveExistingTokenFromClosedSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(false)); + + $this->session->expects($this->once()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); + } + + public function testRemoveExistingTokenFromActiveSession() + { + $this->session->expects($this->any()) + ->method('isStarted') + ->will($this->returnValue(true)); + + $this->session->expects($this->never()) + ->method('start'); + + $this->session->expects($this->once()) + ->method('remove') + ->with(self::SESSION_NAMESPACE.'/token_id') + ->will($this->returnValue('TOKEN')); + + $this->assertSame('TOKEN', $this->storage->removeToken('token_id')); + } +} diff --git a/src/Symfony/Component/Security/Csrf/TokenGenerator/TokenGeneratorInterface.php b/src/Symfony/Component/Security/Csrf/TokenGenerator/TokenGeneratorInterface.php new file mode 100644 index 0000000000000..4d81da9c40d9e --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/TokenGenerator/TokenGeneratorInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Csrf\TokenGenerator; + +/** + * Generates and validates CSRF tokens. + * + * You can generate a CSRF token by using the method {@link generateCsrfToken()}. + * This method expects a unique token ID as argument. The token ID can later be + * used to validate a token provided by the user. + * + * Token IDs do not necessarily have to be secret, but they should NEVER be + * created from data provided by the client. A good practice is to hard-code the + * token IDs for the various CSRF tokens used by your application. + * + * You should use the method {@link isCsrfTokenValid()} to check a CSRF token + * submitted by the client. This method will return true if the CSRF token is + * valid. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface TokenGeneratorInterface +{ + /** + * Generates a CSRF token. + * + * @return string The generated CSRF token + */ + public function generateToken(); +} diff --git a/src/Symfony/Component/Security/Csrf/TokenGenerator/UriSafeTokenGenerator.php b/src/Symfony/Component/Security/Csrf/TokenGenerator/UriSafeTokenGenerator.php new file mode 100644 index 0000000000000..558273de22d31 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/TokenGenerator/UriSafeTokenGenerator.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\Security\Csrf\TokenGenerator; + +use Symfony\Component\Security\Core\Util\SecureRandomInterface; +use Symfony\Component\Security\Core\Util\SecureRandom; + +/** + * Generates CSRF tokens. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class UriSafeTokenGenerator implements TokenGeneratorInterface +{ + /** + * The generator for random values. + * + * @var SecureRandomInterface + */ + private $random; + + /** + * The amount of entropy collected for each token (in bits). + * + * @var integer + */ + private $entropy; + + /** + * Generates URI-safe CSRF tokens. + * + * @param SecureRandomInterface|null $random The random value generator used for + * generating entropy + * @param integer $entropy The amount of entropy collected for + * each token (in bits) + */ + public function __construct(SecureRandomInterface $random = null, $entropy = 256) + { + $this->random = $random ?: new SecureRandom(); + $this->entropy = $entropy; + } + + /** + * {@inheritdoc} + */ + public function generateToken() + { + // Generate an URI safe base64 encoded string that does not contain "+", + // "/" or "=" which need to be URL encoded and make URLs unnecessarily + // longer. + $bytes = $this->random->nextBytes($this->entropy / 8); + + return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='); + } +} diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.php new file mode 100644 index 0000000000000..8e9b280008e0a --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/NativeSessionTokenStorage.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\Security\Csrf\TokenStorage; + +use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; + +/** + * Token storage that uses PHP's native session handling. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class NativeSessionTokenStorage implements TokenStorageInterface +{ + /** + * The namespace used to store values in the session. + * @var string + */ + const SESSION_NAMESPACE = '_csrf'; + + /** + * @var Boolean + */ + private $sessionStarted = false; + + /** + * @var string + */ + private $namespace; + + /** + * Initializes the storage with a session namespace. + * + * @param string $namespace The namespace under which the token is stored + * in the session + */ + public function __construct($namespace = self::SESSION_NAMESPACE) + { + $this->namespace = $namespace; + } + + /** + * {@inheritdoc} + */ + public function getToken($tokenId) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + if (!isset($_SESSION[$this->namespace][$tokenId])) { + throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.'); + } + + return (string) $_SESSION[$this->namespace][$tokenId]; + } + + /** + * {@inheritdoc} + */ + public function setToken($tokenId, $token) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + $_SESSION[$this->namespace][$tokenId] = (string) $token; + } + + /** + * {@inheritdoc} + */ + public function hasToken($tokenId) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + return isset($_SESSION[$this->namespace][$tokenId]); + } + + /** + * {@inheritdoc} + */ + public function removeToken($tokenId) + { + if (!$this->sessionStarted) { + $this->startSession(); + } + + $token = isset($_SESSION[$this->namespace][$tokenId]) + ? (string) $_SESSION[$this->namespace][$tokenId] + : null; + + unset($_SESSION[$this->namespace][$tokenId]); + + return $token; + } + + private function startSession() + { + if (version_compare(PHP_VERSION, '5.4', '>=')) { + if (PHP_SESSION_NONE === session_status()) { + session_start(); + } + } elseif (!session_id()) { + session_start(); + } + + $this->sessionStarted = true; + } +} diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php new file mode 100644 index 0000000000000..f08eb962172ad --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.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\Security\Csrf\TokenStorage; + +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; + +/** + * Token storage that uses a Symfony2 Session object. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class SessionTokenStorage implements TokenStorageInterface +{ + /** + * The namespace used to store values in the session. + * @var string + */ + const SESSION_NAMESPACE = '_csrf'; + + /** + * The user session from which the session ID is returned + * @var SessionInterface + */ + private $session; + + /** + * @var string + */ + private $namespace; + + /** + * Initializes the storage with a Session object and a session namespace. + * + * @param SessionInterface $session The user session + * @param string $namespace The namespace under which the token + * is stored in the session + */ + public function __construct(SessionInterface $session, $namespace = self::SESSION_NAMESPACE) + { + $this->session = $session; + $this->namespace = $namespace; + } + + /** + * {@inheritdoc} + */ + public function getToken($tokenId) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + if (!$this->session->has($this->namespace.'/'.$tokenId)) { + throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.'); + } + + return (string) $this->session->get($this->namespace.'/'.$tokenId); + } + + /** + * {@inheritdoc} + */ + public function setToken($tokenId, $token) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + $this->session->set($this->namespace.'/'.$tokenId, (string) $token); + } + + /** + * {@inheritdoc} + */ + public function hasToken($tokenId) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + return $this->session->has($this->namespace.'/'.$tokenId); + } + + /** + * {@inheritdoc} + */ + public function removeToken($tokenId) + { + if (!$this->session->isStarted()) { + $this->session->start(); + } + + return $this->session->remove($this->namespace.'/'.$tokenId); + } +} diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/TokenStorageInterface.php b/src/Symfony/Component/Security/Csrf/TokenStorage/TokenStorageInterface.php new file mode 100644 index 0000000000000..3fb3191249731 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/TokenStorageInterface.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\Security\Csrf\TokenStorage; + +/** + * Stores CSRF tokens. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface TokenStorageInterface +{ + /** + * Reads a stored CSRF token. + * + * @param string $tokenId The token ID + * + * @return string The stored token + * + * @throws \Symfony\Component\Security\Csrf\Exception\TokenNotFoundException If the token ID does not exist + */ + public function getToken($tokenId); + + /** + * Stores a CSRF token. + * + * @param string $tokenId The token ID + * @param string $token The CSRF token + */ + public function setToken($tokenId, $token); + + /** + * Removes a CSRF token. + * + * @param string $tokenId The token ID + * + * @return string|null Returns the removed token if one existed, NULL + * otherwise + */ + public function removeToken($tokenId); + + /** + * Checks whether a token with the given token ID exists. + * + * @param string $tokenId The token ID + * + * @return Boolean Whether a token exists with the given ID + */ + public function hasToken($tokenId); +} diff --git a/src/Symfony/Component/Security/Csrf/composer.json b/src/Symfony/Component/Security/Csrf/composer.json new file mode 100644 index 0000000000000..398a2d3c454f4 --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/security-csrf", + "type": "library", + "description": "Symfony Security Component - CSRF Library", + "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.3", + "symfony/security-core": "~2.4" + }, + "require-dev": { + "symfony/http-foundation": "~2.1" + }, + "suggest": { + "symfony/http-foundation": "For using the class SessionTokenStorage." + }, + "autoload": { + "psr-0": { "Symfony\\Component\\Security\\Csrf\\": "" } + }, + "target-dir": "Symfony/Component/Security/Csrf", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + } +} diff --git a/src/Symfony/Component/Security/Csrf/phpunit.xml.dist b/src/Symfony/Component/Security/Csrf/phpunit.xml.dist new file mode 100644 index 0000000000000..0718c768c5cda --- /dev/null +++ b/src/Symfony/Component/Security/Csrf/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./vendor + ./Tests + + + + diff --git a/src/Symfony/Component/Security/Http/.gitignore b/src/Symfony/Component/Security/Http/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Security/Http/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Security/Http/AccessMap.php b/src/Symfony/Component/Security/Http/AccessMap.php index 051a8c22b1c1c..dc2e66a03aeea 100644 --- a/src/Symfony/Component/Security/Http/AccessMap.php +++ b/src/Symfony/Component/Security/Http/AccessMap.php @@ -28,12 +28,12 @@ class AccessMap implements AccessMapInterface * Constructor. * * @param RequestMatcherInterface $requestMatcher A RequestMatcherInterface instance - * @param array $roles An array of roles needed to access the resource + * @param array $attributes An array of attributes to pass to the access decision manager (like roles) * @param string|null $channel The channel to enforce (http, https, or null) */ - public function add(RequestMatcherInterface $requestMatcher, array $roles = array(), $channel = null) + public function add(RequestMatcherInterface $requestMatcher, array $attributes = array(), $channel = null) { - $this->map[] = array($requestMatcher, $roles, $channel); + $this->map[] = array($requestMatcher, $attributes, $channel); } /** diff --git a/src/Symfony/Component/Security/Http/Authentication/SimpleAuthenticationHandler.php b/src/Symfony/Component/Security/Http/Authentication/SimpleAuthenticationHandler.php new file mode 100644 index 0000000000000..2280d8f5cb125 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authentication/SimpleAuthenticationHandler.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\Security\Http\Authentication; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Psr\Log\LoggerInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\SimpleAuthenticatorInterface; + +/** + * Class to proxy authentication success/failure handlers + * + * Events are sent to the SimpleAuthenticatorInterface if it implements + * the right interface, otherwise (or if it fails to return a Response) + * the default handlers are triggered. + * + * @author Jordi Boggiano + */ +class SimpleAuthenticationHandler implements AuthenticationFailureHandlerInterface, AuthenticationSuccessHandlerInterface +{ + protected $successHandler; + protected $failureHandler; + protected $simpleAuthenticator; + protected $logger; + + /** + * Constructor. + * + * @param SimpleAuthenticatorInterface $authenticator SimpleAuthenticatorInterface instance + * @param AuthenticationSuccessHandlerInterface $successHandler Default success handler + * @param AuthenticationFailureHandlerInterface $failureHandler Default failure handler + * @param LoggerInterface $logger Optional logger + */ + public function __construct(SimpleAuthenticatorInterface $authenticator, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, LoggerInterface $logger = null) + { + $this->simpleAuthenticator = $authenticator; + $this->successHandler = $successHandler; + $this->failureHandler = $failureHandler; + $this->logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token) + { + if ($this->simpleAuthenticator instanceof AuthenticationSuccessHandlerInterface) { + if ($this->logger) { + $this->logger->debug(sprintf('Using the %s object as authentication success handler', get_class($this->simpleAuthenticator))); + } + + $response = $this->simpleAuthenticator->onAuthenticationSuccess($request, $token); + if ($response instanceof Response) { + return $response; + } + + if (null !== $response) { + throw new \UnexpectedValueException(sprintf('The %s::onAuthenticationSuccess method must return null to use the default success handler, or a Response object', get_class($this->simpleAuthenticator))); + } + } + + if ($this->logger) { + $this->logger->debug('Fallback to the default authentication success handler'); + } + + return $this->successHandler->onAuthenticationSuccess($request, $token); + } + + /** + * {@inheritDoc} + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + if ($this->simpleAuthenticator instanceof AuthenticationFailureHandlerInterface) { + if ($this->logger) { + $this->logger->debug(sprintf('Using the %s object as authentication failure handler', get_class($this->simpleAuthenticator))); + } + + $response = $this->simpleAuthenticator->onAuthenticationFailure($request, $exception); + if ($response instanceof Response) { + return $response; + } + + if (null !== $response) { + throw new \UnexpectedValueException(sprintf('The %s::onAuthenticationFailure method must return null to use the default failure handler, or a Response object', get_class($this->simpleAuthenticator))); + } + } + + if ($this->logger) { + $this->logger->debug('Fallback to the default authentication failure handler'); + } + + return $this->failureHandler->onAuthenticationFailure($request, $exception); + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php index 4f1cf30d03a77..98fda5ec6c9b8 100644 --- a/src/Symfony/Component/Security/Http/Firewall.php +++ b/src/Symfony/Component/Security/Http/Firewall.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Security\Http; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -31,6 +31,7 @@ class Firewall implements EventSubscriberInterface { private $map; private $dispatcher; + private $exceptionListeners; /** * Constructor. @@ -42,6 +43,7 @@ public function __construct(FirewallMapInterface $map, EventDispatcherInterface { $this->map = $map; $this->dispatcher = $dispatcher; + $this->exceptionListeners = new \SplObjectStorage(); } /** @@ -51,13 +53,14 @@ public function __construct(FirewallMapInterface $map, EventDispatcherInterface */ public function onKernelRequest(GetResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } // register listeners for this firewall list($listeners, $exception) = $this->map->getListeners($event->getRequest()); if (null !== $exception) { + $this->exceptionListeners[$event->getRequest()] = $exception; $exception->register($this->dispatcher); } @@ -71,11 +74,24 @@ public function onKernelRequest(GetResponseEvent $event) } } + public function onKernelFinishRequest(FinishRequestEvent $event) + { + $request = $event->getRequest(); + + if (isset($this->exceptionListeners[$request])) { + $this->exceptionListeners[$request]->unregister($this->dispatcher); + unset($this->exceptionListeners[$request]); + } + } + /** * {@inheritDoc} */ public static function getSubscribedEvents() { - return array(KernelEvents::REQUEST => array('onKernelRequest', 8)); + return array( + KernelEvents::REQUEST => array('onKernelRequest', 8), + KernelEvents::FINISH_REQUEST => 'onKernelFinishRequest', + ); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 60ab3dfdfb119..05e260e74cd2f 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Security\Http\Firewall; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; @@ -66,7 +65,7 @@ public function __construct(SecurityContextInterface $context, array $userProvid */ public function handle(GetResponseEvent $event) { - if (!$this->registered && null !== $this->dispatcher && HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) { + if (!$this->registered && null !== $this->dispatcher && $event->isMasterRequest()) { $this->dispatcher->addListener(KernelEvents::RESPONSE, array($this, 'onKernelResponse')); $this->registered = true; } @@ -106,7 +105,7 @@ public function handle(GetResponseEvent $event) */ public function onKernelResponse(FilterResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (!$event->isMasterRequest()) { return; } diff --git a/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php index ea85e776a84c2..a5e0222b32b61 100644 --- a/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/DigestAuthenticationListener.php @@ -139,7 +139,7 @@ private function fail(GetResponseEvent $event, Request $request, AuthenticationE class DigestData { - private $elements; + private $elements = array(); private $header; private $nonceExpiryTime; @@ -147,7 +147,6 @@ public function __construct($header) { $this->header = $header; preg_match_all('/(\w+)=("((?:[^"\\\\]|\\\\.)+)"|([^\s,$]+))/', $header, $matches, PREG_SET_ORDER); - $this->elements = array(); foreach ($matches as $match) { if (isset($match[1]) && isset($match[3])) { $this->elements[$match[1]] = isset($match[4]) ? $match[4] : $match[3]; diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php index e7e29891cae4b..d0b167e38b4a0 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -69,6 +69,16 @@ public function register(EventDispatcherInterface $dispatcher) $dispatcher->addListener(KernelEvents::EXCEPTION, array($this, 'onKernelException')); } + /** + * Unregisters the dispatcher. + * + * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance + */ + public function unregister(EventDispatcherInterface $dispatcher) + { + $dispatcher->removeListener(KernelEvents::EXCEPTION, array($this, 'onKernelException')); + } + /** * Handles security related exceptions. * @@ -76,10 +86,6 @@ public function register(EventDispatcherInterface $dispatcher) */ public function onKernelException(GetResponseForExceptionEvent $event) { - // we need to remove ourselves as the exception listener can be - // different depending on the Request - $event->getDispatcher()->removeListener(KernelEvents::EXCEPTION, array($this, 'onKernelException')); - $exception = $event->getException(); do { if ($exception instanceof AuthenticationException) { diff --git a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php index 7dc9503bd91e1..ed7f7fe3ce656 100644 --- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php @@ -11,12 +11,16 @@ namespace Symfony\Component\Security\Http\Firewall; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Core\Exception\LogoutException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; @@ -33,19 +37,25 @@ class LogoutListener implements ListenerInterface private $handlers; private $successHandler; private $httpUtils; - private $csrfProvider; + private $csrfTokenManager; /** * Constructor. * * @param SecurityContextInterface $securityContext - * @param HttpUtils $httpUtils An HttpUtilsInterface instance - * @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance - * @param array $options An array of options to process a logout attempt - * @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance + * @param HttpUtils $httpUtils An HttpUtilsInterface instance + * @param LogoutSuccessHandlerInterface $successHandler A LogoutSuccessHandlerInterface instance + * @param array $options An array of options to process a logout attempt + * @param CsrfTokenManagerInterface $csrfTokenManager A CsrfTokenManagerInterface instance */ - public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, LogoutSuccessHandlerInterface $successHandler, array $options = array(), CsrfProviderInterface $csrfProvider = null) + public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, LogoutSuccessHandlerInterface $successHandler, array $options = array(), $csrfTokenManager = null) { + if ($csrfTokenManager instanceof CsrfProviderInterface) { + $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); + } elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) { + throw new InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.'); + } + $this->securityContext = $securityContext; $this->httpUtils = $httpUtils; $this->options = array_merge(array( @@ -54,7 +64,7 @@ public function __construct(SecurityContextInterface $securityContext, HttpUtils 'logout_path' => '/logout', ), $options); $this->successHandler = $successHandler; - $this->csrfProvider = $csrfProvider; + $this->csrfTokenManager = $csrfTokenManager; $this->handlers = array(); } @@ -71,7 +81,7 @@ public function addHandler(LogoutHandlerInterface $handler) /** * Performs the logout if requested * - * If a CsrfProviderInterface instance is available, it will be used to + * If a CsrfTokenManagerInterface instance is available, it will be used to * validate the request. * * @param GetResponseEvent $event A GetResponseEvent instance @@ -87,10 +97,10 @@ public function handle(GetResponseEvent $event) return; } - if (null !== $this->csrfProvider) { + if (null !== $this->csrfTokenManager) { $csrfToken = $request->get($this->options['csrf_parameter'], null, true); - if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) { + if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken))) { throw new LogoutException('Invalid CSRF token.'); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/SimpleFormAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/SimpleFormAuthenticationListener.php new file mode 100644 index 0000000000000..f79ce5c41202b --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/SimpleFormAuthenticationListener.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface; +use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; +use Psr\Log\LoggerInterface; + +/** + * @author Jordi Boggiano + */ +class SimpleFormAuthenticationListener extends AbstractAuthenticationListener +{ + private $simpleAuthenticator; + private $csrfTokenManager; + + /** + * Constructor. + * + * @param SecurityContextInterface $securityContext A SecurityContext instance + * @param AuthenticationManagerInterface $authenticationManager An AuthenticationManagerInterface instance + * @param SessionAuthenticationStrategyInterface $sessionStrategy + * @param HttpUtils $httpUtils An HttpUtilsInterface instance + * @param string $providerKey + * @param AuthenticationSuccessHandlerInterface $successHandler + * @param AuthenticationFailureHandlerInterface $failureHandler + * @param array $options An array of options for the processing of a + * successful, or failed authentication attempt + * @param LoggerInterface $logger A LoggerInterface instance + * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance + * @param SimpleFormAuthenticatorInterface $simpleAuthenticator A SimpleFormAuthenticatorInterface instance + * @param CsrfTokenManagerInterface $csrfTokenManager A CsrfTokenManagerInterface instance + */ + public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenManagerInterface $csrfTokenManager = null, SimpleFormAuthenticatorInterface $simpleAuthenticator = null) + { + if (!$simpleAuthenticator) { + throw new \InvalidArgumentException('Missing simple authenticator'); + } + + if ($csrfTokenManager instanceof CsrfProviderInterface) { + $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); + } elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) { + throw new InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.'); + } + + $this->simpleAuthenticator = $simpleAuthenticator; + $this->csrfTokenManager = $csrfTokenManager; + + $options = array_merge(array( + 'username_parameter' => '_username', + 'password_parameter' => '_password', + 'csrf_parameter' => '_csrf_token', + 'intention' => 'authenticate', + 'post_only' => true, + ), $options); + parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, $options, $logger, $dispatcher); + } + + /** + * {@inheritdoc} + */ + protected function requiresAuthentication(Request $request) + { + if ($this->options['post_only'] && !$request->isMethod('POST')) { + return false; + } + + return parent::requiresAuthentication($request); + } + + /** + * {@inheritdoc} + */ + protected function attemptAuthentication(Request $request) + { + if (null !== $this->csrfTokenManager) { + $csrfToken = $request->get($this->options['csrf_parameter'], null, true); + + if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken))) { + throw new InvalidCsrfTokenException('Invalid CSRF token.'); + } + } + + if ($this->options['post_only']) { + $username = trim($request->request->get($this->options['username_parameter'], null, true)); + $password = $request->request->get($this->options['password_parameter'], null, true); + } else { + $username = trim($request->get($this->options['username_parameter'], null, true)); + $password = $request->get($this->options['password_parameter'], null, true); + } + + $request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username); + + $token = $this->simpleAuthenticator->createToken($request, $username, $password, $this->providerKey); + + return $this->authenticationManager->authenticate($token); + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php new file mode 100644 index 0000000000000..258ca96d6d8ce --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; + +/** + * SimplePreAuthenticationListener implements simple proxying to an authenticator. + * + * @author Jordi Boggiano + */ +class SimplePreAuthenticationListener implements ListenerInterface +{ + private $securityContext; + private $authenticationManager; + private $providerKey; + private $simpleAuthenticator; + private $logger; + + /** + * Constructor. + * + * @param SecurityContextInterface $securityContext A SecurityContext instance + * @param AuthenticationManagerInterface $authenticationManager An AuthenticationManagerInterface instance + * @param string $providerKey + * @param SimplePreAuthenticatorInterface $simpleAuthenticator A SimplePreAuthenticatorInterface instance + * @param LoggerInterface $logger A LoggerInterface instance + */ + public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, $providerKey, SimplePreAuthenticatorInterface $simpleAuthenticator, LoggerInterface $logger = null) + { + if (empty($providerKey)) { + throw new \InvalidArgumentException('$providerKey must not be empty.'); + } + + $this->securityContext = $securityContext; + $this->authenticationManager = $authenticationManager; + $this->providerKey = $providerKey; + $this->simpleAuthenticator = $simpleAuthenticator; + $this->logger = $logger; + } + + /** + * Handles basic authentication. + * + * @param GetResponseEvent $event A GetResponseEvent instance + */ + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + + if (null !== $this->logger) { + $this->logger->info(sprintf('Attempting simple pre-authorization %s', $this->providerKey)); + } + + if (null !== $this->securityContext->getToken() && !$this->securityContext->getToken() instanceof AnonymousToken) { + return; + } + + try { + $token = $this->simpleAuthenticator->createToken($request, $this->providerKey); + $token = $this->authenticationManager->authenticate($token); + $this->securityContext->setToken($token); + } catch (AuthenticationException $e) { + $this->securityContext->setToken(null); + + if (null !== $this->logger) { + $this->logger->info(sprintf('Authentication request failed: %s', $e->getMessage())); + } + + if ($this->simpleAuthenticator instanceof AuthenticationFailureHandlerInterface) { + $response = $this->simpleAuthenticator->onAuthenticationFailure($request, $e); + if ($response instanceof Response) { + $event->setResponse($response); + } elseif (null !== $response) { + throw new \UnexpectedValueException(sprintf('The %s::onAuthenticationFailure method must return null or a Response object', get_class($this->simpleAuthenticator))); + } + } + + return; + } + + if ($this->simpleAuthenticator instanceof AuthenticationSuccessHandlerInterface) { + $response = $this->simpleAuthenticator->onAuthenticationSuccess($request, $token); + if ($response instanceof Response) { + $event->setResponse($response); + } elseif (null !== $response) { + throw new \UnexpectedValueException(sprintf('The %s::onAuthenticationSuccess method must return null or a Response object', get_class($this->simpleAuthenticator))); + } + } + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index 77000968e14f7..b46b1bc3cf729 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -92,7 +92,9 @@ public function handle(GetResponseEvent $event) } } - $request->server->set('QUERY_STRING', ''); + $request->query->remove($this->usernameParameter); + $request->server->set('QUERY_STRING', http_build_query($request->query->all())); + $response = new RedirectResponse($request->getUri(), 302); $event->setResponse($response); diff --git a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php index 81c2b374d07ce..f24d2163f1a7f 100644 --- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php @@ -11,15 +11,19 @@ namespace Symfony\Component\Security\Http\Firewall; +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter; use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\HttpFoundation\Request; use Psr\Log\LoggerInterface; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\InvalidArgumentException; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -32,13 +36,19 @@ */ class UsernamePasswordFormAuthenticationListener extends AbstractAuthenticationListener { - private $csrfProvider; + private $csrfTokenManager; /** * {@inheritdoc} */ - public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null) + public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, $csrfTokenManager = null) { + if ($csrfTokenManager instanceof CsrfProviderInterface) { + $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager); + } elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) { + throw new InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.'); + } + parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array( 'username_parameter' => '_username', 'password_parameter' => '_password', @@ -47,7 +57,7 @@ public function __construct(SecurityContextInterface $securityContext, Authentic 'post_only' => true, ), $options), $logger, $dispatcher); - $this->csrfProvider = $csrfProvider; + $this->csrfTokenManager = $csrfTokenManager; } /** @@ -67,10 +77,10 @@ protected function requiresAuthentication(Request $request) */ protected function attemptAuthentication(Request $request) { - if (null !== $this->csrfProvider) { + if (null !== $this->csrfTokenManager) { $csrfToken = $request->get($this->options['csrf_parameter'], null, true); - if (false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken)) { + if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken))) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); } } diff --git a/src/Symfony/Component/Security/Http/HttpUtils.php b/src/Symfony/Component/Security/Http/HttpUtils.php index 0c8b21b78075e..422c8ce9e9332 100644 --- a/src/Symfony/Component/Security/Http/HttpUtils.php +++ b/src/Symfony/Component/Security/Http/HttpUtils.php @@ -72,7 +72,7 @@ public function createRedirectResponse(Request $request, $path, $status = 302) */ public function createRequest(Request $request, $path) { - $newRequest = $request::create($this->generateUri($request, $path), 'get', array(), $request->cookies->all(), array(), $request->server->all()); + $newRequest = Request::create($this->generateUri($request, $path), 'get', array(), $request->cookies->all(), array(), $request->server->all()); if ($request->hasSession()) { $newRequest->setSession($request->getSession()); } diff --git a/src/Symfony/Component/Security/Http/LICENSE b/src/Symfony/Component/Security/Http/LICENSE new file mode 100644 index 0000000000000..0b3292cf90235 --- /dev/null +++ b/src/Symfony/Component/Security/Http/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2014 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Security/Http/README.md b/src/Symfony/Component/Security/Http/README.md new file mode 100644 index 0000000000000..c0760d4edae4a --- /dev/null +++ b/src/Symfony/Component/Security/Http/README.md @@ -0,0 +1,23 @@ +Security Component - HTTP Integration +===================================== + +Security provides an infrastructure for sophisticated authorization systems, +which makes it possible to easily separate the actual authorization logic from +so called user providers that hold the users credentials. It is inspired by +the Java Spring framework. + +Resources +--------- + +Documentation: + +http://symfony.com/doc/2.5/book/security.html + +Tests +----- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Security/Http/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php index df0ea1b5a007f..571abbeeecaea 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php @@ -147,6 +147,6 @@ protected function generateCookieValue($class, $username, $expires, $password) */ protected function generateCookieHash($class, $username, $expires, $password) { - return hash('sha256', $class.$username.$expires.$password.$this->getKey()); + return hash_hmac('sha256', $class.$username.$expires.$password, $this->getKey()); } } diff --git a/src/Symfony/Component/Security/Tests/Http/AccessMapTest.php b/src/Symfony/Component/Security/Http/Tests/AccessMapTest.php similarity index 86% rename from src/Symfony/Component/Security/Tests/Http/AccessMapTest.php rename to src/Symfony/Component/Security/Http/Tests/AccessMapTest.php index 653152a4e076d..d8ab7aae90d59 100644 --- a/src/Symfony/Component/Security/Tests/Http/AccessMapTest.php +++ b/src/Symfony/Component/Security/Http/Tests/AccessMapTest.php @@ -9,19 +9,12 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http; +namespace Symfony\Component\Security\Http\Tests; use Symfony\Component\Security\Http\AccessMap; class AccessMapTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testReturnsFirstMatchedPattern() { $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); diff --git a/src/Symfony/Component/Security/Tests/Http/Authentication/DefaultAuthenticationFailureHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php similarity index 92% rename from src/Symfony/Component/Security/Tests/Http/Authentication/DefaultAuthenticationFailureHandlerTest.php rename to src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php index e610b6b4a72cc..15adcdf357ec5 100644 --- a/src/Symfony/Component/Security/Tests/Http/Authentication/DefaultAuthenticationFailureHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Authentication; +namespace Symfony\Component\Security\Http\Tests\Authentication; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler; use Symfony\Component\Security\Core\SecurityContextInterface; @@ -31,18 +31,6 @@ class DefaultAuthenticationFailureHandlerTest extends \PHPUnit_Framework_TestCas protected function setUp() { - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!interface_exists('Psr\Log\LoggerInterface')) { - $this->markTestSkipped('The "LoggerInterface" is not available'); - } - $this->httpKernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); $this->httpUtils = $this->getMock('Symfony\Component\Security\Http\HttpUtils'); $this->logger = $this->getMock('Psr\Log\LoggerInterface'); diff --git a/src/Symfony/Component/Security/Tests/Http/Authentication/DefaultAuthenticationSuccessHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php similarity index 96% rename from src/Symfony/Component/Security/Tests/Http/Authentication/DefaultAuthenticationSuccessHandlerTest.php rename to src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php index e6bc6ca51247a..b77558847fbd9 100644 --- a/src/Symfony/Component/Security/Tests/Http/Authentication/DefaultAuthenticationSuccessHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Authentication; +namespace Symfony\Component\Security\Http\Tests\Authentication; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; @@ -23,10 +23,6 @@ class DefaultAuthenticationSuccessHandlerTest extends \PHPUnit_Framework_TestCas protected function setUp() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $this->httpUtils = $this->getMock('Symfony\Component\Security\Http\HttpUtils'); $this->request = $this->getMock('Symfony\Component\HttpFoundation\Request'); $this->request->headers = $this->getMock('Symfony\Component\HttpFoundation\HeaderBag'); diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/SimpleAuthenticationHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/SimpleAuthenticationHandlerTest.php new file mode 100644 index 0000000000000..507addc817acd --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/SimpleAuthenticationHandlerTest.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests; + +use Symfony\Component\Security\Core\Authentication\SimpleAuthenticatorInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Authentication\SimpleAuthenticationHandler; + +class SimpleAuthenticationHandlerTest extends \PHPUnit_Framework_TestCase +{ + private $successHandler; + + private $failureHandler; + + private $request; + + private $token; + + private $authenticationException; + + private $response; + + public function setUp() + { + $this->successHandler = $this->getMock('Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface'); + $this->failureHandler = $this->getMock('Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface'); + + $this->request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $this->token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $this->authenticationException = $this->getMock('Symfony\Component\Security\Core\Exception\AuthenticationException'); + + $this->response = $this->getMock('Symfony\Component\HttpFoundation\Response'); + } + + public function testOnAuthenticationSuccessFallsBackToDefaultHandlerIfSimpleIsNotASuccessHandler() + { + $authenticator = $this->getMock('Symfony\Component\Security\Core\Authentication\SimpleAuthenticatorInterface'); + + $this->successHandler->expects($this->once()) + ->method('onAuthenticationSuccess') + ->with($this->request, $this->token) + ->will($this->returnValue($this->response)); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $result = $handler->onAuthenticationSuccess($this->request, $this->token); + + $this->assertSame($this->response, $result); + } + + public function testOnAuthenticationSuccessCallsSimpleAuthenticator() + { + $this->successHandler->expects($this->never()) + ->method('onAuthenticationSuccess'); + + $authenticator = $this->getMockForAbstractClass('Symfony\Component\Security\Http\Tests\TestSuccessHandlerInterface'); + $authenticator->expects($this->once()) + ->method('onAuthenticationSuccess') + ->with($this->request, $this->token) + ->will($this->returnValue($this->response)); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $result = $handler->onAuthenticationSuccess($this->request, $this->token); + + $this->assertSame($this->response, $result); + } + + /** + * @expectedException \UnexpectedValueException + * @expectedExceptionMessage onAuthenticationSuccess method must return null to use the default success handler, or a Response object + */ + public function testOnAuthenticationSuccessThrowsAnExceptionIfNonResponseIsReturned() + { + $this->successHandler->expects($this->never()) + ->method('onAuthenticationSuccess'); + + $authenticator = $this->getMockForAbstractClass('Symfony\Component\Security\Http\Tests\TestSuccessHandlerInterface'); + $authenticator->expects($this->once()) + ->method('onAuthenticationSuccess') + ->with($this->request, $this->token) + ->will($this->returnValue(new \stdClass())); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $handler->onAuthenticationSuccess($this->request, $this->token); + } + + public function testOnAuthenticationSuccessFallsBackToDefaultHandlerIfNullIsReturned() + { + $this->successHandler->expects($this->once()) + ->method('onAuthenticationSuccess') + ->with($this->request, $this->token) + ->will($this->returnValue($this->response)); + + $authenticator = $this->getMockForAbstractClass('Symfony\Component\Security\Http\Tests\TestSuccessHandlerInterface'); + $authenticator->expects($this->once()) + ->method('onAuthenticationSuccess') + ->with($this->request, $this->token) + ->will($this->returnValue(null)); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $result = $handler->onAuthenticationSuccess($this->request, $this->token); + + $this->assertSame($this->response, $result); + } + + public function testOnAuthenticationFailureFallsBackToDefaultHandlerIfSimpleIsNotAFailureHandler() + { + $authenticator = $this->getMock('Symfony\Component\Security\Core\Authentication\SimpleAuthenticatorInterface'); + + $this->failureHandler->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->authenticationException) + ->will($this->returnValue($this->response)); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $result = $handler->onAuthenticationFailure($this->request, $this->authenticationException); + + $this->assertSame($this->response, $result); + } + + public function testOnAuthenticationFailureCallsSimpleAuthenticator() + { + $this->failureHandler->expects($this->never()) + ->method('onAuthenticationFailure'); + + $authenticator = $this->getMockForAbstractClass('Symfony\Component\Security\Http\Tests\TestFailureHandlerInterface'); + $authenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->authenticationException) + ->will($this->returnValue($this->response)); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $result = $handler->onAuthenticationFailure($this->request, $this->authenticationException); + + $this->assertSame($this->response, $result); + } + + /** + * @expectedException \UnexpectedValueException + * @expectedExceptionMessage onAuthenticationFailure method must return null to use the default failure handler, or a Response object + */ + public function testOnAuthenticationFailureThrowsAnExceptionIfNonResponseIsReturned() + { + $this->failureHandler->expects($this->never()) + ->method('onAuthenticationFailure'); + + $authenticator = $this->getMockForAbstractClass('Symfony\Component\Security\Http\Tests\TestFailureHandlerInterface'); + $authenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->authenticationException) + ->will($this->returnValue(new \stdClass())); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $handler->onAuthenticationFailure($this->request, $this->authenticationException); + } + + public function testOnAuthenticationFailureFallsBackToDefaultHandlerIfNullIsReturned() + { + $this->failureHandler->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->authenticationException) + ->will($this->returnValue($this->response)); + + $authenticator = $this->getMockForAbstractClass('Symfony\Component\Security\Http\Tests\TestFailureHandlerInterface'); + $authenticator->expects($this->once()) + ->method('onAuthenticationFailure') + ->with($this->request, $this->authenticationException) + ->will($this->returnValue(null)); + + $handler = new SimpleAuthenticationHandler($authenticator, $this->successHandler, $this->failureHandler); + $result = $handler->onAuthenticationFailure($this->request, $this->authenticationException); + + $this->assertSame($this->response, $result); + } +} + +interface TestSuccessHandlerInterface extends AuthenticationSuccessHandlerInterface, SimpleAuthenticatorInterface +{ +} + +interface TestFailureHandlerInterface extends AuthenticationFailureHandlerInterface, SimpleAuthenticatorInterface +{ +} diff --git a/src/Symfony/Component/Security/Tests/Http/EntryPoint/BasicAuthenticationEntryPointTest.php b/src/Symfony/Component/Security/Http/Tests/EntryPoint/BasicAuthenticationEntryPointTest.php similarity index 83% rename from src/Symfony/Component/Security/Tests/Http/EntryPoint/BasicAuthenticationEntryPointTest.php rename to src/Symfony/Component/Security/Http/Tests/EntryPoint/BasicAuthenticationEntryPointTest.php index b9e289d718ee6..ca5922c8df0f1 100644 --- a/src/Symfony/Component/Security/Tests/Http/EntryPoint/BasicAuthenticationEntryPointTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EntryPoint/BasicAuthenticationEntryPointTest.php @@ -9,20 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\EntryPoint; +namespace Symfony\Component\Security\Http\Tests\EntryPoint; use Symfony\Component\Security\Http\EntryPoint\BasicAuthenticationEntryPoint; use Symfony\Component\Security\Core\Exception\AuthenticationException; class BasicAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testStart() { $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); diff --git a/src/Symfony/Component/Security/Tests/Http/EntryPoint/DigestAuthenticationEntryPointTest.php b/src/Symfony/Component/Security/Http/Tests/EntryPoint/DigestAuthenticationEntryPointTest.php similarity index 89% rename from src/Symfony/Component/Security/Tests/Http/EntryPoint/DigestAuthenticationEntryPointTest.php rename to src/Symfony/Component/Security/Http/Tests/EntryPoint/DigestAuthenticationEntryPointTest.php index 8dfd6183eea7c..181e340e60a31 100644 --- a/src/Symfony/Component/Security/Tests/Http/EntryPoint/DigestAuthenticationEntryPointTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EntryPoint/DigestAuthenticationEntryPointTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\EntryPoint; +namespace Symfony\Component\Security\Http\Tests\EntryPoint; use Symfony\Component\Security\Http\EntryPoint\DigestAuthenticationEntryPoint; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -17,13 +17,6 @@ class DigestAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testStart() { $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); diff --git a/src/Symfony/Component/Security/Tests/Http/EntryPoint/FormAuthenticationEntryPointTest.php b/src/Symfony/Component/Security/Http/Tests/EntryPoint/FormAuthenticationEntryPointTest.php similarity index 85% rename from src/Symfony/Component/Security/Tests/Http/EntryPoint/FormAuthenticationEntryPointTest.php rename to src/Symfony/Component/Security/Http/Tests/EntryPoint/FormAuthenticationEntryPointTest.php index cbec1bdd63154..3acb9c2616675 100644 --- a/src/Symfony/Component/Security/Tests/Http/EntryPoint/FormAuthenticationEntryPointTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EntryPoint/FormAuthenticationEntryPointTest.php @@ -9,24 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\EntryPoint; +namespace Symfony\Component\Security\Http\Tests\EntryPoint; use Symfony\Component\Security\Http\EntryPoint\FormAuthenticationEntryPoint; use Symfony\Component\HttpKernel\HttpKernelInterface; class FormAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testStart() { $request = $this->getMock('Symfony\Component\HttpFoundation\Request', array(), array(), '', false, false); diff --git a/src/Symfony/Component/Security/Tests/Http/EntryPoint/RetryAuthenticationEntryPointTest.php b/src/Symfony/Component/Security/Http/Tests/EntryPoint/RetryAuthenticationEntryPointTest.php similarity index 87% rename from src/Symfony/Component/Security/Tests/Http/EntryPoint/RetryAuthenticationEntryPointTest.php rename to src/Symfony/Component/Security/Http/Tests/EntryPoint/RetryAuthenticationEntryPointTest.php index 91de1ca7fd4d5..f4e542593398e 100644 --- a/src/Symfony/Component/Security/Tests/Http/EntryPoint/RetryAuthenticationEntryPointTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EntryPoint/RetryAuthenticationEntryPointTest.php @@ -9,20 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\EntryPoint; +namespace Symfony\Component\Security\Http\Tests\EntryPoint; use Symfony\Component\Security\Http\EntryPoint\RetryAuthenticationEntryPoint; use Symfony\Component\HttpFoundation\Request; class RetryAuthenticationEntryPointTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - /** * @dataProvider dataForStart */ diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/AbstractPreAuthenticatedListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AbstractPreAuthenticatedListenerTest.php similarity index 93% rename from src/Symfony/Component/Security/Tests/Http/Firewall/AbstractPreAuthenticatedListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/AbstractPreAuthenticatedListenerTest.php index 76721ec9550f5..57a91cf67cdc2 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/AbstractPreAuthenticatedListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AbstractPreAuthenticatedListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; @@ -18,21 +18,6 @@ class AbstractPreAuthenticatedListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testHandleWithValidValues() { $userCredentials = array('TheUser', 'TheCredentials'); diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php similarity index 92% rename from src/Symfony/Component/Security/Tests/Http/Firewall/AccessListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php index 53ab3507a5081..f9b0f3c4a1796 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/AccessListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php @@ -9,27 +9,12 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\Security\Http\Firewall\AccessListener; class AccessListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - /** * @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException */ diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/AnonymousAuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php similarity index 90% rename from src/Symfony/Component/Security/Tests/Http/Firewall/AnonymousAuthenticationListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php index 73ee8214de0cf..1fb7350c6cbae 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/AnonymousAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php @@ -9,19 +9,12 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener; class AnonymousAuthenticationListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - } - public function testHandleWithContextHavingAToken() { $context = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/BasicAuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/BasicAuthenticationListenerTest.php similarity index 93% rename from src/Symfony/Component/Security/Tests/Http/Firewall/BasicAuthenticationListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/BasicAuthenticationListenerTest.php index 7616149015695..eb51f5f38d6c5 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/BasicAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/BasicAuthenticationListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; @@ -20,21 +20,6 @@ class BasicAuthenticationListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testHandleWithValidUsernameAndPasswordServerParameters() { $request = new Request(array(), array(), array(), array(), array(), array( diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/ChannelListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php similarity index 90% rename from src/Symfony/Component/Security/Tests/Http/Firewall/ChannelListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php index 17bf0a087d971..2b27e7558c81c 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/ChannelListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\Security\Http\Firewall\ChannelListener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -17,21 +17,6 @@ class ChannelListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testHandleWithNotSecuredRequestAndHttpChannel() { $request = $this->getMock('Symfony\Component\HttpFoundation\Request', array(), array(), '', false, false); diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/ContextListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php similarity index 92% rename from src/Symfony/Component/Security/Tests/Http/Firewall/ContextListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php index 336c3334fe9b7..c153fd5d886c0 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/ContextListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -26,18 +26,6 @@ class ContextListenerTest extends \PHPUnit_Framework_TestCase { protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - $this->securityContext = new SecurityContext( $this->getMock('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface'), $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface') @@ -201,8 +189,8 @@ public function testHandleAddsKernelResponseListener() $listener = new ContextListener($context, array(), 'key123', null, $dispatcher); $event->expects($this->any()) - ->method('getRequestType') - ->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST)); + ->method('isMasterRequest') + ->will($this->returnValue(true)); $event->expects($this->any()) ->method('getRequest') ->will($this->returnValue($this->getMock('Symfony\Component\HttpFoundation\Request'))); diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/DigestDataTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/DigestDataTest.php similarity index 99% rename from src/Symfony/Component/Security/Tests/Http/Firewall/DigestDataTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/DigestDataTest.php index 8b63d9c953f14..86a5327c5082b 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/DigestDataTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/DigestDataTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\Security\Http\Firewall\DigestData; diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/LogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php similarity index 80% rename from src/Symfony/Component/Security/Tests/Http/Firewall/LogoutListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php index 456b281d12c9a..29ce114432a92 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/LogoutListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -17,25 +17,6 @@ class LogoutListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Form\Form')) { - $this->markTestSkipped('The "Form" component is not available'); - } - - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testHandleUnmatchedPath() { list($listener, $context, $httpUtils, $options) = $this->getListener(); @@ -56,22 +37,21 @@ public function testHandleUnmatchedPath() public function testHandleMatchedPathWithSuccessHandlerAndCsrfValidation() { $successHandler = $this->getSuccessHandler(); - $csrfProvider = $this->getCsrfProvider(); + $tokenManager = $this->getTokenManager(); - list($listener, $context, $httpUtils, $options) = $this->getListener($successHandler, $csrfProvider); + list($listener, $context, $httpUtils, $options) = $this->getListener($successHandler, $tokenManager); list($event, $request) = $this->getGetResponseEvent(); - $request->query->set('_csrf_token', $csrfToken = 'token'); + $request->query->set('_csrf_token', 'token'); $httpUtils->expects($this->once()) ->method('checkRequestPath') ->with($request, $options['logout_path']) ->will($this->returnValue(true)); - $csrfProvider->expects($this->once()) - ->method('isCsrfTokenValid') - ->with('logout', $csrfToken) + $tokenManager->expects($this->once()) + ->method('isTokenValid') ->will($this->returnValue(true)); $successHandler->expects($this->once()) @@ -170,30 +150,29 @@ public function testSuccessHandlerReturnsNonResponse() */ public function testCsrfValidationFails() { - $csrfProvider = $this->getCsrfProvider(); + $tokenManager = $this->getTokenManager(); - list($listener, $context, $httpUtils, $options) = $this->getListener(null, $csrfProvider); + list($listener, $context, $httpUtils, $options) = $this->getListener(null, $tokenManager); list($event, $request) = $this->getGetResponseEvent(); - $request->query->set('_csrf_token', $csrfToken = 'token'); + $request->query->set('_csrf_token', 'token'); $httpUtils->expects($this->once()) ->method('checkRequestPath') ->with($request, $options['logout_path']) ->will($this->returnValue(true)); - $csrfProvider->expects($this->once()) - ->method('isCsrfTokenValid') - ->with('logout', $csrfToken) + $tokenManager->expects($this->once()) + ->method('isTokenValid') ->will($this->returnValue(false)); $listener->handle($event); } - private function getCsrfProvider() + private function getTokenManager() { - return $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'); + return $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface'); } private function getContext() @@ -228,7 +207,7 @@ private function getHttpUtils() ->getMock(); } - private function getListener($successHandler = null, $csrfProvider = null) + private function getListener($successHandler = null, $tokenManager = null) { $listener = new LogoutListener( $context = $this->getContext(), @@ -240,7 +219,7 @@ private function getListener($successHandler = null, $csrfProvider = null) 'logout_path' => '/logout', 'target_url' => '/', ), - $csrfProvider + $tokenManager ); return array($listener, $context, $httpUtils, $options); diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php similarity index 89% rename from src/Symfony/Component/Security/Tests/Http/Firewall/RememberMeListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php index 8ad4c55a940fa..950669282ce05 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/RememberMeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Firewall\RememberMeListener; @@ -17,21 +17,6 @@ class RememberMeListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testOnCoreSecurityDoesNotTryToPopulateNonEmptySecurityContext() { list($listener, $context, $service,,) = $this->getListener(); diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php similarity index 74% rename from src/Symfony/Component/Security/Tests/Http/Firewall/SwitchUserListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index f8bb9f60aa841..110e05cf2845a 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\Security\Http\Firewall\SwitchUserListener; @@ -29,19 +29,12 @@ class SwitchUserListenerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - $this->securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); $this->userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface'); $this->userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); $this->accessDecisionManager = $this->getMock('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'); $this->request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $this->request->query = $this->getMock('Symfony\Component\HttpFoundation\ParameterBag'); $this->request->server = $this->getMock('Symfony\Component\HttpFoundation\ServerBag'); $this->event = $this->getEvent($this->request); } @@ -59,7 +52,7 @@ public function testEventIsIgnoredIfUsernameIsNotPassedWithTheRequest() { $this->request->expects($this->any())->method('get')->with('_switch_user')->will($this->returnValue(null)); - $this->event->expects($this->never())->method('setResopnse'); + $this->event->expects($this->never())->method('setResponse'); $this->securityContext->expects($this->never())->method('setToken'); $listener = new SwitchUserListener($this->securityContext, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); @@ -94,6 +87,8 @@ public function testExitUserUpdatesToken() $this->request->expects($this->any())->method('get')->with('_switch_user')->will($this->returnValue('_exit')); $this->request->expects($this->any())->method('getUri')->will($this->returnValue('/')); + $this->request->query->expects($this->once())->method('remove', '_switch_user'); + $this->request->query->expects($this->any())->method('all')->will($this->returnValue(array())); $this->request->server->expects($this->once())->method('set')->with('QUERY_STRING', ''); $this->securityContext->expects($this->once()) @@ -108,7 +103,7 @@ public function testExitUserUpdatesToken() /** * @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException */ - public function testSwitchUserIsDissallowed() + public function testSwitchUserIsDisallowed() { $token = $this->getToken(array($this->getMock('Symfony\Component\Security\Core\Role\RoleInterface'))); @@ -131,6 +126,9 @@ public function testSwitchUser() $this->securityContext->expects($this->any())->method('getToken')->will($this->returnValue($token)); $this->request->expects($this->any())->method('get')->with('_switch_user')->will($this->returnValue('kuba')); + $this->request->query->expects($this->once())->method('remove', '_switch_user'); + $this->request->query->expects($this->any())->method('all')->will($this->returnValue(array())); + $this->request->expects($this->any())->method('getUri')->will($this->returnValue('/')); $this->request->server->expects($this->once())->method('set')->with('QUERY_STRING', ''); @@ -150,6 +148,35 @@ public function testSwitchUser() $listener->handle($this->event); } + public function testSwitchUserKeepsOtherQueryStringParameters() + { + $token = $this->getToken(array($this->getMock('Symfony\Component\Security\Core\Role\RoleInterface'))); + $user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); + $user->expects($this->any())->method('getRoles')->will($this->returnValue(array())); + + $this->securityContext->expects($this->any())->method('getToken')->will($this->returnValue($token)); + $this->request->expects($this->any())->method('get')->with('_switch_user')->will($this->returnValue('kuba')); + $this->request->query->expects($this->once())->method('remove', '_switch_user'); + $this->request->query->expects($this->any())->method('all')->will($this->returnValue(array('page'=>3,'section'=>2))); + $this->request->expects($this->any())->method('getUri')->will($this->returnValue('/')); + $this->request->server->expects($this->once())->method('set')->with('QUERY_STRING', 'page=3§ion=2'); + + $this->accessDecisionManager->expects($this->once()) + ->method('decide')->with($token, array('ROLE_ALLOWED_TO_SWITCH')) + ->will($this->returnValue(true)); + + $this->userProvider->expects($this->once()) + ->method('loadUserByUsername')->with('kuba') + ->will($this->returnValue($user)); + $this->userChecker->expects($this->once()) + ->method('checkPostAuth')->with($user); + $this->securityContext->expects($this->once()) + ->method('setToken')->with($this->isInstanceOf('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken')); + + $listener = new SwitchUserListener($this->securityContext, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + $listener->handle($this->event); + } + private function getEvent($request) { $event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent') diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/X509AuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/X509AuthenticationListenerTest.php similarity index 92% rename from src/Symfony/Component/Security/Tests/Http/Firewall/X509AuthenticationListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/Firewall/X509AuthenticationListenerTest.php index c48aeac0a219c..7725f4beac2d4 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/X509AuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/X509AuthenticationListenerTest.php @@ -9,20 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Firewall; +namespace Symfony\Component\Security\Http\Tests\Firewall; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Http\Firewall\X509AuthenticationListener; class X509AuthenticationListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - /** * @dataProvider dataProviderGetPreAuthenticatedData */ diff --git a/src/Symfony/Component/Security/Tests/Http/FirewallMapTest.php b/src/Symfony/Component/Security/Http/Tests/FirewallMapTest.php similarity index 90% rename from src/Symfony/Component/Security/Tests/Http/FirewallMapTest.php rename to src/Symfony/Component/Security/Http/Tests/FirewallMapTest.php index c7f13e18f0439..85a57ab82cebd 100644 --- a/src/Symfony/Component/Security/Tests/Http/FirewallMapTest.php +++ b/src/Symfony/Component/Security/Http/Tests/FirewallMapTest.php @@ -9,24 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http; +namespace Symfony\Component\Security\Http\Tests; use Symfony\Component\Security\Http\FirewallMap; use Symfony\Component\HttpFoundation\Request; class FirewallMapTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testGetListeners() { $map = new FirewallMap(); diff --git a/src/Symfony/Component/Security/Tests/Http/FirewallTest.php b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php similarity index 86% rename from src/Symfony/Component/Security/Tests/Http/FirewallTest.php rename to src/Symfony/Component/Security/Http/Tests/FirewallTest.php index 0c1d82c1804f2..67f4f150fd7a1 100644 --- a/src/Symfony/Component/Security/Tests/Http/FirewallTest.php +++ b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http; +namespace Symfony\Component\Security\Http\Tests; use Symfony\Component\Security\Http\Firewall; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -17,21 +17,6 @@ class FirewallTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) { - $this->markTestSkipped('The "HttpKernel" component is not available'); - } - } - public function testOnKernelRequestRegistersExceptionListener() { $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); diff --git a/src/Symfony/Component/Security/Tests/Http/HttpUtilsTest.php b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php similarity index 95% rename from src/Symfony/Component/Security/Tests/Http/HttpUtilsTest.php rename to src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php index db1aa4b94e6ad..90380eae9a314 100644 --- a/src/Symfony/Component/Security/Tests/Http/HttpUtilsTest.php +++ b/src/Symfony/Component/Security/Http/Tests/HttpUtilsTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http; +namespace Symfony\Component\Security\Http\Tests; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\MethodNotAllowedException; @@ -19,17 +19,6 @@ class HttpUtilsTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - - if (!class_exists('Symfony\Component\Routing\Router')) { - $this->markTestSkipped('The "Routing" component is not available'); - } - } - public function testCreateRedirectResponseWithPath() { $utils = new HttpUtils($this->getUrlGenerator()); diff --git a/src/Symfony/Component/Security/Tests/Http/Logout/CookieClearingLogoutHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/CookieClearingLogoutHandlerTest.php similarity index 86% rename from src/Symfony/Component/Security/Tests/Http/Logout/CookieClearingLogoutHandlerTest.php rename to src/Symfony/Component/Security/Http/Tests/Logout/CookieClearingLogoutHandlerTest.php index b32a81322f8d7..84745040c8aef 100644 --- a/src/Symfony/Component/Security/Tests/Http/Logout/CookieClearingLogoutHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Logout/CookieClearingLogoutHandlerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Logout; +namespace Symfony\Component\Security\Http\Tests\Logout; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; @@ -18,13 +18,6 @@ class CookieClearingLogoutHandlerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testLogout() { $request = new Request(); diff --git a/src/Symfony/Component/Security/Tests/Http/Logout/DefaultLogoutSuccessHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/DefaultLogoutSuccessHandlerTest.php similarity index 80% rename from src/Symfony/Component/Security/Tests/Http/Logout/DefaultLogoutSuccessHandlerTest.php rename to src/Symfony/Component/Security/Http/Tests/Logout/DefaultLogoutSuccessHandlerTest.php index e1b1227bf0b80..76a8cd99c3494 100644 --- a/src/Symfony/Component/Security/Tests/Http/Logout/DefaultLogoutSuccessHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Logout/DefaultLogoutSuccessHandlerTest.php @@ -9,20 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Logout; +namespace Symfony\Component\Security\Http\Tests\Logout; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Logout\DefaultLogoutSuccessHandler; class DefaultLogoutSuccessHandlerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testLogout() { $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); diff --git a/src/Symfony/Component/Security/Tests/Http/Logout/SessionLogoutHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/SessionLogoutHandlerTest.php similarity index 80% rename from src/Symfony/Component/Security/Tests/Http/Logout/SessionLogoutHandlerTest.php rename to src/Symfony/Component/Security/Http/Tests/Logout/SessionLogoutHandlerTest.php index 8e2dd28488584..c3429951fc893 100644 --- a/src/Symfony/Component/Security/Tests/Http/Logout/SessionLogoutHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Logout/SessionLogoutHandlerTest.php @@ -9,20 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Logout; +namespace Symfony\Component\Security\Http\Tests\Logout; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Logout\SessionLogoutHandler; class SessionLogoutHandlerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testLogout() { $handler = new SessionLogoutHandler(); diff --git a/src/Symfony/Component/Security/Tests/Http/RememberMe/AbstractRememberMeServicesTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/AbstractRememberMeServicesTest.php similarity index 93% rename from src/Symfony/Component/Security/Tests/Http/RememberMe/AbstractRememberMeServicesTest.php rename to src/Symfony/Component/Security/Http/Tests/RememberMe/AbstractRememberMeServicesTest.php index 857168643c273..927b77115b7b4 100644 --- a/src/Symfony/Component/Security/Tests/Http/RememberMe/AbstractRememberMeServicesTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/AbstractRememberMeServicesTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\RememberMe; +namespace Symfony\Component\Security\Http\Tests\RememberMe; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; use Symfony\Component\HttpFoundation\Request; @@ -17,13 +17,6 @@ class AbstractRememberMeServicesTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testGetRememberMeParameter() { $service = $this->getService(null, array('remember_me_parameter' => 'foo')); @@ -50,7 +43,7 @@ public function testAutoLoginReturnsNullWhenNoCookie() public function testAutoLoginThrowsExceptionWhenImplementationDoesNotReturnUserInterface() { $service = $this->getService(null, array('name' => 'foo', 'path' => null, 'domain' => null)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', 'foo'); $service @@ -113,8 +106,8 @@ public function testLoginFail() public function testLoginSuccessIsNotProcessedWhenTokenDoesNotContainUserInterfaceImplementation() { $service = $this->getService(null, array('name' => 'foo', 'always_remember_me' => true, 'path' => null, 'domain' => null)); - $request = new Request; - $response = new Response; + $request = new Request(); + $response = new Response(); $account = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token @@ -136,8 +129,8 @@ public function testLoginSuccessIsNotProcessedWhenTokenDoesNotContainUserInterfa public function testLoginSuccessIsNotProcessedWhenRememberMeIsNotRequested() { $service = $this->getService(null, array('name' => 'foo', 'always_remember_me' => false, 'remember_me_parameter' => 'foo', 'path' => null, 'domain' => null)); - $request = new Request; - $response = new Response; + $request = new Request(); + $response = new Response(); $account = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token @@ -160,8 +153,8 @@ public function testLoginSuccessIsNotProcessedWhenRememberMeIsNotRequested() public function testLoginSuccessWhenRememberMeAlwaysIsTrue() { $service = $this->getService(null, array('name' => 'foo', 'always_remember_me' => true, 'path' => null, 'domain' => null)); - $request = new Request; - $response = new Response; + $request = new Request(); + $response = new Response(); $account = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token @@ -186,9 +179,9 @@ public function testLoginSuccessWhenRememberMeParameterWithPathIsPositive($value { $service = $this->getService(null, array('name' => 'foo', 'always_remember_me' => false, 'remember_me_parameter' => 'foo[bar]', 'path' => null, 'domain' => null)); - $request = new Request; + $request = new Request(); $request->request->set('foo', array('bar' => $value)); - $response = new Response; + $response = new Response(); $account = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token @@ -213,9 +206,9 @@ public function testLoginSuccessWhenRememberMeParameterIsPositive($value) { $service = $this->getService(null, array('name' => 'foo', 'always_remember_me' => false, 'remember_me_parameter' => 'foo', 'path' => null, 'domain' => null)); - $request = new Request; + $request = new Request(); $request->request->set('foo', $value); - $response = new Response; + $response = new Response(); $account = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token diff --git a/src/Symfony/Component/Security/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentTokenBasedRememberMeServicesTest.php similarity index 95% rename from src/Symfony/Component/Security/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php rename to src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentTokenBasedRememberMeServicesTest.php index 7fc3021c8f176..098f5b6e61ed7 100644 --- a/src/Symfony/Component/Security/Tests/Http/RememberMe/PersistentTokenBasedRememberMeServicesTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentTokenBasedRememberMeServicesTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\RememberMe; +namespace Symfony\Component\Security\Http\Tests\RememberMe; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -26,13 +26,6 @@ class PersistentTokenBasedRememberMeServicesTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testAutoLoginReturnsNullWhenNoCookie() { $service = $this->getService(null, array('name' => 'foo')); @@ -43,7 +36,7 @@ public function testAutoLoginReturnsNullWhenNoCookie() public function testAutoLoginThrowsExceptionOnInvalidCookie() { $service = $this->getService(null, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => false, 'remember_me_parameter' => 'foo')); - $request = new Request; + $request = new Request(); $request->request->set('foo', 'true'); $request->cookies->set('foo', 'foo'); @@ -54,7 +47,7 @@ public function testAutoLoginThrowsExceptionOnInvalidCookie() public function testAutoLoginThrowsExceptionOnNonExistentToken() { $service = $this->getService(null, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => false, 'remember_me_parameter' => 'foo')); - $request = new Request; + $request = new Request(); $request->request->set('foo', 'true'); $request->cookies->set('foo', $this->encodeCookie(array( $series = 'fooseries', @@ -77,7 +70,7 @@ public function testAutoLoginReturnsNullOnNonExistentUser() { $userProvider = $this->getProvider(); $service = $this->getService($userProvider, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => true, 'lifetime' => 3600, 'secure' => false, 'httponly' => false)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', $this->encodeCookie(array('fooseries', 'foovalue'))); $tokenProvider = $this->getMock('Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface'); @@ -102,7 +95,7 @@ public function testAutoLoginThrowsExceptionOnStolenCookieAndRemovesItFromThePer { $userProvider = $this->getProvider(); $service = $this->getService($userProvider, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => true)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', $this->encodeCookie(array('fooseries', 'foovalue'))); $tokenProvider = $this->getMock('Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface'); @@ -132,7 +125,7 @@ public function testAutoLoginThrowsExceptionOnStolenCookieAndRemovesItFromThePer public function testAutoLoginDoesNotAcceptAnExpiredCookie() { $service = $this->getService(null, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => true, 'lifetime' => 3600)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', $this->encodeCookie(array('fooseries', 'foovalue'))); $tokenProvider = $this->getMock('Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface'); @@ -166,7 +159,7 @@ public function testAutoLogin() ; $service = $this->getService($userProvider, array('name' => 'foo', 'path' => null, 'domain' => null, 'secure' => false, 'httponly' => false, 'always_remember_me' => true, 'lifetime' => 3600)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', $this->encodeCookie(array('fooseries', 'foovalue'))); $tokenProvider = $this->getMock('Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface'); @@ -214,8 +207,8 @@ public function testLogout() public function testLogoutSimplyIgnoresNonSetRequestCookie() { $service = $this->getService(null, array('name' => 'foo', 'path' => null, 'domain' => null)); - $request = new Request; - $response = new Response; + $request = new Request(); + $response = new Response(); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $tokenProvider = $this->getMock('Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface'); @@ -236,9 +229,9 @@ public function testLogoutSimplyIgnoresNonSetRequestCookie() public function testLogoutSimplyIgnoresInvalidCookie() { $service = $this->getService(null, array('name' => 'foo', 'path' => null, 'domain' => null)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', 'somefoovalue'); - $response = new Response; + $response = new Response(); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $tokenProvider = $this->getMock('Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface'); @@ -266,8 +259,8 @@ public function testLoginFail() public function testLoginSuccessSetsCookieWhenLoggedInWithNonRememberMeTokenInterfaceImplementation() { $service = $this->getService(null, array('name' => 'foo', 'domain' => 'myfoodomain.foo', 'path' => '/foo/path', 'secure' => true, 'httponly' => true, 'lifetime' => 3600, 'always_remember_me' => true)); - $request = new Request; - $response = new Response; + $request = new Request(); + $response = new Response(); $account = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); $account diff --git a/src/Symfony/Component/Security/Tests/Http/RememberMe/ResponseListenerTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/ResponseListenerTest.php similarity index 88% rename from src/Symfony/Component/Security/Tests/Http/RememberMe/ResponseListenerTest.php rename to src/Symfony/Component/Security/Http/Tests/RememberMe/ResponseListenerTest.php index cbd3f1f9af79b..5d69a65b1f847 100644 --- a/src/Symfony/Component/Security/Tests/Http/RememberMe/ResponseListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/ResponseListenerTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\RememberMe; +namespace Symfony\Component\Security\Http\Tests\RememberMe; use Symfony\Component\Security\Http\RememberMe\ResponseListener; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -19,13 +19,6 @@ class ResponseListenerTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testRememberMeCookieIsSentWithResponse() { $cookie = new Cookie('rememberme'); @@ -41,7 +34,7 @@ public function testRememberMeCookieIsSentWithResponse() $listener->onKernelResponse($this->getEvent($request, $response)); } - public function testRemmeberMeCookieIsNotSendWithResponse() + public function testRememberMeCookieIsNotSendWithResponse() { $request = $this->getRequest(); diff --git a/src/Symfony/Component/Security/Tests/Http/RememberMe/TokenBasedRememberMeServicesTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/TokenBasedRememberMeServicesTest.php similarity index 95% rename from src/Symfony/Component/Security/Tests/Http/RememberMe/TokenBasedRememberMeServicesTest.php rename to src/Symfony/Component/Security/Http/Tests/RememberMe/TokenBasedRememberMeServicesTest.php index 4699257fa3b6e..95c942e006813 100644 --- a/src/Symfony/Component/Security/Tests/Http/RememberMe/TokenBasedRememberMeServicesTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/TokenBasedRememberMeServicesTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\RememberMe; +namespace Symfony\Component\Security\Http\Tests\RememberMe; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -22,13 +22,6 @@ class TokenBasedRememberMeServicesTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testAutoLoginReturnsNullWhenNoCookie() { $service = $this->getService(null, array('name' => 'foo')); @@ -51,7 +44,7 @@ public function testAutoLoginThrowsExceptionOnNonExistentUser() { $userProvider = $this->getProvider(); $service = $this->getService($userProvider, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => true, 'lifetime' => 3600)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', $this->getCookie('fooclass', 'foouser', time()+3600, 'foopass')); $userProvider @@ -68,7 +61,7 @@ public function testAutoLoginDoesNotAcceptCookieWithInvalidHash() { $userProvider = $this->getProvider(); $service = $this->getService($userProvider, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => true, 'lifetime' => 3600)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', base64_encode('class:'.base64_encode('foouser').':123456789:fooHash')); $user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); @@ -93,7 +86,7 @@ public function testAutoLoginDoesNotAcceptAnExpiredCookie() { $userProvider = $this->getProvider(); $service = $this->getService($userProvider, array('name' => 'foo', 'path' => null, 'domain' => null, 'always_remember_me' => true, 'lifetime' => 3600)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', $this->getCookie('fooclass', 'foouser', time() - 1, 'foopass')); $user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); @@ -137,7 +130,7 @@ public function testAutoLogin() ; $service = $this->getService($userProvider, array('name' => 'foo', 'always_remember_me' => true, 'lifetime' => 3600)); - $request = new Request; + $request = new Request(); $request->cookies->set('foo', $this->getCookie('fooclass', 'foouser', time()+3600, 'foopass')); $returnedToken = $service->autoLogin($request); @@ -179,8 +172,8 @@ public function testLoginFail() public function testLoginSuccessIgnoresTokensWhichDoNotContainAnUserInterfaceImplementation() { $service = $this->getService(null, array('name' => 'foo', 'always_remember_me' => true, 'path' => null, 'domain' => null)); - $request = new Request; - $response = new Response; + $request = new Request(); + $response = new Response(); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token ->expects($this->once()) @@ -200,8 +193,8 @@ public function testLoginSuccessIgnoresTokensWhichDoNotContainAnUserInterfaceImp public function testLoginSuccess() { $service = $this->getService(null, array('name' => 'foo', 'domain' => 'myfoodomain.foo', 'path' => '/foo/path', 'secure' => true, 'httponly' => true, 'lifetime' => 3600, 'always_remember_me' => true)); - $request = new Request; - $response = new Response; + $request = new Request(); + $response = new Response(); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); diff --git a/src/Symfony/Component/Security/Tests/Http/Session/SessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php similarity index 89% rename from src/Symfony/Component/Security/Tests/Http/Session/SessionAuthenticationStrategyTest.php rename to src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php index 43c52b564d689..7be9054e54061 100644 --- a/src/Symfony/Component/Security/Tests/Http/Session/SessionAuthenticationStrategyTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionAuthenticationStrategyTest.php @@ -9,19 +9,12 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Tests\Http\Session; +namespace Symfony\Component\Security\Http\Tests\Session; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; class SessionAuthenticationStrategyTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - } - public function testSessionIsNotChanged() { $request = $this->getRequest(); diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json new file mode 100644 index 0000000000000..c544ad173ab41 --- /dev/null +++ b/src/Symfony/Component/Security/Http/composer.json @@ -0,0 +1,44 @@ +{ + "name": "symfony/security-http", + "type": "library", + "description": "Symfony Security Component - HTTP Integration", + "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.3", + "symfony/security-core": "~2.4", + "symfony/event-dispatcher": "~2.1", + "symfony/http-foundation": "~2.4", + "symfony/http-kernel": "~2.4" + }, + "require-dev": { + "symfony/routing": "~2.2", + "symfony/security-csrf": "~2.4", + "psr/log": "~1.0" + }, + "suggest": { + "symfony/security-csrf": "For using tokens to protect authentication/logout attempts", + "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\Security\\Http\\": "" } + }, + "target-dir": "Symfony/Component/Security/Http", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + } +} diff --git a/src/Symfony/Component/Security/Http/phpunit.xml.dist b/src/Symfony/Component/Security/Http/phpunit.xml.dist new file mode 100644 index 0000000000000..a735efdb19418 --- /dev/null +++ b/src/Symfony/Component/Security/Http/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./vendor + ./Tests + + + + diff --git a/src/Symfony/Component/Security/README.md b/src/Symfony/Component/Security/README.md index 53efa5eece987..5866d129e9fc8 100644 --- a/src/Symfony/Component/Security/README.md +++ b/src/Symfony/Component/Security/README.md @@ -11,10 +11,10 @@ Resources Documentation: -http://symfony.com/doc/2.3/book/security.html +http://symfony.com/doc/2.5/book/security.html -Resources ---------- +Tests +----- You can run the unit tests with the following command: diff --git a/src/Symfony/Component/Security/Tests/Http/Firewall/ExceptionListenerTest.php b/src/Symfony/Component/Security/Tests/Http/Firewall/ExceptionListenerTest.php index b1c7622414fb6..bc19da4db0d03 100644 --- a/src/Symfony/Component/Security/Tests/Http/Firewall/ExceptionListenerTest.php +++ b/src/Symfony/Component/Security/Tests/Http/Firewall/ExceptionListenerTest.php @@ -166,12 +166,7 @@ private function createEvent(\Exception $exception, $kernel = null) $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); } - $event = new GetResponseForExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, $exception); - - $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $event->setDispatcher($dispatcher); - - return $event; + return new GetResponseForExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, $exception); } private function createExceptionListener(SecurityContextInterface $context = null, AuthenticationTrustResolverInterface $trustResolver = null, HttpUtils $httpUtils = null, AuthenticationEntryPointInterface $authenticationEntryPoint = null, $errorPage = null, AccessDeniedHandlerInterface $accessDeniedHandler = null) diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index 826c58b2a45e6..a8a99f553f48f 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -19,25 +19,31 @@ "php": ">=5.3.3", "symfony/event-dispatcher": "~2.1", "symfony/http-foundation": "~2.1", - "symfony/http-kernel": "~2.1" + "symfony/http-kernel": "~2.4" + }, + "replace": { + "symfony/security-acl": "self.version", + "symfony/security-core": "self.version", + "symfony/security-csrf": "self.version", + "symfony/security-http": "self.version" }, "require-dev": { - "symfony/form": "~2.0", "symfony/routing": "~2.2", "symfony/validator": "~2.2", "doctrine/common": "~2.2", "doctrine/dbal": "~2.2", "psr/log": "~1.0", - "ircmaxell/password-compat": "1.0.*" + "ircmaxell/password-compat": "1.0.*", + "symfony/expression-language": "~2.4" }, "suggest": { - "symfony/class-loader": "", - "symfony/finder": "", - "symfony/form": "", - "symfony/validator": "", - "symfony/routing": "", - "doctrine/dbal": "to use the built-in ACL implementation", - "ircmaxell/password-compat": "" + "symfony/class-loader": "For using the ACL generateSql script", + "symfony/finder": "For using the ACL generateSql script", + "symfony/validator": "For using the user password constraint", + "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs", + "doctrine/dbal": "For using the built-in ACL implementation", + "symfony/expression-language": "For using the expression voter", + "ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5" }, "autoload": { "psr-0": { "Symfony\\Component\\Security\\": "" } @@ -46,7 +52,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Security/phpunit.xml.dist b/src/Symfony/Component/Security/phpunit.xml.dist index f45a44ec5a3d3..65cc1868610bf 100644 --- a/src/Symfony/Component/Security/phpunit.xml.dist +++ b/src/Symfony/Component/Security/phpunit.xml.dist @@ -13,7 +13,9 @@ > - ./Tests/ + ./Acl/Tests/ + ./Core/Tests/ + ./Http/Tests/ @@ -22,7 +24,9 @@ ./ ./vendor - ./Tests + ./Acl/Tests + ./Core/Tests + ./Http/Tests diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 5a332da390bb5..66081baa05854 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +2.4.0 +----- + + * added `$context` support for XMLEncoder. + * [DEPRECATION] JsonEncode and JsonDecode where modified to throw + an exception if error found. No need for get*Error() functions + 2.3.0 ----- diff --git a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php index 9dd336d5f7857..9bd5fcbe1692e 100644 --- a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php +++ b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Encoder; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + /** * Defines the interface of decoders * @@ -31,6 +33,8 @@ interface DecoderInterface * phpdoc comment. * * @return mixed + * + * @throws UnexpectedValueException */ public function decode($data, $format, array $context = array()); diff --git a/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php b/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php index 2290db7f50d85..b928b165294bb 100644 --- a/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php +++ b/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Encoder; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + /** * Defines the interface of encoders * @@ -26,6 +28,8 @@ interface EncoderInterface * @param array $context options that normalizers/encoders have access to. * * @return scalar + * + * @throws UnexpectedValueException */ public function encode($data, $format, array $context = array()); diff --git a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php index f8dfab35a8894..e649bffff9e3a 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Encoder; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + /** * Decodes JSON data * @@ -33,6 +35,7 @@ class JsonDecode implements DecoderInterface private $recursionDepth; private $lastError = JSON_ERROR_NONE; + protected $serializer; /** @@ -52,6 +55,8 @@ public function __construct($associative = false, $depth = 512) * * @return integer * + * @deprecated since 2.5, decode() throws an exception if error found, will be removed in 3.0 + * * @see http://php.net/manual/en/function.json-last-error.php json_last_error */ public function getLastError() @@ -82,6 +87,8 @@ public function getLastError() * * @return mixed * + * @throws UnexpectedValueException + * * @see http://php.net/json_decode json_decode */ public function decode($data, $format, array $context = array()) @@ -98,7 +105,9 @@ public function decode($data, $format, array $context = array()) $decodedData = json_decode($data, $associative, $recursionDepth); } - $this->lastError = json_last_error(); + if (JSON_ERROR_NONE !== $this->lastError = json_last_error()) { + throw new UnexpectedValueException(JsonEncoder::getLastErrorMessage()); + } return $decodedData; } diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php index 4e0de3ed75890..5869f969c09f9 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Encoder; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; + /** * Encodes JSON data * @@ -27,10 +29,12 @@ public function __construct($bitmask = 0) } /** - * Returns the last encoding error (if any) + * Returns the last encoding error (if any). * * @return integer * + * @deprecated since 2.5, encode() throws an exception if error found, will be removed in 3.0 + * * @see http://php.net/manual/en/function.json-last-error.php json_last_error */ public function getLastError() @@ -48,7 +52,10 @@ public function encode($data, $format, array $context = array()) $context = $this->resolveContext($context); $encodedJson = json_encode($data, $context['json_encode_options']); - $this->lastError = json_last_error(); + + if (JSON_ERROR_NONE !== $this->lastError = json_last_error()) { + throw new UnexpectedValueException(JsonEncoder::getLastErrorMessage()); + } return $encodedJson; } diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php index 02b179bc91a66..78f2e945e9bcc 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php @@ -32,14 +32,16 @@ class JsonEncoder implements EncoderInterface, DecoderInterface public function __construct(JsonEncode $encodingImpl = null, JsonDecode $decodingImpl = null) { - $this->encodingImpl = null === $encodingImpl ? new JsonEncode() : $encodingImpl; - $this->decodingImpl = null === $decodingImpl ? new JsonDecode(true) : $decodingImpl; + $this->encodingImpl = $encodingImpl ?: new JsonEncode(); + $this->decodingImpl = $decodingImpl ?: new JsonDecode(true); } /** * Returns the last encoding error (if any) * * @return integer + * + * @deprecated since 2.5, JsonEncode throws exception if an error is found, will be removed in 3.0 */ public function getLastEncodingError() { @@ -50,6 +52,8 @@ public function getLastEncodingError() * Returns the last decoding error (if any) * * @return integer + * + * @deprecated since 2.5, JsonDecode throws exception if an error is found, will be removed in 3.0 */ public function getLastDecodingError() { @@ -87,4 +91,31 @@ public function supportsDecoding($format) { return self::FORMAT === $format; } + + /** + * Resolves json_last_error message. + * + * @return string + */ + public static function getLastErrorMessage() + { + if (function_exists('json_last_error_msg')) { + return json_last_error_msg(); + } + + switch (json_last_error()) { + case JSON_ERROR_DEPTH: + return 'Maximum stack depth exceeded'; + case JSON_ERROR_STATE_MISMATCH: + return 'Underflow or the modes mismatch'; + case JSON_ERROR_CTRL_CHAR: + return 'Unexpected control character found'; + case JSON_ERROR_SYNTAX: + return 'Syntax error, malformed JSON'; + case JSON_ERROR_UTF8: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + default: + return 'Unknown error'; + } + } } diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index fe666285212df..9cd2417b468b2 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -24,6 +24,7 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec { private $dom; private $format; + private $context; private $rootNodeName = 'response'; /** @@ -47,8 +48,9 @@ public function encode($data, $format, array $context = array()) $xmlRootNodeName = $this->resolveXmlRootName($context); - $this->dom = new \DOMDocument(); + $this->dom = $this->createDomDocument($context); $this->format = $format; + $this->context = $context; if (null !== $data && !is_scalar($data)) { $root = $this->dom->createElement($xmlRootNodeName); @@ -109,24 +111,16 @@ public function decode($data, $format, array $context = array()) } /** - * Checks whether the serializer can encode to given format - * - * @param string $format format name - * - * @return Boolean - */ + * {@inheritdoc} + */ public function supportsEncoding($format) { return 'xml' === $format; } /** - * Checks whether the serializer can decode from given format - * - * @param string $format format name - * - * @return Boolean - */ + * {@inheritdoc} + */ public function supportsDecoding($format) { return 'xml' === $format; @@ -153,11 +147,11 @@ public function getRootNodeName() /** * @param \DOMNode $node - * @param string $val + * @param string $val * * @return Boolean */ - final protected function appendXMLString($node, $val) + final protected function appendXMLString(\DOMNode $node, $val) { if (strlen($val) > 0) { $frag = $this->dom->createDocumentFragment(); @@ -171,12 +165,12 @@ final protected function appendXMLString($node, $val) } /** - * @param DOMNode $node - * @param string $val + * @param \DOMNode $node + * @param string $val * * @return Boolean */ - final protected function appendText($node, $val) + final protected function appendText(\DOMNode $node, $val) { $nodeText = $this->dom->createTextNode($val); $node->appendChild($nodeText); @@ -185,12 +179,12 @@ final protected function appendText($node, $val) } /** - * @param DOMNode $node - * @param string $val + * @param \DOMNode $node + * @param string $val * * @return Boolean */ - final protected function appendCData($node, $val) + final protected function appendCData(\DOMNode $node, $val) { $nodeText = $this->dom->createCDATASection($val); $node->appendChild($nodeText); @@ -199,12 +193,12 @@ final protected function appendCData($node, $val) } /** - * @param DOMNode $node - * @param DOMDocumentFragment $fragment + * @param \DOMNode $node + * @param \DOMDocumentFragment $fragment * * @return Boolean */ - final protected function appendDocumentFragment($node, $fragment) + final protected function appendDocumentFragment(\DOMNode $node, $fragment) { if ($fragment instanceof \DOMDocumentFragment) { $node->appendChild($fragment); @@ -232,11 +226,11 @@ final protected function isElementNameValid($name) /** * Parse the input SimpleXmlElement into an array. * - * @param SimpleXmlElement $node xml to parse + * @param \SimpleXmlElement $node xml to parse * * @return array */ - private function parseXml($node) + private function parseXml(\SimpleXmlElement $node) { $data = array(); if ($node->attributes()) { @@ -260,9 +254,9 @@ private function parseXml($node) if ($key === 'item') { if (isset($value['@key'])) { if (isset($value['#'])) { - $data[(string) $value['@key']] = $value['#']; + $data[$value['@key']] = $value['#']; } else { - $data[(string) $value['@key']] = $value; + $data[$value['@key']] = $value; } } else { $data['item'][] = $value; @@ -283,15 +277,15 @@ private function parseXml($node) /** * Parse the data and convert it to DOMElements * - * @param DOMNode $parentNode - * @param array|object $data data - * @param string $xmlRootNodeName + * @param \DOMNode $parentNode + * @param array|object $data + * @param string|null $xmlRootNodeName * * @return Boolean * * @throws UnexpectedValueException */ - private function buildXml($parentNode, $data, $xmlRootNodeName = null) + private function buildXml(\DOMNode $parentNode, $data, $xmlRootNodeName = null) { $append = true; @@ -329,7 +323,7 @@ private function buildXml($parentNode, $data, $xmlRootNodeName = null) } if (is_object($data)) { - $data = $this->serializer->normalize($data, $this->format); + $data = $this->serializer->normalize($data, $this->format, $this->context); if (null !== $data && !is_scalar($data)) { return $this->buildXml($parentNode, $data, $xmlRootNodeName); } @@ -351,14 +345,14 @@ private function buildXml($parentNode, $data, $xmlRootNodeName = null) /** * Selects the type of node to create and appends it to the parent. * - * @param DOMNode $parentNode + * @param \DOMNode $parentNode * @param array|object $data * @param string $nodeName * @param string $key * * @return Boolean */ - private function appendNode($parentNode, $data, $nodeName, $key = null) + private function appendNode(\DOMNode $parentNode, $data, $nodeName, $key = null) { $node = $this->dom->createElement($nodeName); if (null !== $key) { @@ -388,12 +382,12 @@ private function needsCdataWrapping($val) /** * Tests the value being passed and decide what sort of element to create * - * @param DOMNode $node - * @param mixed $val + * @param \DOMNode $node + * @param mixed $val * * @return Boolean */ - private function selectNodeType($node, $val) + private function selectNodeType(\DOMNode $node, $val) { if (is_array($val)) { return $this->buildXml($node, $val); @@ -403,7 +397,7 @@ private function selectNodeType($node, $val) } elseif ($val instanceof \Traversable) { $this->buildXml($node, $val); } elseif (is_object($val)) { - return $this->buildXml($node, $this->serializer->normalize($val, $this->format)); + return $this->buildXml($node, $this->serializer->normalize($val, $this->format, $this->context)); } elseif (is_numeric($val)) { return $this->appendText($node, (string) $val); } elseif (is_string($val) && $this->needsCdataWrapping($val)) { @@ -430,4 +424,32 @@ private function resolveXmlRootName(array $context = array()) : $this->rootNodeName; } + /** + * Create a DOM document, taking serializer options into account. + * + * @param array $context options that the encoder has access to. + * + * @return \DOMDocument + */ + private function createDomDocument(array $context) + { + $document = new \DOMDocument(); + + // Set an attribute on the DOM document specifying, as part of the XML declaration, + $xmlOptions = array( + // the version number of the document + 'xml_version' => 'xmlVersion', + // the encoding of the document + 'xml_encoding' => 'encoding', + // whether the document is standalone + 'xml_standalone' => 'xmlStandalone', + ); + foreach ($xmlOptions as $xmlOption => $documentProperty) { + if (isset($context[$xmlOption])) { + $document->$documentProperty = $context[$xmlOption]; + } + } + + return $document; + } } diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 54854c6cf499e..1b1a5f5688749 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -41,9 +41,9 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal protected $camelizedAttributes = array(); /** - * Set normalization callbacks + * Set normalization callbacks. * - * @param array $callbacks help normalize the result + * @param callable[] $callbacks help normalize the result * * @throws InvalidArgumentException if a non-callable callback is set */ diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php index b5ec1a2352235..00714f23ba2a9 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php @@ -19,7 +19,7 @@ class JsonEncoderTest extends \PHPUnit_Framework_TestCase { protected function setUp() { - $this->encoder = new JsonEncoder; + $this->encoder = new JsonEncoder(); $this->serializer = new Serializer(array(new CustomNormalizer()), array('json' => new JsonEncoder())); } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index 71da72f8e006d..6ad2a6c2e2561 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -24,14 +24,14 @@ class XmlEncoderTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->encoder = new XmlEncoder; + $this->encoder = new XmlEncoder(); $serializer = new Serializer(array(new CustomNormalizer()), array('xml' => new XmlEncoder())); $this->encoder->setSerializer($serializer); } public function testEncodeScalar() { - $obj = new ScalarDummy; + $obj = new ScalarDummy(); $obj->xmlFoo = "foo"; $expected = ''."\n". @@ -42,7 +42,7 @@ public function testEncodeScalar() public function testSetRootNodeName() { - $obj = new ScalarDummy; + $obj = new ScalarDummy(); $obj->xmlFoo = "foo"; $this->encoder->setRootNodeName('test'); @@ -63,7 +63,7 @@ public function testDocTypeIsNotAllowed() public function testAttributes() { - $obj = new ScalarDummy; + $obj = new ScalarDummy(); $obj->xmlFoo = array( 'foo-bar' => array( '@id' => 1, @@ -92,7 +92,7 @@ public function testAttributes() public function testElementNameValid() { - $obj = new ScalarDummy; + $obj = new ScalarDummy(); $obj->xmlFoo = array( 'foo-bar' => 'a', 'foo_bar' => 'a', @@ -120,6 +120,23 @@ public function testEncodeSimpleXML() $this->assertEquals($expected, $this->encoder->encode($array, 'xml')); } + public function testEncodeXmlAttributes() + { + $xml = simplexml_load_string('Peter'); + $array = array('person' => $xml); + + $expected = ''."\n". + 'Peter'."\n"; + + $context = array( + 'xml_version' => '1.1', + 'xml_encoding' => 'utf-8', + 'xml_standalone' => true, + ); + + $this->assertSame($expected, $this->encoder->encode($array, 'xml', $context)); + } + public function testEncodeScalarRootAttributes() { $array = array( @@ -189,7 +206,7 @@ public function testEncode() public function testEncodeSerializerXmlRootNodeNameOption() { $options = array('xml_root_node_name' => 'test'); - $this->encoder = new XmlEncoder; + $this->encoder = new XmlEncoder(); $serializer = new Serializer(array(), array('xml' => new XmlEncoder())); $this->encoder->setSerializer($serializer); @@ -272,7 +289,7 @@ public function testDecodeArray() public function testDecodeWithoutItemHash() { - $obj = new ScalarDummy; + $obj = new ScalarDummy(); $obj->xmlFoo = array( 'foo-bar' => array( '@key' => "value", @@ -345,7 +362,7 @@ protected function getXmlSource() protected function getObject() { - $obj = new Dummy; + $obj = new Dummy(); $obj->foo = 'foo'; $obj->bar = array('a', 'b'); $obj->baz = array('key' => 'val', 'key2' => 'val', 'A B' => 'bar', 'item' => array(array('title' => 'title1'), array('title' => 'title2')), 'Barry' => array('FooBar' => array('Baz' => 'Ed', '@id' => 1))); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/CustomNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/CustomNormalizerTest.php index 7b4b4ae28d85f..eef163442edac 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/CustomNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/CustomNormalizerTest.php @@ -19,13 +19,13 @@ class CustomNormalizerTest extends \PHPUnit_Framework_TestCase { protected function setUp() { - $this->normalizer = new CustomNormalizer; - $this->normalizer->setSerializer(new Serializer); + $this->normalizer = new CustomNormalizer(); + $this->normalizer->setSerializer(new Serializer()); } public function testSerialize() { - $obj = new ScalarDummy; + $obj = new ScalarDummy(); $obj->foo = 'foo'; $obj->xmlFoo = 'xml'; $this->assertEquals('foo', $this->normalizer->normalize($obj, 'json')); @@ -34,18 +34,18 @@ public function testSerialize() public function testDeserialize() { - $obj = $this->normalizer->denormalize('foo', get_class(new ScalarDummy), 'xml'); + $obj = $this->normalizer->denormalize('foo', get_class(new ScalarDummy()), 'xml'); $this->assertEquals('foo', $obj->xmlFoo); $this->assertNull($obj->foo); - $obj = $this->normalizer->denormalize('foo', get_class(new ScalarDummy), 'json'); + $obj = $this->normalizer->denormalize('foo', get_class(new ScalarDummy()), 'json'); $this->assertEquals('foo', $obj->foo); $this->assertNull($obj->xmlFoo); } public function testSupportsNormalization() { - $this->assertTrue($this->normalizer->supportsNormalization(new ScalarDummy)); + $this->assertTrue($this->normalizer->supportsNormalization(new ScalarDummy())); $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass)); } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 42846166e453b..f3bf9694d206c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -17,13 +17,13 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase { protected function setUp() { - $this->normalizer = new GetSetMethodNormalizer; + $this->normalizer = new GetSetMethodNormalizer(); $this->normalizer->setSerializer($this->getMock('Symfony\Component\Serializer\Serializer')); } public function testNormalize() { - $obj = new GetSetDummy; + $obj = new GetSetDummy(); $obj->setFoo('foo'); $obj->setBar('bar'); $obj->setCamelCase('camelcase'); @@ -118,7 +118,7 @@ public function testIgnoredAttributes() { $this->normalizer->setIgnoredAttributes(array('foo', 'bar', 'camelCase')); - $obj = new GetSetDummy; + $obj = new GetSetDummy(); $obj->setFoo('foo'); $obj->setBar('bar'); diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 3c189461f28f1..106bcff7adb59 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -34,14 +34,14 @@ public function testNormalizeNoMatch() public function testNormalizeTraversable() { $this->serializer = new Serializer(array(), array('json' => new JsonEncoder())); - $result = $this->serializer->serialize(new TraversableDummy, 'json'); + $result = $this->serializer->serialize(new TraversableDummy(), 'json'); $this->assertEquals('{"foo":"foo","bar":"bar"}', $result); } public function testNormalizeGivesPriorityToInterfaceOverTraversable() { - $this->serializer = new Serializer(array(new CustomNormalizer), array('json' => new JsonEncoder())); - $result = $this->serializer->serialize(new NormalizableTraversableDummy, 'json'); + $this->serializer = new Serializer(array(new CustomNormalizer()), array('json' => new JsonEncoder())); + $result = $this->serializer->serialize(new NormalizableTraversableDummy(), 'json'); $this->assertEquals('{"foo":"normalizedFoo","bar":"normalizedBar"}', $result); } diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index ab537b2955270..9e0fe096b07e7 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Stopwatch/StopwatchEvent.php b/src/Symfony/Component/Stopwatch/StopwatchEvent.php index 7adfdf0bb84b0..41d7279e35824 100644 --- a/src/Symfony/Component/Stopwatch/StopwatchEvent.php +++ b/src/Symfony/Component/Stopwatch/StopwatchEvent.php @@ -21,7 +21,7 @@ class StopwatchEvent /** * @var StopwatchPeriod[] */ - private $periods; + private $periods = array(); /** * @var float @@ -36,13 +36,13 @@ class StopwatchEvent /** * @var float[] */ - private $started; + private $started = array(); /** * Constructor. * - * @param float $origin The origin time in milliseconds - * @param string $category The event category + * @param float $origin The origin time in milliseconds + * @param string|null $category The event category or null to use the default * * @throws \InvalidArgumentException When the raw time is not valid */ @@ -50,8 +50,6 @@ public function __construct($origin, $category = null) { $this->origin = $this->formatTime($origin); $this->category = is_string($category) ? $category : 'default'; - $this->started = array(); - $this->periods = array(); } /** @@ -67,7 +65,7 @@ public function getCategory() /** * Gets the origin. * - * @return integer The origin in milliseconds + * @return float The origin in milliseconds */ public function getOrigin() { @@ -178,7 +176,7 @@ public function getDuration() $total += $period->getDuration(); } - return $this->formatTime($total); + return $total; } /** diff --git a/src/Symfony/Component/Stopwatch/StopwatchPeriod.php b/src/Symfony/Component/Stopwatch/StopwatchPeriod.php index 57393f4742be0..f757a016a93b4 100644 --- a/src/Symfony/Component/Stopwatch/StopwatchPeriod.php +++ b/src/Symfony/Component/Stopwatch/StopwatchPeriod.php @@ -23,10 +23,10 @@ class StopwatchPeriod private $memory; /** - * Constructor + * Constructor. * - * @param integer $start The relative time of the start of the period - * @param integer $end The relative time of the end of the period + * @param integer $start The relative time of the start of the period (in milliseconds) + * @param integer $end The relative time of the end of the period (in milliseconds) */ public function __construct($start, $end) { diff --git a/src/Symfony/Component/Stopwatch/composer.json b/src/Symfony/Component/Stopwatch/composer.json index 97e214fa2df2e..75ed1f60fc050 100644 --- a/src/Symfony/Component/Stopwatch/composer.json +++ b/src/Symfony/Component/Stopwatch/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Templating/Asset/UrlPackage.php b/src/Symfony/Component/Templating/Asset/UrlPackage.php index a21a6b4848c40..da5c0830fb4ea 100644 --- a/src/Symfony/Component/Templating/Asset/UrlPackage.php +++ b/src/Symfony/Component/Templating/Asset/UrlPackage.php @@ -73,7 +73,7 @@ public function getBaseUrl($path) return $this->baseUrls[0]; default: - return $this->baseUrls[fmod(hexdec(substr(md5($path), 0, 10)), $count)]; + return $this->baseUrls[fmod(hexdec(substr(hash('sha256', $path), 0, 10)), $count)]; } } } diff --git a/src/Symfony/Component/Templating/DebuggerInterface.php b/src/Symfony/Component/Templating/DebuggerInterface.php index 43026620ec03d..00d29472852a5 100644 --- a/src/Symfony/Component/Templating/DebuggerInterface.php +++ b/src/Symfony/Component/Templating/DebuggerInterface.php @@ -16,6 +16,8 @@ * to debug template loader instances. * * @author Fabien Potencier + * + * @deprecated Deprecated in 2.4, to be removed in 3.0. Use Psr\Log\LoggerInterface instead. */ interface DebuggerInterface { diff --git a/src/Symfony/Component/Templating/DelegatingEngine.php b/src/Symfony/Component/Templating/DelegatingEngine.php index cd3705194383b..c08540bf36c66 100644 --- a/src/Symfony/Component/Templating/DelegatingEngine.php +++ b/src/Symfony/Component/Templating/DelegatingEngine.php @@ -23,7 +23,7 @@ class DelegatingEngine implements EngineInterface, StreamingEngineInterface /** * @var EngineInterface[] */ - protected $engines; + protected $engines = array(); /** * Constructor. @@ -34,7 +34,6 @@ class DelegatingEngine implements EngineInterface, StreamingEngineInterface */ public function __construct(array $engines = array()) { - $this->engines = array(); foreach ($engines as $engine) { $this->addEngine($engine); } @@ -106,7 +105,7 @@ public function supports($name) /** * Get an engine able to render the given template. * - * @param mixed $name A template name or a TemplateReferenceInterface instance + * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance * * @return EngineInterface The engine * @@ -114,7 +113,7 @@ public function supports($name) * * @api */ - protected function getEngine($name) + public function getEngine($name) { foreach ($this->engines as $engine) { if ($engine->supports($name)) { diff --git a/src/Symfony/Component/Templating/EngineInterface.php b/src/Symfony/Component/Templating/EngineInterface.php index dfa9c55efadcf..a7291bfa8188c 100644 --- a/src/Symfony/Component/Templating/EngineInterface.php +++ b/src/Symfony/Component/Templating/EngineInterface.php @@ -23,7 +23,7 @@ * TemplateReferenceInterface, a TemplateNameParserInterface should be used to * convert the name to a TemplateReferenceInterface instance. * - * Each template loader use the logical template name to look for + * Each template loader uses the logical template name to look for * the template. * * @author Fabien Potencier @@ -35,8 +35,8 @@ interface EngineInterface /** * Renders a template. * - * @param mixed $name A template name or a TemplateReferenceInterface instance - * @param array $parameters An array of parameters to pass to the template + * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance + * @param array $parameters An array of parameters to pass to the template * * @return string The evaluated template as a string * @@ -49,10 +49,12 @@ public function render($name, array $parameters = array()); /** * Returns true if the template exists. * - * @param mixed $name A template name or a TemplateReferenceInterface instance + * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance * * @return Boolean true if the template exists, false otherwise * + * @throws \RuntimeException if the engine cannot handle the template name + * * @api */ public function exists($name); @@ -60,7 +62,7 @@ public function exists($name); /** * Returns true if this class is able to render the given template. * - * @param mixed $name A template name or a TemplateReferenceInterface instance + * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance * * @return Boolean true if this class supports the given template, false otherwise * diff --git a/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php b/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php index a0a643a41b5c3..73f1455622821 100644 --- a/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php +++ b/src/Symfony/Component/Templating/Helper/CoreAssetsHelper.php @@ -28,7 +28,7 @@ class CoreAssetsHelper extends Helper implements PackageInterface { protected $defaultPackage; - protected $namedPackages; + protected $namedPackages = array(); /** * Constructor. @@ -39,7 +39,6 @@ class CoreAssetsHelper extends Helper implements PackageInterface public function __construct(PackageInterface $defaultPackage, array $namedPackages = array()) { $this->defaultPackage = $defaultPackage; - $this->namedPackages = array(); foreach ($namedPackages as $name => $package) { $this->addPackage($name, $package); diff --git a/src/Symfony/Component/Templating/Loader/CacheLoader.php b/src/Symfony/Component/Templating/Loader/CacheLoader.php index 9fcb6bf767599..832b3cb7b5658 100644 --- a/src/Symfony/Component/Templating/Loader/CacheLoader.php +++ b/src/Symfony/Component/Templating/Loader/CacheLoader.php @@ -50,13 +50,16 @@ public function __construct(LoaderInterface $loader, $dir) */ public function load(TemplateReferenceInterface $template) { - $key = md5($template->getLogicalName()); + $key = hash('sha256', $template->getLogicalName()); $dir = $this->dir.DIRECTORY_SEPARATOR.substr($key, 0, 2); $file = substr($key, 2).'.tpl'; $path = $dir.DIRECTORY_SEPARATOR.$file; if (is_file($path)) { - if (null !== $this->debugger) { + if (null !== $this->logger) { + $this->logger->debug(sprintf('Fetching template "%s" from cache', $template->get('name'))); + } elseif (null !== $this->debugger) { + // just for BC, to be removed in 3.0 $this->debugger->log(sprintf('Fetching template "%s" from cache', $template->get('name'))); } @@ -75,7 +78,10 @@ public function load(TemplateReferenceInterface $template) file_put_contents($path, $content); - if (null !== $this->debugger) { + if (null !== $this->logger) { + $this->logger->debug(sprintf('Storing template "%s" in cache', $template->get('name'))); + } elseif (null !== $this->debugger) { + // just for BC, to be removed in 3.0 $this->debugger->log(sprintf('Storing template "%s" in cache', $template->get('name'))); } diff --git a/src/Symfony/Component/Templating/Loader/ChainLoader.php b/src/Symfony/Component/Templating/Loader/ChainLoader.php index 2009856e79b18..bcb250b08f5e2 100644 --- a/src/Symfony/Component/Templating/Loader/ChainLoader.php +++ b/src/Symfony/Component/Templating/Loader/ChainLoader.php @@ -21,7 +21,7 @@ */ class ChainLoader extends Loader { - protected $loaders; + protected $loaders = array(); /** * Constructor. @@ -30,7 +30,6 @@ class ChainLoader extends Loader */ public function __construct(array $loaders = array()) { - $this->loaders = array(); foreach ($loaders as $loader) { $this->addLoader($loader); } diff --git a/src/Symfony/Component/Templating/Loader/FilesystemLoader.php b/src/Symfony/Component/Templating/Loader/FilesystemLoader.php index 563010c6a6106..08c2e6a9d96d8 100644 --- a/src/Symfony/Component/Templating/Loader/FilesystemLoader.php +++ b/src/Symfony/Component/Templating/Loader/FilesystemLoader.php @@ -63,20 +63,25 @@ public function load(TemplateReferenceInterface $template) $logs = array(); foreach ($this->templatePathPatterns as $templatePathPattern) { if (is_file($file = strtr($templatePathPattern, $replacements)) && is_readable($file)) { - if (null !== $this->debugger) { + if (null !== $this->logger) { + $this->logger->debug(sprintf('Loaded template file "%s"', $file)); + } elseif (null !== $this->debugger) { + // just for BC, to be removed in 3.0 $this->debugger->log(sprintf('Loaded template file "%s"', $file)); } return new FileStorage($file); } - if (null !== $this->debugger) { + if (null !== $this->logger || null !== $this->debugger) { $logs[] = sprintf('Failed loading template file "%s"', $file); } } - if (null !== $this->debugger) { - foreach ($logs as $log) { + foreach ($logs as $log) { + if (null !== $this->logger) { + $this->logger->debug($log); + } elseif (null !== $this->debugger) { $this->debugger->log($log); } } diff --git a/src/Symfony/Component/Templating/Loader/Loader.php b/src/Symfony/Component/Templating/Loader/Loader.php index 8fac1cce64d3d..7239ac73d2a86 100644 --- a/src/Symfony/Component/Templating/Loader/Loader.php +++ b/src/Symfony/Component/Templating/Loader/Loader.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Templating\Loader; +use Psr\Log\LoggerInterface; use Symfony\Component\Templating\DebuggerInterface; /** @@ -20,12 +21,32 @@ */ abstract class Loader implements LoaderInterface { + /** + * @var LoggerInterface|null + */ + protected $logger; + + /** + * @deprecated Deprecated in 2.4, to be removed in 3.0. Use $this->logger instead. + */ protected $debugger; + /** + * Sets the debug logger to use for this loader. + * + * @param LoggerInterface $logger A logger instance + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + /** * Sets the debugger to use for this loader. * * @param DebuggerInterface $debugger A debugger instance + * + * @deprecated Deprecated in 2.4, to be removed in 3.0. Use $this->setLogger() instead. */ public function setDebugger(DebuggerInterface $debugger) { diff --git a/src/Symfony/Component/Templating/PhpEngine.php b/src/Symfony/Component/Templating/PhpEngine.php index 11d4b61c857fd..9ba1ae92dac5e 100644 --- a/src/Symfony/Component/Templating/PhpEngine.php +++ b/src/Symfony/Component/Templating/PhpEngine.php @@ -32,16 +32,19 @@ class PhpEngine implements EngineInterface, \ArrayAccess { protected $loader; protected $current; - protected $helpers; - protected $parents; - protected $stack; - protected $charset; - protected $cache; - protected $escapers; - protected static $escaperCache; - protected $globals; + protected $helpers = array(); + protected $parents = array(); + protected $stack = array(); + protected $charset = 'UTF-8'; + protected $cache = array(); + protected $escapers = array(); + protected static $escaperCache = array(); + protected $globals = array(); protected $parser; + private $evalTemplate; + private $evalParameters; + /** * Constructor. * @@ -53,13 +56,8 @@ public function __construct(TemplateNameParserInterface $parser, LoaderInterface { $this->parser = $parser; $this->loader = $loader; - $this->parents = array(); - $this->stack = array(); - $this->charset = 'UTF-8'; - $this->cache = array(); - $this->globals = array(); - $this->setHelpers($helpers); + $this->addHelpers($helpers); $this->initializeEscapers(); foreach ($this->escapers as $context => $escaper) { @@ -68,22 +66,16 @@ public function __construct(TemplateNameParserInterface $parser, LoaderInterface } /** - * Renders a template. - * - * @param mixed $name A template name or a TemplateReferenceInterface instance - * @param array $parameters An array of parameters to pass to the template - * - * @return string The evaluated template as a string + * {@inheritdoc} * * @throws \InvalidArgumentException if the template does not exist - * @throws \RuntimeException if the template cannot be rendered * * @api */ public function render($name, array $parameters = array()) { $storage = $this->load($name); - $key = md5(serialize($storage)); + $key = hash('sha256', serialize($storage)); $this->current = $key; $this->parents[$key] = null; @@ -109,11 +101,7 @@ public function render($name, array $parameters = array()) } /** - * Returns true if the template exists. - * - * @param mixed $name A template name or a TemplateReferenceInterface instance - * - * @return Boolean true if the template exists, false otherwise + * {@inheritdoc} * * @api */ @@ -129,11 +117,7 @@ public function exists($name) } /** - * Returns true if this class is able to render the given template. - * - * @param mixed $name A template name or a TemplateReferenceInterface instance - * - * @return Boolean true if this class supports the given resource, false otherwise + * {@inheritdoc} * * @api */ @@ -156,24 +140,36 @@ public function supports($name) */ protected function evaluate(Storage $template, array $parameters = array()) { - $__template__ = $template; + $this->evalTemplate = $template; + $this->evalParameters = $parameters; + unset($template, $parameters); - if (isset($parameters['__template__'])) { - throw new \InvalidArgumentException('Invalid parameter (__template__)'); + if (isset($this->evalParameters['this'])) { + throw new \InvalidArgumentException('Invalid parameter (this)'); + } + if (isset($this->evalParameters['view'])) { + throw new \InvalidArgumentException('Invalid parameter (view)'); } - if ($__template__ instanceof FileStorage) { - extract($parameters, EXTR_SKIP); - $view = $this; + $view = $this; + if ($this->evalTemplate instanceof FileStorage) { + extract($this->evalParameters, EXTR_SKIP); + $this->evalParameters = null; + ob_start(); - require $__template__; + require $this->evalTemplate; + + $this->evalTemplate = null; return ob_get_clean(); - } elseif ($__template__ instanceof StringStorage) { - extract($parameters, EXTR_SKIP); - $view = $this; + } elseif ($this->evalTemplate instanceof StringStorage) { + extract($this->evalParameters, EXTR_SKIP); + $this->evalParameters = null; + ob_start(); - eval('; ?>'.$__template__.''.$this->evalTemplate.'evalTemplate = null; return ob_get_clean(); } @@ -552,7 +548,7 @@ public function getLoader() /** * Loads the given template. * - * @param mixed $name A template name or a TemplateReferenceInterface instance + * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance * * @return Storage A Storage instance * diff --git a/src/Symfony/Component/Templating/StreamingEngineInterface.php b/src/Symfony/Component/Templating/StreamingEngineInterface.php index 586f6b1de2c1c..f8815f85f19b6 100644 --- a/src/Symfony/Component/Templating/StreamingEngineInterface.php +++ b/src/Symfony/Component/Templating/StreamingEngineInterface.php @@ -23,8 +23,8 @@ interface StreamingEngineInterface * * The implementation should output the content directly to the client. * - * @param mixed $name A template name or a TemplateReferenceInterface instance - * @param array $parameters An array of parameters to pass to the template + * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance + * @param array $parameters An array of parameters to pass to the template * * @throws \RuntimeException if the template cannot be rendered * @throws \LogicException if the template cannot be streamed diff --git a/src/Symfony/Component/Templating/TemplateNameParser.php b/src/Symfony/Component/Templating/TemplateNameParser.php index 585697afae6e8..ac6b573636a85 100644 --- a/src/Symfony/Component/Templating/TemplateNameParser.php +++ b/src/Symfony/Component/Templating/TemplateNameParser.php @@ -24,11 +24,7 @@ class TemplateNameParser implements TemplateNameParserInterface { /** - * Parses a template to an array of parameters. - * - * @param string $name A template name - * - * @return TemplateReferenceInterface A template + * {@inheritdoc} * * @api */ diff --git a/src/Symfony/Component/Templating/TemplateNameParserInterface.php b/src/Symfony/Component/Templating/TemplateNameParserInterface.php index 6305f43be2471..30df0c94f4a38 100644 --- a/src/Symfony/Component/Templating/TemplateNameParserInterface.php +++ b/src/Symfony/Component/Templating/TemplateNameParserInterface.php @@ -24,7 +24,7 @@ interface TemplateNameParserInterface /** * Convert a template name to a TemplateReferenceInterface instance. * - * @param string $name A template name + * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance * * @return TemplateReferenceInterface A template * diff --git a/src/Symfony/Component/Templating/TemplateReference.php b/src/Symfony/Component/Templating/TemplateReference.php index 585bd7a2add97..560ee1a0f876b 100644 --- a/src/Symfony/Component/Templating/TemplateReference.php +++ b/src/Symfony/Component/Templating/TemplateReference.php @@ -30,20 +30,16 @@ public function __construct($name = null, $engine = null) ); } + /** + * {@inheritdoc} + */ public function __toString() { return $this->getLogicalName(); } /** - * Sets a template parameter. - * - * @param string $name The parameter name - * @param string $value The parameter value - * - * @return TemplateReferenceInterface The TemplateReferenceInterface instance - * - * @throws \InvalidArgumentException if the parameter is not defined + * {@inheritdoc} * * @api */ @@ -59,13 +55,7 @@ public function set($name, $value) } /** - * Gets a template parameter. - * - * @param string $name The parameter name - * - * @return string The parameter value - * - * @throws \InvalidArgumentException if the parameter is not defined + * {@inheritdoc} * * @api */ @@ -79,9 +69,7 @@ public function get($name) } /** - * Gets the template parameters. - * - * @return array An array of parameters + * {@inheritdoc} * * @api */ @@ -91,11 +79,7 @@ public function all() } /** - * Returns the path to the template. - * - * By default, it just returns the template name. - * - * @return string A path to the template or a resource + * {@inheritdoc} * * @api */ @@ -105,11 +89,7 @@ public function getPath() } /** - * Returns the "logical" template name. - * - * The template name acts as a unique identifier for the template. - * - * @return string The template name + * {@inheritdoc} * * @api */ diff --git a/src/Symfony/Component/Templating/TemplateReferenceInterface.php b/src/Symfony/Component/Templating/TemplateReferenceInterface.php index 60c910a1cdb49..d6b2ef43b55d0 100644 --- a/src/Symfony/Component/Templating/TemplateReferenceInterface.php +++ b/src/Symfony/Component/Templating/TemplateReferenceInterface.php @@ -37,7 +37,7 @@ public function all(); * * @return TemplateReferenceInterface The TemplateReferenceInterface instance * - * @throws \InvalidArgumentException if the parameter is not defined + * @throws \InvalidArgumentException if the parameter name is not supported * * @api */ @@ -50,7 +50,7 @@ public function set($name, $value); * * @return string The parameter value * - * @throws \InvalidArgumentException if the parameter is not defined + * @throws \InvalidArgumentException if the parameter name is not supported * * @api */ @@ -77,4 +77,15 @@ public function getPath(); * @api */ public function getLogicalName(); + + /** + * Returns the string representation as shortcut for getLogicalName(). + * + * Alias of getLogicalName(). + * + * @return string The template name + * + * @api + */ + public function __toString(); } diff --git a/src/Symfony/Component/Templating/Tests/DelegatingEngineTest.php b/src/Symfony/Component/Templating/Tests/DelegatingEngineTest.php new file mode 100644 index 0000000000000..93b35014331a9 --- /dev/null +++ b/src/Symfony/Component/Templating/Tests/DelegatingEngineTest.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Templating\Tests; + +use Symfony\Component\Templating\DelegatingEngine; +use Symfony\Component\Templating\StreamingEngineInterface; +use Symfony\Component\Templating\EngineInterface; + +class DelegatingEngineTest extends \PHPUnit_Framework_TestCase +{ + public function testRenderDelegatesToSupportedEngine() + { + $firstEngine = $this->getEngineMock('template.php', false); + $secondEngine = $this->getEngineMock('template.php', true); + + $secondEngine->expects($this->once()) + ->method('render') + ->with('template.php', array('foo' => 'bar')) + ->will($this->returnValue('')); + + $delegatingEngine = new DelegatingEngine(array($firstEngine, $secondEngine)); + $result = $delegatingEngine->render('template.php', array('foo' => 'bar')); + + $this->assertSame('', $result); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage No engine is able to work with the template "template.php" + */ + public function testRenderWithNoSupportedEngine() + { + $firstEngine = $this->getEngineMock('template.php', false); + $secondEngine = $this->getEngineMock('template.php', false); + + $delegatingEngine = new DelegatingEngine(array($firstEngine, $secondEngine)); + $delegatingEngine->render('template.php', array('foo' => 'bar')); + } + + public function testStreamDelegatesToSupportedEngine() + { + $streamingEngine = $this->getStreamingEngineMock('template.php', true); + $streamingEngine->expects($this->once()) + ->method('stream') + ->with('template.php', array('foo' => 'bar')) + ->will($this->returnValue('')); + + $delegatingEngine = new DelegatingEngine(array($streamingEngine)); + $result = $delegatingEngine->stream('template.php', array('foo' => 'bar')); + + $this->assertNull($result); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage Template "template.php" cannot be streamed as the engine supporting it does not implement StreamingEngineInterface + */ + public function testStreamRequiresStreamingEngine() + { + $engine = $this->getEngineMock('template.php', true); + $engine->expects($this->never())->method('stream'); + + $delegatingEngine = new DelegatingEngine(array($engine)); + $delegatingEngine->stream('template.php', array('foo' => 'bar')); + } + + public function testExists() + { + $engine = $this->getEngineMock('template.php', true); + $engine->expects($this->once()) + ->method('exists') + ->with('template.php') + ->will($this->returnValue(true)); + + $delegatingEngine = new DelegatingEngine(array($engine)); + + $this->assertTrue($delegatingEngine->exists('template.php')); + } + + public function testSupports() + { + $engine = $this->getEngineMock('template.php', true); + + $delegatingEngine = new DelegatingEngine(array($engine)); + + $this->assertTrue($delegatingEngine->supports('template.php')); + } + + public function testSupportsWithNoSupportedEngine() + { + $engine = $this->getEngineMock('template.php', false); + + $delegatingEngine = new DelegatingEngine(array($engine)); + + $this->assertFalse($delegatingEngine->supports('template.php')); + } + + public function testGetExistingEngine() + { + $firstEngine = $this->getEngineMock('template.php', false); + $secondEngine = $this->getEngineMock('template.php', true); + + $delegatingEngine = new DelegatingEngine(array($firstEngine, $secondEngine)); + + $this->assertSame($secondEngine, $delegatingEngine->getEngine('template.php', array('foo' => 'bar'))); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage No engine is able to work with the template "template.php" + */ + public function testGetInvalidEngine() + { + $firstEngine = $this->getEngineMock('template.php', false); + $secondEngine = $this->getEngineMock('template.php', false); + + $delegatingEngine = new DelegatingEngine(array($firstEngine, $secondEngine)); + $delegatingEngine->getEngine('template.php', array('foo' => 'bar')); + } + + private function getEngineMock($template, $supports) + { + $engine = $this->getMock('Symfony\Component\Templating\EngineInterface'); + + $engine->expects($this->once()) + ->method('supports') + ->with($template) + ->will($this->returnValue($supports)); + + return $engine; + } + + private function getStreamingEngineMock($template, $supports) + { + $engine = $this->getMockForAbstractClass('Symfony\Component\Templating\Tests\MyStreamingEngine'); + + $engine->expects($this->once()) + ->method('supports') + ->with($template) + ->will($this->returnValue($supports)); + + return $engine; + } +} + +interface MyStreamingEngine extends StreamingEngineInterface, EngineInterface +{ +} diff --git a/src/Symfony/Component/Templating/Tests/Fixtures/ProjectTemplateDebugger.php b/src/Symfony/Component/Templating/Tests/Fixtures/ProjectTemplateDebugger.php deleted file mode 100644 index f414078eb2bbd..0000000000000 --- a/src/Symfony/Component/Templating/Tests/Fixtures/ProjectTemplateDebugger.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Templating\Tests\Fixtures; - -use Symfony\Component\Templating\DebuggerInterface; - -class ProjectTemplateDebugger implements DebuggerInterface -{ - protected $messages = array(); - - public function log($message) - { - $this->messages[] = $message; - } - - public function hasMessage($regex) - { - foreach ($this->messages as $message) { - if (preg_match('#'.preg_quote($regex, '#').'#', $message)) { - return true; - } - } - - return false; - } - - public function getMessages() - { - return $this->messages; - } -} diff --git a/src/Symfony/Component/Templating/Tests/Loader/CacheLoaderTest.php b/src/Symfony/Component/Templating/Tests/Loader/CacheLoaderTest.php index 6db7fecbffef7..b8b5f10e8701e 100644 --- a/src/Symfony/Component/Templating/Tests/Loader/CacheLoaderTest.php +++ b/src/Symfony/Component/Templating/Tests/Loader/CacheLoaderTest.php @@ -31,13 +31,19 @@ public function testLoad() { $dir = sys_get_temp_dir().DIRECTORY_SEPARATOR.rand(111111, 999999); mkdir($dir, 0777, true); + $loader = new ProjectTemplateLoader($varLoader = new ProjectTemplateLoaderVar(new TemplateNameParser()), $dir); - $loader->setDebugger($debugger = new \Symfony\Component\Templating\Tests\Fixtures\ProjectTemplateDebugger()); $this->assertFalse($loader->load(new TemplateReference('foo', 'php')), '->load() returns false if the embed loader is not able to load the template'); + + $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger->expects($this->once())->method('debug')->with('Storing template "index" in cache'); + $loader->setLogger($logger); $loader->load(new TemplateReference('index')); - $this->assertTrue($debugger->hasMessage('Storing template'), '->load() logs a "Storing template" message if the template is found'); + + $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger->expects($this->once())->method('debug')->with('Fetching template "index" from cache'); + $loader->setLogger($logger); $loader->load(new TemplateReference('index')); - $this->assertTrue($debugger->hasMessage('Fetching template'), '->load() logs a "Storing template" message if the template is fetched from cache'); } } diff --git a/src/Symfony/Component/Templating/Tests/Loader/FilesystemLoaderTest.php b/src/Symfony/Component/Templating/Tests/Loader/FilesystemLoaderTest.php index 8eb663637d021..74234808ddcbf 100644 --- a/src/Symfony/Component/Templating/Tests/Loader/FilesystemLoaderTest.php +++ b/src/Symfony/Component/Templating/Tests/Loader/FilesystemLoaderTest.php @@ -59,15 +59,16 @@ public function testLoad() $this->assertInstanceOf('Symfony\Component\Templating\Storage\FileStorage', $storage, '->load() returns a FileStorage if you pass a relative template that exists'); $this->assertEquals($path.'/foo.php', (string) $storage, '->load() returns a FileStorage pointing to the absolute path of the template'); + $logger = $this->getMock('Psr\Log\LoggerInterface'); + $logger->expects($this->exactly(2))->method('debug'); + $loader = new ProjectTemplateLoader2($pathPattern); - $loader->setDebugger($debugger = new \Symfony\Component\Templating\Tests\Fixtures\ProjectTemplateDebugger()); + $loader->setLogger($logger); $this->assertFalse($loader->load(new TemplateReference('foo.xml', 'php')), '->load() returns false if the template does not exist for the given engine'); - $this->assertTrue($debugger->hasMessage('Failed loading template'), '->load() logs a "Failed loading template" message if the template is not found'); $loader = new ProjectTemplateLoader2(array(self::$fixturesPath.'/null/%name%', $pathPattern)); - $loader->setDebugger($debugger = new \Symfony\Component\Templating\Tests\Fixtures\ProjectTemplateDebugger()); + $loader->setLogger($logger); $loader->load(new TemplateReference('foo.php', 'php')); - $this->assertTrue($debugger->hasMessage('Loaded template file'), '->load() logs a "Loaded template file" message if the template is found'); } } diff --git a/src/Symfony/Component/Templating/Tests/Loader/LoaderTest.php b/src/Symfony/Component/Templating/Tests/Loader/LoaderTest.php index 5964f596255c2..4be7ea94a7f49 100644 --- a/src/Symfony/Component/Templating/Tests/Loader/LoaderTest.php +++ b/src/Symfony/Component/Templating/Tests/Loader/LoaderTest.php @@ -17,11 +17,20 @@ class LoaderTest extends \PHPUnit_Framework_TestCase { + public function testGetSetLogger() + { + $loader = new ProjectTemplateLoader4(new TemplateNameParser()); + $logger = $this->getMock('Psr\Log\LoggerInterface'); + $loader->setLogger($logger); + $this->assertSame($logger, $loader->getLogger(), '->setLogger() sets the logger instance'); + } + public function testGetSetDebugger() { $loader = new ProjectTemplateLoader4(new TemplateNameParser()); - $loader->setDebugger($debugger = new \Symfony\Component\Templating\Tests\Fixtures\ProjectTemplateDebugger()); - $this->assertTrue($loader->getDebugger() === $debugger, '->setDebugger() sets the debugger instance'); + $debugger = $this->getMock('Symfony\Component\Templating\DebuggerInterface'); + $loader->setDebugger($debugger); + $this->assertSame($debugger, $loader->getDebugger(), '->setDebugger() sets the debugger instance'); } } @@ -31,6 +40,11 @@ public function load(TemplateReferenceInterface $template) { } + public function getLogger() + { + return $this->logger; + } + public function getDebugger() { return $this->debugger; diff --git a/src/Symfony/Component/Templating/Tests/PhpEngineTest.php b/src/Symfony/Component/Templating/Tests/PhpEngineTest.php index 055b1b7173d19..d7fe561656993 100644 --- a/src/Symfony/Component/Templating/Tests/PhpEngineTest.php +++ b/src/Symfony/Component/Templating/Tests/PhpEngineTest.php @@ -116,6 +116,32 @@ public function testExtendRender() $this->assertEquals('bar-foo-', $engine->render('foo.php', array('foo' => 'foo', 'bar' => 'bar')), '->render() supports render() calls in templates'); } + public function testRenderParameter() + { + $engine = new ProjectTemplateEngine(new TemplateNameParser(), $this->loader); + $this->loader->setTemplate('foo.php', ''); + $this->assertEquals('foobar', $engine->render('foo.php', array('template' => 'foo', 'parameters' => 'bar')), '->render() extract variables'); + } + + /** + * @expectedException \InvalidArgumentException + * @dataProvider forbiddenParameterNames + */ + public function testRenderForbiddenParameter($name) + { + $engine = new ProjectTemplateEngine(new TemplateNameParser(), $this->loader); + $this->loader->setTemplate('foo.php', 'bar'); + $engine->render('foo.php', array($name => 'foo')); + } + + public function forbiddenParameterNames() + { + return array( + array('this'), + array('view'), + ); + } + public function testEscape() { $engine = new ProjectTemplateEngine(new TemplateNameParser(), $this->loader); diff --git a/src/Symfony/Component/Templating/composer.json b/src/Symfony/Component/Templating/composer.json index b27f56c2f18bf..d1dd60d15a2ed 100644 --- a/src/Symfony/Component/Templating/composer.json +++ b/src/Symfony/Component/Templating/composer.json @@ -18,6 +18,12 @@ "require": { "php": ">=5.3.3" }, + "require-dev": { + "psr/log": "~1.0" + }, + "suggest": { + "psr/log": "For using debug logging in loaders" + }, "autoload": { "psr-0": { "Symfony\\Component\\Templating\\": "" } }, @@ -25,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Translation/Dumper/FileDumper.php b/src/Symfony/Component/Translation/Dumper/FileDumper.php index 63c1e6c3eb37c..ffe6017720c93 100644 --- a/src/Symfony/Component/Translation/Dumper/FileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/FileDumper.php @@ -30,7 +30,7 @@ abstract class FileDumper implements DumperInterface public function dump(MessageCatalogue $messages, $options = array()) { if (!array_key_exists('path', $options)) { - throw new \InvalidArgumentException('The file dumper need a path options.'); + throw new \InvalidArgumentException('The file dumper needs a path option.'); } // save a file for each domain diff --git a/src/Symfony/Component/Translation/Dumper/JsonFileDumper.php b/src/Symfony/Component/Translation/Dumper/JsonFileDumper.php new file mode 100644 index 0000000000000..762392b406d42 --- /dev/null +++ b/src/Symfony/Component/Translation/Dumper/JsonFileDumper.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\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +if (!defined('JSON_PRETTY_PRINT')) { + define('JSON_PRETTY_PRINT', 128); +} + +/** + * JsonFileDumper generates an json formatted string representation of a message catalogue. + * + * @author singles + */ +class JsonFileDumper extends FileDumper +{ + /** + * {@inheritDoc} + */ + public function format(MessageCatalogue $messages, $domain = 'messages') + { + return json_encode($messages->all($domain), JSON_PRETTY_PRINT); + } + + /** + * {@inheritDoc} + */ + protected function getExtension() + { + return 'json'; + } +} diff --git a/src/Symfony/Component/Translation/IdentityTranslator.php b/src/Symfony/Component/Translation/IdentityTranslator.php index 8564aa7b3c6c2..9c9212e803050 100644 --- a/src/Symfony/Component/Translation/IdentityTranslator.php +++ b/src/Symfony/Component/Translation/IdentityTranslator.php @@ -26,13 +26,13 @@ class IdentityTranslator implements TranslatorInterface /** * Constructor. * - * @param MessageSelector $selector The message selector for pluralization + * @param MessageSelector|null $selector The message selector for pluralization * * @api */ - public function __construct(MessageSelector $selector) + public function __construct(MessageSelector $selector = null) { - $this->selector = $selector; + $this->selector = $selector ?: new MessageSelector(); } /** @@ -60,7 +60,7 @@ public function getLocale() * * @api */ - public function trans($id, array $parameters = array(), $domain = 'messages', $locale = null) + public function trans($id, array $parameters = array(), $domain = null, $locale = null) { return strtr((string) $id, $parameters); } @@ -70,7 +70,7 @@ public function trans($id, array $parameters = array(), $domain = 'messages', $l * * @api */ - public function transChoice($id, $number, array $parameters = array(), $domain = 'messages', $locale = null) + public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) { return strtr($this->selector->choose((string) $id, (int) $number, $locale ?: $this->getLocale()), $parameters); } diff --git a/src/Symfony/Component/Translation/Loader/IniFileLoader.php b/src/Symfony/Component/Translation/Loader/IniFileLoader.php index 616fa7e0e775f..b4907280397a2 100644 --- a/src/Symfony/Component/Translation/Loader/IniFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/IniFileLoader.php @@ -35,7 +35,7 @@ public function load($resource, $locale, $domain = 'messages') throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } - $messages = parse_ini_file($resource, true); + $messages = parse_ini_file($resource, true, INI_SCANNER_RAW); $catalogue = parent::load($messages, $locale, $domain); $catalogue->addResource(new FileResource($resource)); diff --git a/src/Symfony/Component/Translation/Loader/JsonFileLoader.php b/src/Symfony/Component/Translation/Loader/JsonFileLoader.php new file mode 100644 index 0000000000000..4d3613b03f711 --- /dev/null +++ b/src/Symfony/Component/Translation/Loader/JsonFileLoader.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\Translation\Loader; + +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Config\Resource\FileResource; + +/** + * JsonFileLoader loads translations from an json file. + * + * @author singles + */ +class JsonFileLoader extends ArrayLoader implements LoaderInterface +{ + /** + * {@inheritdoc} + */ + public function load($resource, $locale, $domain = 'messages') + { + if (!stream_is_local($resource)) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource)) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + $messages = json_decode(file_get_contents($resource), true); + + if (0 < $errorCode = json_last_error()) { + throw new InvalidResourceException(sprintf('Error parsing JSON - %s', $this->getJSONErrorMessage($errorCode))); + } + + if (null === $messages) { + $messages = array(); + } + + $catalogue = parent::load($messages, $locale, $domain); + $catalogue->addResource(new FileResource($resource)); + + return $catalogue; + } + + /** + * Translates JSON_ERROR_* constant into meaningful message. + * + * @param integer $errorCode Error code returned by json_last_error() call + * + * @return string Message string + */ + private function getJSONErrorMessage($errorCode) + { + switch ($errorCode) { + case JSON_ERROR_DEPTH: + return 'Maximum stack depth exceeded'; + case JSON_ERROR_STATE_MISMATCH: + return 'Underflow or the modes mismatch'; + case JSON_ERROR_CTRL_CHAR: + return 'Unexpected control character found'; + case JSON_ERROR_SYNTAX: + return 'Syntax error, malformed JSON'; + case JSON_ERROR_UTF8: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + default: + return 'Unknown error'; + } + } +} diff --git a/src/Symfony/Component/Translation/PluralizationRules.php b/src/Symfony/Component/Translation/PluralizationRules.php index 719de4fd0a87f..e1ff10360a652 100644 --- a/src/Symfony/Component/Translation/PluralizationRules.php +++ b/src/Symfony/Component/Translation/PluralizationRules.php @@ -18,7 +18,6 @@ */ class PluralizationRules { - // @codeCoverageIgnoreStart private static $rules = array(); /** @@ -31,9 +30,9 @@ class PluralizationRules */ public static function get($number, $locale) { - if ("pt_BR" == $locale) { + if ('pt_BR' === $locale) { // temporary set a locale for brazilian - $locale = "xbr"; + $locale = 'xbr'; } if (strlen($locale) > 3) { @@ -197,9 +196,9 @@ public static function get($number, $locale) */ public static function set($rule, $locale) { - if ("pt_BR" == $locale) { + if ('pt_BR' === $locale) { // temporary set a locale for brazilian - $locale = "xbr"; + $locale = 'xbr'; } if (strlen($locale) > 3) { @@ -212,6 +211,4 @@ public static function set($rule, $locale) self::$rules[$locale] = $rule; } - - // @codeCoverageIgnoreEnd } diff --git a/src/Symfony/Component/Translation/README.md b/src/Symfony/Component/Translation/README.md index 87349048b7605..641db8733c3c3 100644 --- a/src/Symfony/Component/Translation/README.md +++ b/src/Symfony/Component/Translation/README.md @@ -26,7 +26,7 @@ https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/TranslationServic Documentation: -http://symfony.com/doc/2.3/book/translation.html +http://symfony.com/doc/2.5/book/translation.html You can run the unit tests with the following command: diff --git a/src/Symfony/Component/Translation/Tests/Dumper/JsonFileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/JsonFileDumperTest.php new file mode 100644 index 0000000000000..acd2087fdbc75 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Dumper/JsonFileDumperTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Dumper\JsonFileDumper; + +class JsonFileDumperTest extends \PHPUnit_Framework_TestCase +{ + public function testDump() + { + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + $this->markTestIncomplete('PHP below 5.4 doesn\'t support JSON pretty printing'); + } + + $catalogue = new MessageCatalogue('en'); + $catalogue->add(array('foo' => 'bar')); + + $tempDir = sys_get_temp_dir(); + $dumper = new JsonFileDumper(); + $dumper->dump($catalogue, array('path' => $tempDir)); + + $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.json'), file_get_contents($tempDir.'/messages.en.json')); + + unlink($tempDir.'/messages.en.json'); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php index e4e68e02b3a11..3c68ade753114 100644 --- a/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php +++ b/src/Symfony/Component/Translation/Tests/Dumper/YamlFileDumperTest.php @@ -16,13 +16,6 @@ class YamlFileDumperTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Yaml\Yaml')) { - $this->markTestSkipped('The "Yaml" component is not available'); - } - } - public function testDump() { $catalogue = new MessageCatalogue('en'); diff --git a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php index cec702a1b99b6..3a0decd3b2572 100644 --- a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Translation\Tests; use Symfony\Component\Translation\IdentityTranslator; -use Symfony\Component\Translation\MessageSelector; class IdentityTranslatorTest extends \PHPUnit_Framework_TestCase { @@ -21,7 +20,7 @@ class IdentityTranslatorTest extends \PHPUnit_Framework_TestCase */ public function testTrans($expected, $id, $parameters) { - $translator = new IdentityTranslator(new MessageSelector()); + $translator = new IdentityTranslator(); $this->assertEquals($expected, $translator->trans($id, $parameters)); } @@ -31,7 +30,7 @@ public function testTrans($expected, $id, $parameters) */ public function testTransChoiceWithExplicitLocale($expected, $id, $number, $parameters) { - $translator = new IdentityTranslator(new MessageSelector()); + $translator = new IdentityTranslator(); $translator->setLocale('en'); $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); @@ -44,14 +43,14 @@ public function testTransChoiceWithDefaultLocale($expected, $id, $number, $param { \Locale::setDefault('en'); - $translator = new IdentityTranslator(new MessageSelector()); + $translator = new IdentityTranslator(); $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); } public function testGetSetLocale() { - $translator = new IdentityTranslator(new MessageSelector()); + $translator = new IdentityTranslator(); $translator->setLocale('en'); $this->assertEquals('en', $translator->getLocale()); @@ -59,7 +58,7 @@ public function testGetSetLocale() public function testGetLocaleReturnsDefaultLocaleIfNotSet() { - $translator = new IdentityTranslator(new MessageSelector()); + $translator = new IdentityTranslator(); \Locale::setDefault('en'); $this->assertEquals('en', $translator->getLocale()); diff --git a/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php index 59569a3d7091f..463d3b50816f6 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/CsvFileLoaderTest.php @@ -16,13 +16,6 @@ class CsvFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testLoad() { $loader = new CsvFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php index a3bd67abaaa4f..ea9643d682b3e 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/IcuDatFileLoaderTest.php @@ -18,10 +18,6 @@ class IcuDatFileLoaderTest extends LocalizedTestCase { protected function setUp() { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - if (!extension_loaded('intl')) { $this->markTestSkipped('This test requires intl extension to work.'); } diff --git a/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php index 233e189783c7c..1a935c0404d32 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/IcuResFileLoaderTest.php @@ -18,10 +18,6 @@ class IcuResFileLoaderTest extends LocalizedTestCase { protected function setUp() { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - if (!extension_loaded('intl')) { $this->markTestSkipped('This test requires intl extension to work.'); } diff --git a/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php index ae1289d0861d1..1a5de0ed58d69 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/IniFileLoaderTest.php @@ -16,13 +16,6 @@ class IniFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testLoad() { $loader = new IniFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/JsonFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/JsonFileLoaderTest.php new file mode 100644 index 0000000000000..6d4f353bf7dde --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Loader/JsonFileLoaderTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests\Loader; + +use Symfony\Component\Translation\Loader\JsonFileLoader; +use Symfony\Component\Config\Resource\FileResource; + +class JsonFileLoaderTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + if (!class_exists('Symfony\Component\Config\Loader\Loader')) { + $this->markTestSkipped('The "Config" component is not available'); + } + } + + public function testLoad() + { + $loader = new JsonFileLoader(); + $resource = __DIR__.'/../fixtures/resources.json'; + $catalogue = $loader->load($resource, 'en', 'domain1'); + + $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1')); + $this->assertEquals('en', $catalogue->getLocale()); + $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); + } + + public function testLoadDoesNothingIfEmpty() + { + $loader = new JsonFileLoader(); + $resource = __DIR__.'/../fixtures/empty.json'; + $catalogue = $loader->load($resource, 'en', 'domain1'); + + $this->assertEquals(array(), $catalogue->all('domain1')); + $this->assertEquals('en', $catalogue->getLocale()); + $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources()); + } + + /** + * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException + */ + public function testLoadNonExistingResource() + { + $loader = new JsonFileLoader(); + $resource = __DIR__.'/../fixtures/non-existing.json'; + $loader->load($resource, 'en', 'domain1'); + } + + /** + * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException + * @expectedExceptionMessage Error parsing JSON - Syntax error, malformed JSON + */ + public function testParseException() + { + $loader = new JsonFileLoader(); + $resource = __DIR__.'/../fixtures/malformed.json'; + $loader->load($resource, 'en', 'domain1'); + } +} diff --git a/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php index c2616bda6a417..d568e4530b30b 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/MoFileLoaderTest.php @@ -16,13 +16,6 @@ class MoFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testLoad() { $loader = new MoFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php index 5dfe837deaa68..0816b0f8549d1 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/PhpFileLoaderTest.php @@ -16,13 +16,6 @@ class PhpFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testLoad() { $loader = new PhpFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php index cd3d85a1a14bc..dc4123c527a83 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/PoFileLoaderTest.php @@ -16,13 +16,6 @@ class PoFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testLoad() { $loader = new PoFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php index c1dd7b1058a23..71338fdd0f6da 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/QtFileLoaderTest.php @@ -16,13 +16,6 @@ class QtFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testLoad() { $loader = new QtFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php index 1d58e0ee0fb2e..da54169ef8c56 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/XliffFileLoaderTest.php @@ -16,13 +16,6 @@ class XliffFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - } - public function testLoad() { $loader = new XliffFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php index 511b1279746b9..00f7163468d55 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php @@ -16,17 +16,6 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - - if (!class_exists('Symfony\Component\Yaml\Yaml')) { - $this->markTestSkipped('The "Yaml" component is not available'); - } - } - public function testLoad() { $loader = new YamlFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php index 344e39b253668..7d956553d98c6 100644 --- a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php +++ b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php @@ -82,10 +82,6 @@ public function testReplace() public function testAddCatalogue() { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $r = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); $r->expects($this->any())->method('__toString')->will($this->returnValue('r')); @@ -108,10 +104,6 @@ public function testAddCatalogue() public function testAddFallbackCatalogue() { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $r = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); $r->expects($this->any())->method('__toString')->will($this->returnValue('r')); @@ -155,10 +147,6 @@ public function testAddCatalogueWhenLocaleIsNotTheSameAsTheCurrentOne() public function testGetAddResource() { - if (!class_exists('Symfony\Component\Config\Loader\Loader')) { - $this->markTestSkipped('The "Config" component is not available'); - } - $catalogue = new MessageCatalogue('en'); $r = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); $r->expects($this->any())->method('__toString')->will($this->returnValue('r')); diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index e45ab3b4819fb..f36e6c863d32a 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -12,14 +12,13 @@ namespace Symfony\Component\Translation\Tests; use Symfony\Component\Translation\Translator; -use Symfony\Component\Translation\MessageSelector; use Symfony\Component\Translation\Loader\ArrayLoader; class TranslatorTest extends \PHPUnit_Framework_TestCase { public function testSetGetLocale() { - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $this->assertEquals('en', $translator->getLocale()); @@ -29,7 +28,7 @@ public function testSetGetLocale() public function testSetFallbackLocales() { - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('foo' => 'foofoo'), 'en'); $translator->addResource('array', array('bar' => 'foobar'), 'fr'); @@ -43,7 +42,7 @@ public function testSetFallbackLocales() public function testSetFallbackLocalesMultiple() { - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('foo' => 'foo (en)'), 'en'); $translator->addResource('array', array('bar' => 'bar (fr)'), 'fr'); @@ -57,7 +56,7 @@ public function testSetFallbackLocalesMultiple() public function testTransWithFallbackLocale() { - $translator = new Translator('fr_FR', new MessageSelector()); + $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('foo' => 'foofoo'), 'en_US'); $translator->addResource('array', array('bar' => 'foobar'), 'en'); @@ -69,7 +68,7 @@ public function testTransWithFallbackLocale() public function testAddResourceAfterTrans() { - $translator = new Translator('fr', new MessageSelector()); + $translator = new Translator('fr'); $translator->addLoader('array', new ArrayLoader()); $translator->setFallbackLocale(array('en')); @@ -88,7 +87,7 @@ public function testAddResourceAfterTrans() public function testTransWithoutFallbackLocaleFile($format, $loader) { $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader; - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $translator->addLoader($format, new $loaderClass()); $translator->addResource($format, __DIR__.'/fixtures/non-existing', 'en'); $translator->addResource($format, __DIR__.'/fixtures/resources.'.$format, 'en'); @@ -103,7 +102,7 @@ public function testTransWithoutFallbackLocaleFile($format, $loader) public function testTransWithFallbackLocaleFile($format, $loader) { $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader; - $translator = new Translator('en_GB', new MessageSelector()); + $translator = new Translator('en_GB'); $translator->addLoader($format, new $loaderClass()); $translator->addResource($format, __DIR__.'/fixtures/non-existing', 'en_GB'); $translator->addResource($format, __DIR__.'/fixtures/resources.'.$format, 'en', 'resources'); @@ -113,7 +112,7 @@ public function testTransWithFallbackLocaleFile($format, $loader) public function testTransWithFallbackLocaleBis() { - $translator = new Translator('en_US', new MessageSelector()); + $translator = new Translator('en_US'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('foo' => 'foofoo'), 'en_US'); $translator->addResource('array', array('bar' => 'foobar'), 'en'); @@ -122,7 +121,7 @@ public function testTransWithFallbackLocaleBis() public function testTransWithFallbackLocaleTer() { - $translator = new Translator('fr_FR', new MessageSelector()); + $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('foo' => 'foo (en_US)'), 'en_US'); $translator->addResource('array', array('bar' => 'bar (en)'), 'en'); @@ -135,7 +134,7 @@ public function testTransWithFallbackLocaleTer() public function testTransNonExistentWithFallback() { - $translator = new Translator('fr', new MessageSelector()); + $translator = new Translator('fr'); $translator->setFallbackLocales(array('en')); $translator->addLoader('array', new ArrayLoader()); $this->assertEquals('non-existent', $translator->trans('non-existent')); @@ -146,7 +145,7 @@ public function testTransNonExistentWithFallback() */ public function testWhenAResourceHasNoRegisteredLoader() { - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $translator->addResource('array', array('foo' => 'foofoo'), 'en'); $translator->trans('foo'); @@ -157,7 +156,7 @@ public function testWhenAResourceHasNoRegisteredLoader() */ public function testTrans($expected, $id, $translation, $parameters, $locale, $domain) { - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array((string) $id => $translation), $locale, $domain); @@ -169,7 +168,7 @@ public function testTrans($expected, $id, $translation, $parameters, $locale, $d */ public function testFlattenedTrans($expected, $messages, $id) { - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', $messages, 'fr', ''); @@ -181,7 +180,7 @@ public function testFlattenedTrans($expected, $messages, $id) */ public function testTransChoice($expected, $id, $translation, $number, $parameters, $locale, $domain) { - $translator = new Translator('en', new MessageSelector()); + $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array((string) $id => $translation), $locale, $domain); @@ -199,6 +198,7 @@ public function getTransFileTests() array('ts', 'QtFileLoader'), array('xlf', 'XliffFileLoader'), array('yml', 'YamlFileLoader'), + array('json', 'JsonFileLoader'), ); } @@ -259,7 +259,7 @@ public function getTransChoiceTests() public function testTransChoiceFallback() { - $translator = new Translator('ru', new MessageSelector()); + $translator = new Translator('ru'); $translator->setFallbackLocales(array('en')); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('some_message2' => 'one thing|%count% things'), 'en'); @@ -269,7 +269,7 @@ public function testTransChoiceFallback() public function testTransChoiceFallbackBis() { - $translator = new Translator('ru', new MessageSelector()); + $translator = new Translator('ru'); $translator->setFallbackLocales(array('en_US', 'en')); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('some_message2' => 'one thing|%count% things'), 'en_US'); @@ -279,7 +279,7 @@ public function testTransChoiceFallbackBis() public function testTransChoiceFallbackWithNoTranslation() { - $translator = new Translator('ru', new MessageSelector()); + $translator = new Translator('ru'); $translator->setFallbackLocales(array('en')); $translator->addLoader('array', new ArrayLoader()); diff --git a/src/Symfony/Component/Translation/Tests/fixtures/empty.json b/src/Symfony/Component/Translation/Tests/fixtures/empty.json new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Symfony/Component/Translation/Tests/fixtures/malformed.json b/src/Symfony/Component/Translation/Tests/fixtures/malformed.json new file mode 100644 index 0000000000000..1ee47ffc05c6c --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/malformed.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" " +} \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Tests/fixtures/resources.json b/src/Symfony/Component/Translation/Tests/fixtures/resources.json new file mode 100644 index 0000000000000..8a79687628fe8 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/resources.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} \ No newline at end of file diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 8e74b79f60836..808bf710b041b 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -222,6 +222,16 @@ public function transChoice($id, $number, array $parameters = array(), $domain = return strtr($this->selector->choose($catalogue->get($id, $domain), (int) $number, $locale), $parameters); } + /** + * Gets the loaders. + * + * @return array LoaderInterface[] + */ + protected function getLoaders() + { + return $this->loaders; + } + protected function loadCatalogue($locale) { try { diff --git a/src/Symfony/Component/Translation/TranslatorInterface.php b/src/Symfony/Component/Translation/TranslatorInterface.php index 3dcdd4fc375bc..8d12341fc13ef 100644 --- a/src/Symfony/Component/Translation/TranslatorInterface.php +++ b/src/Symfony/Component/Translation/TranslatorInterface.php @@ -23,10 +23,10 @@ interface TranslatorInterface /** * Translates the given message. * - * @param string $id The message id (may also be an object that can be cast to string) - * @param array $parameters An array of parameters for the message - * @param string $domain The domain for the message - * @param string $locale The locale + * @param string $id The message id (may also be an object that can be cast to string) + * @param array $parameters An array of parameters for the message + * @param string|null $domain The domain for the message or null to use the default + * @param string|null $locale The locale or null to use the default * * @return string The translated string * @@ -37,11 +37,11 @@ public function trans($id, array $parameters = array(), $domain = null, $locale /** * Translates the given choice message by choosing a translation according to a number. * - * @param string $id The message id (may also be an object that can be cast to string) - * @param integer $number The number to use to find the indice of the message - * @param array $parameters An array of parameters for the message - * @param string $domain The domain for the message - * @param string $locale The locale + * @param string $id The message id (may also be an object that can be cast to string) + * @param integer $number The number to use to find the indice of the message + * @param array $parameters An array of parameters for the message + * @param string|null $domain The domain for the message or null to use the default + * @param string|null $locale The locale or null to use the default * * @return string The translated string * diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 96a8e662a8ad7..37fef30451385 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -33,7 +33,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 7ab8051c1a92b..3eba52930f3f7 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -1,6 +1,18 @@ CHANGELOG ========= +2.5.0 +----- + + * deprecated `ApcCache` in favor of `DoctrineCache` + * added `DoctrineCache` to adapt any Doctrine cache + +2.4.0 +----- + + * added a constraint the uses the expression language + * added `minRatio`, `maxRatio`, `allowSquare`, `allowLandscape`, and `allowPortrait` to Image validator + 2.3.0 ----- diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php index f2d018e3fbef5..0f2c226ae9f77 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -87,8 +87,9 @@ public function __construct($options = null) $invalidOptions = array(); $missingOptions = array_flip((array) $this->getRequiredOptions()); - if (is_array($options) && count($options) == 1 && isset($options['value'])) { - $options = $options['value']; + if (is_array($options) && count($options) >= 1 && isset($options['value']) && !property_exists($this, 'value')) { + $options[$this->getDefaultOption()] = $options['value']; + unset($options['value']); } if (is_array($options) && count($options) > 0 && is_string(key($options))) { diff --git a/src/Symfony/Component/Validator/ConstraintValidatorFactory.php b/src/Symfony/Component/Validator/ConstraintValidatorFactory.php index 683252f949158..5cf36eccffc3a 100644 --- a/src/Symfony/Component/Validator/ConstraintValidatorFactory.php +++ b/src/Symfony/Component/Validator/ConstraintValidatorFactory.php @@ -11,17 +11,33 @@ namespace Symfony\Component\Validator; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Validator\Constraints\ExpressionValidator; + /** * Default implementation of the ConstraintValidatorFactoryInterface. * * This enforces the convention that the validatedBy() method on any * Constraint will return the class name of the ConstraintValidator that * should validate the Constraint. + * + * @author Bernhard Schussek */ class ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface { protected $validators = array(); + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + /** * {@inheritDoc} */ @@ -29,8 +45,19 @@ public function getInstance(Constraint $constraint) { $className = $constraint->validatedBy(); - if (!isset($this->validators[$className]) || $className === 'Symfony\Component\Validator\Constraints\CollectionValidator') { - $this->validators[$className] = new $className(); + // The second condition is a hack that is needed when CollectionValidator + // calls itself recursively (Collection constraints can be nested). + // Since the context of the validator is overwritten when initialize() + // is called for the nested constraint, the outer validator is + // acting on the wrong context when the nested validation terminates. + // + // A better solution - which should be approached in Symfony 3.0 - is to + // remove the initialize() method and pass the context as last argument + // to validate() instead. + if (!isset($this->validators[$className]) || 'Symfony\Component\Validator\Constraints\CollectionValidator' === $className) { + $this->validators[$className] = 'validator.expression' === $className + ? new ExpressionValidator($this->propertyAccessor) + : new $className(); } return $this->validators[$className]; diff --git a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php index 6b76fc80b2a21..f6b919331fae7 100644 --- a/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php +++ b/src/Symfony/Component/Validator/Constraints/AbstractComparisonValidator.php @@ -60,6 +60,10 @@ private function valueToType($value) */ private function valueToString($value) { + if (is_object($value) && method_exists($value, '__toString')) { + return (string) $value; + } + if ($value instanceof \DateTime) { return $value->format('Y-m-d H:i:s'); } diff --git a/src/Symfony/Component/Validator/Constraints/Callback.php b/src/Symfony/Component/Validator/Constraints/Callback.php index e93efa4b99145..01aeb6ddb7982 100644 --- a/src/Symfony/Component/Validator/Constraints/Callback.php +++ b/src/Symfony/Component/Validator/Constraints/Callback.php @@ -22,26 +22,52 @@ */ class Callback extends Constraint { + /** + * @var string|callable + * + * @since 2.4 + */ + public $callback; + + /** + * @var array + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. + */ public $methods; /** - * {@inheritDoc} + * {@inheritdoc} */ - public function getRequiredOptions() + public function __construct($options = null) { - return array('methods'); + // Invocation through annotations with an array parameter only + if (is_array($options) && 1 === count($options) && isset($options['value'])) { + $options = $options['value']; + } + + if (is_array($options) && !isset($options['callback']) && !isset($options['methods']) && !isset($options['groups'])) { + if (is_callable($options)) { + $options = array('callback' => $options); + } else { + // BC with Symfony < 2.4 + $options = array('methods' => $options); + } + } + + parent::__construct($options); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getDefaultOption() { - return 'methods'; + return 'callback'; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getTargets() { diff --git a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php index ab3c56f98e2c9..28b34250af6fc 100644 --- a/src/Symfony/Component/Validator/Constraints/CallbackValidator.php +++ b/src/Symfony/Component/Validator/Constraints/CallbackValidator.php @@ -34,13 +34,20 @@ public function validate($object, Constraint $constraint) return; } + if (null !== $constraint->callback && null !== $constraint->methods) { + throw new ConstraintDefinitionException( + 'The Callback constraint supports either the option "callback" ' . + 'or "methods", but not both at the same time.' + ); + } + // has to be an array so that we can differentiate between callables // and method names - if (!is_array($constraint->methods)) { + if (null !== $constraint->methods && !is_array($constraint->methods)) { throw new UnexpectedTypeException($constraint->methods, 'array'); } - $methods = $constraint->methods; + $methods = $constraint->methods ?: array($constraint->callback); foreach ($methods as $method) { if (is_array($method) || $method instanceof \Closure) { @@ -54,7 +61,13 @@ public function validate($object, Constraint $constraint) throw new ConstraintDefinitionException(sprintf('Method "%s" targeted by Callback constraint does not exist', $method)); } - $object->$method($this->context); + $reflMethod = new \ReflectionMethod($object, $method); + + if ($reflMethod->isStatic()) { + $reflMethod->invoke(null, $object, $this->context); + } else { + $reflMethod->invoke($object, $this->context); + } } } } diff --git a/src/Symfony/Component/Validator/Constraints/Collection.php b/src/Symfony/Component/Validator/Constraints/Collection.php index 92afd7d29b129..78fa5aee9dd53 100644 --- a/src/Symfony/Component/Validator/Constraints/Collection.php +++ b/src/Symfony/Component/Validator/Constraints/Collection.php @@ -23,7 +23,7 @@ */ class Collection extends Constraint { - public $fields; + public $fields = array(); public $allowExtraFields = false; public $allowMissingFields = false; public $extraFieldsMessage = 'This field was not expected.'; diff --git a/src/Symfony/Component/Validator/Constraints/EmailValidator.php b/src/Symfony/Component/Validator/Constraints/EmailValidator.php index abe45a87d06ff..e0593102bc599 100644 --- a/src/Symfony/Component/Validator/Constraints/EmailValidator.php +++ b/src/Symfony/Component/Validator/Constraints/EmailValidator.php @@ -41,11 +41,6 @@ public function validate($value, Constraint $constraint) if ($valid) { $host = substr($value, strpos($value, '@') + 1); - if (version_compare(PHP_VERSION, '5.3.3', '<') && strpos($host, '.') === false) { - // Likely not a FQDN, bug in PHP FILTER_VALIDATE_EMAIL prior to PHP 5.3.3 - $valid = false; - } - // Check for host DNS resource records if ($valid && $constraint->checkMX) { $valid = $this->checkMX($host); diff --git a/src/Symfony/Component/Validator/Constraints/Expression.php b/src/Symfony/Component/Validator/Constraints/Expression.php new file mode 100644 index 0000000000000..b845a32392b2c --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Expression.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * @Annotation + * + * @author Fabien Potencier + * @author Bernhard Schussek + */ +class Expression extends Constraint +{ + public $message = 'This value is not valid.'; + public $expression; + + /** + * {@inheritDoc} + */ + public function getDefaultOption() + { + return 'expression'; + } + + /** + * {@inheritDoc} + */ + public function getRequiredOptions() + { + return array('expression'); + } + + /** + * {@inheritDoc} + */ + public function getTargets() + { + return array(self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT); + } + + /** + * {@inheritDoc} + */ + public function validatedBy() + { + return 'validator.expression'; + } +} diff --git a/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php new file mode 100644 index 0000000000000..e27859b08c19c --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/ExpressionValidator.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Constraints; + +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Validator\Exception\RuntimeException; + +/** + * @author Fabien Potencier + * @author Bernhard Schussek + */ +class ExpressionValidator extends ConstraintValidator +{ + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ExpressionLanguage + */ + private $expressionLanguage; + + public function __construct(PropertyAccessorInterface $propertyAccessor) + { + $this->propertyAccessor = $propertyAccessor; + } + + /** + * {@inheritDoc} + */ + public function validate($value, Constraint $constraint) + { + if (null === $value || '' === $value) { + return; + } + + $variables = array(); + + if (null === $this->context->getPropertyName()) { + $variables['this'] = $value; + } else { + // Extract the object that the property belongs to from the object + // graph + $path = new PropertyPath($this->context->getPropertyPath()); + $parentPath = $path->getParent(); + $root = $this->context->getRoot(); + + $variables['value'] = $value; + $variables['this'] = $parentPath ? $this->propertyAccessor->getValue($root, $parentPath) : $root; + } + + if (!$this->getExpressionLanguage()->evaluate($constraint->expression, $variables)) { + $this->context->addViolation($constraint->message); + } + } + + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } +} diff --git a/src/Symfony/Component/Validator/Constraints/Image.php b/src/Symfony/Component/Validator/Constraints/Image.php index a23106489f354..9fa8725c6da15 100644 --- a/src/Symfony/Component/Validator/Constraints/Image.php +++ b/src/Symfony/Component/Validator/Constraints/Image.php @@ -23,6 +23,11 @@ class Image extends File public $maxWidth = null; public $maxHeight = null; public $minHeight = null; + public $maxRatio = null; + public $minRatio = null; + public $allowSquare = true; + public $allowLandscape = true; + public $allowPortrait = true; public $mimeTypesMessage = 'This file is not a valid image.'; public $sizeNotDetectedMessage = 'The size of the image could not be detected.'; @@ -30,4 +35,9 @@ class Image extends File public $minWidthMessage = 'The image width is too small ({{ width }}px). Minimum width expected is {{ min_width }}px.'; public $maxHeightMessage = 'The image height is too big ({{ height }}px). Allowed maximum height is {{ max_height }}px.'; public $minHeightMessage = 'The image height is too small ({{ height }}px). Minimum height expected is {{ min_height }}px.'; + public $maxRatioMessage = 'The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}.'; + public $minRatioMessage = 'The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}.'; + public $allowSquareMessage = 'The image is square ({{ width }}x{{ height }}px). Square images are not allowed.'; + public $allowLandscapeMessage = 'The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed.'; + public $allowPortraitMessage = 'The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed.'; } diff --git a/src/Symfony/Component/Validator/Constraints/ImageValidator.php b/src/Symfony/Component/Validator/Constraints/ImageValidator.php index 79e6bdcddbbb1..76ce8767fcea8 100644 --- a/src/Symfony/Component/Validator/Constraints/ImageValidator.php +++ b/src/Symfony/Component/Validator/Constraints/ImageValidator.php @@ -38,7 +38,9 @@ public function validate($value, Constraint $constraint) } if (null === $constraint->minWidth && null === $constraint->maxWidth - && null === $constraint->minHeight && null === $constraint->maxHeight) { + && null === $constraint->minHeight && null === $constraint->maxHeight + && null === $constraint->minRatio && null === $constraint->maxRatio + && $constraint->allowSquare && $constraint->allowLandscape && $constraint->allowPortrait) { return; } @@ -109,5 +111,55 @@ public function validate($value, Constraint $constraint) )); } } + + $ratio = $width / $height; + + if (null !== $constraint->minRatio) { + if (!is_numeric((string) $constraint->minRatio)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid minimum ratio', $constraint->minRatio)); + } + + if ($ratio < $constraint->minRatio) { + $this->context->addViolation($constraint->minRatioMessage, array( + '{{ ratio }}' => $ratio, + '{{ min_ratio }}' => $constraint->minRatio + )); + } + } + + if (null !== $constraint->maxRatio) { + if (!is_numeric((string) $constraint->maxRatio)) { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum ratio', $constraint->maxRatio)); + } + + if ($ratio > $constraint->maxRatio) { + $this->context->addViolation($constraint->maxRatioMessage, array( + '{{ ratio }}' => $ratio, + '{{ max_ratio }}' => $constraint->maxRatio + )); + } + } + + if (!$constraint->allowSquare && $width == $height) { + $this->context->addViolation($constraint->allowSquareMessage, array( + '{{ width }}' => $width, + '{{ height }}' => $height + )); + } + + if (!$constraint->allowLandscape && $width > $height) { + $this->context->addViolation($constraint->allowLandscapeMessage, array( + '{{ width }}' => $width, + '{{ height }}' => $height + )); + } + + if (!$constraint->allowPortrait && $width < $height) { + $this->context->addViolation($constraint->allowPortraitMessage, array( + '{{ width }}' => $width, + '{{ height }}' => $height + )); + } + } } diff --git a/src/Symfony/Component/Validator/Exception/RuntimeException.php b/src/Symfony/Component/Validator/Exception/RuntimeException.php new file mode 100644 index 0000000000000..df4a50c474338 --- /dev/null +++ b/src/Symfony/Component/Validator/Exception/RuntimeException.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\Validator\Exception; + +/** + * Base RuntimeException for the Validator component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Validator/Mapping/Cache/ApcCache.php b/src/Symfony/Component/Validator/Mapping/Cache/ApcCache.php index 226fab36e0c34..64929b09663ab 100644 --- a/src/Symfony/Component/Validator/Mapping/Cache/ApcCache.php +++ b/src/Symfony/Component/Validator/Mapping/Cache/ApcCache.php @@ -13,6 +13,10 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; +/** + * @deprecated Deprecated since version 2.5, to be removed in 3.0. + * Use DoctrineCache with Doctrine\Common\Cache\ApcCache instead. + */ class ApcCache implements CacheInterface { private $prefix; diff --git a/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php b/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php new file mode 100644 index 0000000000000..56ead5d0ccc05 --- /dev/null +++ b/src/Symfony/Component/Validator/Mapping/Cache/DoctrineCache.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Mapping\Cache; + +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Doctrine\Common\Cache\Cache; + +/** + * Adapts a Doctrine cache to a CacheInterface. + * + * @author Florian Voutzinos + */ +final class DoctrineCache implements CacheInterface +{ + private $cache; + + /** + * Creates a new Doctrine cache. + * + * @param Cache $cache The cache to adapt + */ + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + /** + * Sets the cache to adapt. + * + * @param Cache $cache The cache to adapt + */ + public function setCache(Cache $cache) + { + $this->cache = $cache; + } + + /** + * {@inheritdoc} + */ + public function has($class) + { + return $this->cache->contains($class); + } + + /** + * {@inheritdoc} + */ + public function read($class) + { + return $this->cache->fetch($class); + } + + /** + * {@inheritdoc} + */ + public function write(ClassMetadata $metadata) + { + $this->cache->save($metadata->getClassName(), $metadata); + } +} diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php index 18a7690080cf1..9b5093e1bd928 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AbstractLoader.php @@ -20,7 +20,7 @@ abstract class AbstractLoader implements LoaderInterface * Contains all known namespaces indexed by their prefix * @var array */ - protected $namespaces; + protected $namespaces = array(); /** * Adds a namespace alias. diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php index 0e7e89b548c77..10745c72e7ffc 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Mapping\Loader; use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; @@ -63,7 +64,12 @@ public function loadClassMetadata(ClassMetadata $metadata) foreach ($reflClass->getMethods() as $method) { if ($method->getDeclaringClass()->name == $className) { foreach ($this->reader->getMethodAnnotations($method) as $constraint) { - if ($constraint instanceof Constraint) { + if ($constraint instanceof Callback) { + $constraint->callback = $method->getName(); + $constraint->methods = null; + + $metadata->addConstraint($constraint); + } elseif ($constraint instanceof Constraint) { if (preg_match('/^(get|is)(.+)$/i', $method->name, $matches)) { $metadata->addGetterConstraint(lcfirst($matches[2]), $constraint); } else { diff --git a/src/Symfony/Component/Validator/README.md b/src/Symfony/Component/Validator/README.md index ec3784b00bcec..6825a2feee198 100644 --- a/src/Symfony/Component/Validator/README.md +++ b/src/Symfony/Component/Validator/README.md @@ -107,7 +107,7 @@ https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/ValidatorServiceP Documentation: -http://symfony.com/doc/2.3/book/validation.html +http://symfony.com/doc/2.5/book/validation.html JSR-303 Specification: diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf index b2263dadc829b..bd76d2c16be01 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf @@ -68,7 +68,7 @@ The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}. - A fájl mime típusa érvénytelen ({{ type }}). Az engedélyezett mime típusok: {{ types }}. + A fájl MIME típusa érvénytelen ({{ type }}). Az engedélyezett MIME típusok: {{ types }}. This value should be {{ limit }} or less. @@ -80,7 +80,7 @@ This value should be {{ limit }} or more. - Ez az érték legalább {{ limit }} kell legyen. + Ez az érték legalább {{ limit }} kell, hogy legyen. This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more. @@ -252,23 +252,23 @@ This value should be greater than {{ compared_value }}. - Ez az érték nagyobb legyen mint {{ compared_value }}. + Ez az érték nagyobb legyen, mint {{ compared_value }}. This value should be greater than or equal to {{ compared_value }}. - Ez az érték nagyobb vagy egyenlő legyen mint {{ compared_value }}. + Ez az érték nagyobb vagy egyenlő legyen, mint {{ compared_value }}. This value should be identical to {{ compared_value_type }} {{ compared_value }}. - Ez az érték ugyanolyan legyen mint {{ compared_value_type }} {{ compared_value }}. + Ez az érték ugyanolyan legyen, mint {{ compared_value_type }} {{ compared_value }}. This value should be less than {{ compared_value }}. - Ez az érték kisebb legyen mint {{ compared_value }}. + Ez az érték kisebb legyen, mint {{ compared_value }}. This value should be less than or equal to {{ compared_value }}. - Ez az érték kisebb vagy egyenlő legyen mint {{ compared_value }}. + Ez az érték kisebb vagy egyenlő legyen, mint {{ compared_value }}. This value should not be equal to {{ compared_value }}. @@ -276,7 +276,7 @@ This value should not be identical to {{ compared_value_type }} {{ compared_value }}. - Ez az érték ne legyen ugyanolyan mint {{ compared_value_type }} {{ compared_value }}. + Ez az érték ne legyen ugyanolyan, mint {{ compared_value_type }} {{ compared_value }}. diff --git a/src/Symfony/Component/Validator/Tests/ConstraintTest.php b/src/Symfony/Component/Validator/Tests/ConstraintTest.php index 007bb1e3a23e9..015a6dab665e2 100644 --- a/src/Symfony/Component/Validator/Tests/ConstraintTest.php +++ b/src/Symfony/Component/Validator/Tests/ConstraintTest.php @@ -15,6 +15,8 @@ use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintC; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithValue; +use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithValueAsDefault; class ConstraintTest extends \PHPUnit_Framework_TestCase { @@ -71,6 +73,30 @@ public function testSetDefaultPropertyDoctrineStyle() $this->assertEquals('foo', $constraint->property2); } + public function testSetDefaultPropertyDoctrineStylePlusOtherProperty() + { + $constraint = new ConstraintA(array('value' => 'foo', 'property1' => 'bar')); + + $this->assertEquals('foo', $constraint->property2); + $this->assertEquals('bar', $constraint->property1); + } + + public function testSetDefaultPropertyDoctrineStyleWhenDefaultPropertyIsNamedValue() + { + $constraint = new ConstraintWithValueAsDefault(array('value' => 'foo')); + + $this->assertEquals('foo', $constraint->value); + $this->assertNull($constraint->property); + } + + public function testDontSetDefaultPropertyIfValuePropertyExists() + { + $constraint = new ConstraintWithValue(array('value' => 'foo')); + + $this->assertEquals('foo', $constraint->value); + $this->assertNull($constraint->property); + } + public function testSetUndefinedDefaultProperty() { $this->setExpectedException('Symfony\Component\Validator\Exception\ConstraintDefinitionException'); @@ -117,14 +143,14 @@ public function testCanCreateConstraintWithNoDefaultOptionAndEmptyArray() public function testGetTargetsCanBeString() { - $constraint = new ClassConstraint; + $constraint = new ClassConstraint(); $this->assertEquals('class', $constraint->getTargets()); } public function testGetTargetsCanBeArray() { - $constraint = new ConstraintA; + $constraint = new ConstraintA(); $this->assertEquals(array('property', 'class'), $constraint->getTargets()); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php index d72eaf23fabd3..36405e3de88e5 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AbstractComparisonValidatorTestCase.php @@ -14,6 +14,21 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\AbstractComparisonValidator; +class ComparisonTest_Class +{ + protected $value; + + public function __construct($value) + { + $this->value = $value; + } + + public function __toString() + { + return (string) $this->value; + } +} + /** * @author Daniel Holmes */ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php index 4d248c13c8409..cdcd49bb58ed8 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CallbackValidatorTest.php @@ -17,9 +17,9 @@ class CallbackValidatorTest_Class { - public static function validateStatic($object, ExecutionContext $context) + public static function validateCallback($object, ExecutionContext $context) { - $context->addViolation('Static message', array('{{ value }}' => 'foobar'), 'invalidValue'); + $context->addViolation('Callback message', array('{{ value }}' => 'foobar'), 'invalidValue'); return false; } @@ -27,16 +27,16 @@ public static function validateStatic($object, ExecutionContext $context) class CallbackValidatorTest_Object { - public function validateOne(ExecutionContext $context) + public function validate(ExecutionContext $context) { $context->addViolation('My message', array('{{ value }}' => 'foobar'), 'invalidValue'); return false; } - public function validateTwo(ExecutionContext $context) + public static function validateStatic($object, ExecutionContext $context) { - $context->addViolation('Other message', array('{{ value }}' => 'baz'), 'otherInvalidValue'); + $context->addViolation('Static message', array('{{ value }}' => 'baz'), 'otherInvalidValue'); return false; } @@ -68,10 +68,10 @@ public function testNullIsValid() $this->validator->validate(null, new Callback(array('foo'))); } - public function testCallbackSingleMethod() + public function testSingleMethod() { $object = new CallbackValidatorTest_Object(); - $constraint = new Callback(array('validateOne')); + $constraint = new Callback('validate'); $this->context->expects($this->once()) ->method('addViolation') @@ -82,24 +82,137 @@ public function testCallbackSingleMethod() $this->validator->validate($object, $constraint); } - public function testCallbackSingleStaticMethod() + public function testSingleMethodExplicitName() { $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array('callback' => 'validate')); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('My message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); + } + + public function testSingleStaticMethod() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback('validateStatic'); $this->context->expects($this->once()) ->method('addViolation') ->with('Static message', array( + '{{ value }}' => 'baz', + )); + + $this->validator->validate($object, $constraint); + } + + public function testClosure() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(function ($object, ExecutionContext $context) { + $context->addViolation('My message', array('{{ value }}' => 'foobar'), 'invalidValue'); + + return false; + }); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('My message', array( '{{ value }}' => 'foobar', )); - $this->validator->validate($object, new Callback(array( - array(__CLASS__.'_Class', 'validateStatic') - ))); + $this->validator->validate($object, $constraint); + } + + public function testClosureExplicitName() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array( + 'callback' => function ($object, ExecutionContext $context) { + $context->addViolation('My message', array('{{ value }}' => 'foobar'), 'invalidValue'); + + return false; + }, + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('My message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); + } + + public function testArrayCallable() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array(__CLASS__.'_Class', 'validateCallback')); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('Callback message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); + } + + public function testArrayCallableExplicitName() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array( + 'callback' => array(__CLASS__.'_Class', 'validateCallback'), + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('Callback message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); + } + + // BC with Symfony < 2.4 + public function testSingleMethodBc() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array('validate')); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('My message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); } - public function testCallbackMultipleMethods() + // BC with Symfony < 2.4 + public function testSingleMethodBcExplicitName() { $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array('methods' => array('validate'))); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('My message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); + } + + // BC with Symfony < 2.4 + public function testMultipleMethodsBc() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array('validate', 'validateStatic')); $this->context->expects($this->at(0)) ->method('addViolation') @@ -108,23 +221,67 @@ public function testCallbackMultipleMethods() )); $this->context->expects($this->at(1)) ->method('addViolation') - ->with('Other message', array( + ->with('Static message', array( '{{ value }}' => 'baz', )); - $this->validator->validate($object, new Callback(array( - 'validateOne', 'validateTwo' - ))); + $this->validator->validate($object, $constraint); } - /** - * @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException - */ - public function testExpectCallbackArray() + // BC with Symfony < 2.4 + public function testMultipleMethodsBcExplicitName() { $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array( + 'methods' => array('validate', 'validateStatic'), + )); - $this->validator->validate($object, new Callback('foobar')); + $this->context->expects($this->at(0)) + ->method('addViolation') + ->with('My message', array( + '{{ value }}' => 'foobar', + )); + $this->context->expects($this->at(1)) + ->method('addViolation') + ->with('Static message', array( + '{{ value }}' => 'baz', + )); + + $this->validator->validate($object, $constraint); + } + + // BC with Symfony < 2.4 + public function testSingleStaticMethodBc() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array( + array(__CLASS__.'_Class', 'validateCallback') + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('Callback message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); + } + + // BC with Symfony < 2.4 + public function testSingleStaticMethodBcExplicitName() + { + $object = new CallbackValidatorTest_Object(); + $constraint = new Callback(array( + 'methods' => array(array(__CLASS__.'_Class', 'validateCallback')), + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('Callback message', array( + '{{ value }}' => 'foobar', + )); + + $this->validator->validate($object, $constraint); } /** @@ -147,10 +304,43 @@ public function testExpectValidCallbacks() $this->validator->validate($object, new Callback(array(array('foo', 'bar')))); } + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testExpectEitherCallbackOrMethods() + { + $object = new CallbackValidatorTest_Object(); + + $this->validator->validate($object, new Callback(array( + 'callback' => 'validate', + 'methods' => array('validateStatic'), + ))); + } + public function testConstraintGetTargets() { $constraint = new Callback(array('foo')); $this->assertEquals('class', $constraint->getTargets()); } + + // Should succeed. Needed when defining constraints as annotations. + public function testNoConstructorArguments() + { + new Callback(); + } + + public function testAnnotationInvocationSingleValued() + { + $constraint = new Callback(array('value' => 'validateStatic')); + + $this->assertEquals(new Callback('validateStatic'), $constraint); + } + + public function testAnnotationInvocationMultiValued() + { + $constraint = new Callback(array('value' => array(__CLASS__.'_Class', 'validateCallback'))); + + $this->assertEquals(new Callback(array(__CLASS__.'_Class', 'validateCallback')), $constraint); + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php index 90befab262f3f..6f8abc1012509 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/EqualToValidatorTest.php @@ -39,6 +39,7 @@ public function provideValidComparisons() array(3, '3'), array('a', 'a'), array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01')), + array(new ComparisonTest_Class(5), new ComparisonTest_Class(5)), array(null, 1), ); } @@ -51,7 +52,8 @@ public function provideInvalidComparisons() return array( array(1, 2, '2', 'integer'), array('22', '333', "'333'", 'string'), - array(new \DateTime('2001-01-01'), new \DateTime('2000-01-01'), '2000-01-01 00:00:00', 'DateTime') + array(new \DateTime('2001-01-01'), new \DateTime('2000-01-01'), '2000-01-01 00:00:00', 'DateTime'), + array(new ComparisonTest_Class(4), new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'), ); } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.php new file mode 100644 index 0000000000000..b71138e5f6bbd --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/ExpressionValidatorTest.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\Validator\Tests\Constraints; + +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\Validator\Constraints\Expression; +use Symfony\Component\Validator\Constraints\ExpressionValidator; + +class ExpressionValidatorTest extends \PHPUnit_Framework_TestCase +{ + protected $context; + protected $validator; + + protected function setUp() + { + $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); + $this->validator = new ExpressionValidator(PropertyAccess::createPropertyAccessor()); + $this->validator->initialize($this->context); + + $this->context->expects($this->any()) + ->method('getClassName') + ->will($this->returnValue(__CLASS__)); + } + + protected function tearDown() + { + $this->context = null; + $this->validator = null; + } + + public function testNullIsValid() + { + $this->context->expects($this->never()) + ->method('addViolation'); + + $this->validator->validate(null, new Expression('value == 1')); + } + + public function testEmptyStringIsValid() + { + $this->context->expects($this->never()) + ->method('addViolation'); + + $this->validator->validate('', new Expression('value == 1')); + } + + public function testSucceedingExpressionAtObjectLevel() + { + $constraint = new Expression('this.property == 1'); + + $object = (object) array('property' => '1'); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue(null)); + + $this->context->expects($this->never()) + ->method('addViolation'); + + $this->validator->validate($object, $constraint); + } + + public function testFailingExpressionAtObjectLevel() + { + $constraint = new Expression(array( + 'expression' => 'this.property == 1', + 'message' => 'myMessage', + )); + + $object = (object) array('property' => '2'); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue(null)); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage'); + + $this->validator->validate($object, $constraint); + } + + public function testSucceedingExpressionAtPropertyLevel() + { + $constraint = new Expression('value == this.expected'); + + $object = (object) array('expected' => '1'); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue($object)); + + $this->context->expects($this->never()) + ->method('addViolation'); + + $this->validator->validate('1', $constraint); + } + + public function testFailingExpressionAtPropertyLevel() + { + $constraint = new Expression(array( + 'expression' => 'value == this.expected', + 'message' => 'myMessage', + )); + + $object = (object) array('expected' => '1'); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue($object)); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage'); + + $this->validator->validate('2', $constraint); + } + + public function testSucceedingExpressionAtNestedPropertyLevel() + { + $constraint = new Expression('value == this.expected'); + + $object = (object) array('expected' => '1'); + $root = (object) array('nested' => $object); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue('nested.property')); + + $this->context->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue($root)); + + $this->context->expects($this->never()) + ->method('addViolation'); + + $this->validator->validate('1', $constraint); + } + + public function testFailingExpressionAtNestedPropertyLevel() + { + $constraint = new Expression(array( + 'expression' => 'value == this.expected', + 'message' => 'myMessage', + )); + + $object = (object) array('expected' => '1'); + $root = (object) array('nested' => $object); + + $this->context->expects($this->any()) + ->method('getPropertyName') + ->will($this->returnValue('property')); + + $this->context->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue('nested.property')); + + $this->context->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue($root)); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage'); + + $this->validator->validate('2', $constraint); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php index 0ca98067d324d..0927aedacdd5c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/FileValidatorTest.php @@ -24,10 +24,6 @@ abstract class FileValidatorTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\HttpFoundation\File\UploadedFile')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $this->context = $this->getMock('Symfony\Component\Validator\ExecutionContext', array(), array(), '', false); $this->validator = new FileValidator(); $this->validator->initialize($this->context); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_landscape.gif b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_landscape.gif new file mode 100644 index 0000000000000..870123532c3b9 Binary files /dev/null and b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_landscape.gif differ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_portrait.gif b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_portrait.gif new file mode 100644 index 0000000000000..cc480ca88ed7f Binary files /dev/null and b/src/Symfony/Component/Validator/Tests/Constraints/Fixtures/test_portrait.gif differ diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php index 72087fab83e34..1fc5311cf5332 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/GreaterThanValidatorTest.php @@ -37,6 +37,7 @@ public function provideValidComparisons() return array( array(2, 1), array(new \DateTime('2005/01/01'), new \DateTime('2001/01/01')), + array(new ComparisonTest_Class(5), new ComparisonTest_Class(4)), array('333', '22'), array(null, 1), ); @@ -52,6 +53,8 @@ public function provideInvalidComparisons() array(2, 2, '2', 'integer'), array(new \DateTime('2000/01/01'), new \DateTime('2005/01/01'), '2005-01-01 00:00:00', 'DateTime'), array(new \DateTime('2000/01/01'), new \DateTime('2000/01/01'), '2000-01-01 00:00:00', 'DateTime'), + array(new ComparisonTest_Class(4), new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'), + array(new ComparisonTest_Class(5), new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'), array('22', '333', "'333'", 'string'), array('22', '22', "'22'", 'string') ); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php index 63d8666335b1f..39234239c0958 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/IdenticalToValidatorTest.php @@ -35,11 +35,13 @@ protected function createConstraint(array $options) public function provideValidComparisons() { $date = new \DateTime('2000-01-01'); + $object = new ComparisonTest_Class(2); return array( array(3, 3), array('a', 'a'), array($date, $date), + array($object, $object), array(null, 1), ); } @@ -54,7 +56,8 @@ public function provideInvalidComparisons() array(2, '2', "'2'", 'string'), array('22', '333', "'333'", 'string'), array(new \DateTime('2001-01-01'), new \DateTime('2001-01-01'), '2001-01-01 00:00:00', 'DateTime'), - array(new \DateTime('2001-01-01'), new \DateTime('1999-01-01'), '1999-01-01 00:00:00', 'DateTime') + array(new \DateTime('2001-01-01'), new \DateTime('1999-01-01'), '1999-01-01 00:00:00', 'DateTime'), + array(new ComparisonTest_Class(4), new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'), ); } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php index 88545016243d1..114c2d2f0499e 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/ImageValidatorTest.php @@ -20,6 +20,8 @@ class ImageValidatorTest extends \PHPUnit_Framework_TestCase protected $validator; protected $path; protected $image; + protected $imageLandscape; + protected $imagePortrait; protected function setUp() { @@ -27,6 +29,8 @@ protected function setUp() $this->validator = new ImageValidator(); $this->validator->initialize($this->context); $this->image = __DIR__.'/Fixtures/test.gif'; + $this->imageLandscape = __DIR__.'/Fixtures/test_landscape.gif'; + $this->imagePortrait = __DIR__.'/Fixtures/test_portrait.gif'; } public function testNullIsValid() @@ -47,10 +51,6 @@ public function testEmptyStringIsValid() public function testValidImage() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $this->context->expects($this->never()) ->method('addViolation'); @@ -59,10 +59,6 @@ public function testValidImage() public function testValidSize() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $this->context->expects($this->never()) ->method('addViolation'); @@ -78,10 +74,6 @@ public function testValidSize() public function testWidthTooSmall() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'minWidth' => 3, 'minWidthMessage' => 'myMessage', @@ -99,10 +91,6 @@ public function testWidthTooSmall() public function testWidthTooBig() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'maxWidth' => 1, 'maxWidthMessage' => 'myMessage', @@ -120,10 +108,6 @@ public function testWidthTooBig() public function testHeightTooSmall() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'minHeight' => 3, 'minHeightMessage' => 'myMessage', @@ -141,10 +125,6 @@ public function testHeightTooSmall() public function testHeightTooBig() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'maxHeight' => 1, 'maxHeightMessage' => 'myMessage', @@ -165,10 +145,6 @@ public function testHeightTooBig() */ public function testInvalidMinWidth() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'minWidth' => '1abc', )); @@ -181,10 +157,6 @@ public function testInvalidMinWidth() */ public function testInvalidMaxWidth() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'maxWidth' => '1abc', )); @@ -197,10 +169,6 @@ public function testInvalidMaxWidth() */ public function testInvalidMinHeight() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'minHeight' => '1abc', )); @@ -213,14 +181,119 @@ public function testInvalidMinHeight() */ public function testInvalidMaxHeight() { - if (!class_exists('Symfony\Component\HttpFoundation\File\File')) { - $this->markTestSkipped('The "HttpFoundation" component is not available'); - } - $constraint = new Image(array( 'maxHeight' => '1abc', )); $this->validator->validate($this->image, $constraint); } + + public function testRatioTooSmall() + { + $constraint = new Image(array( + 'minRatio' => 2, + 'minRatioMessage' => 'myMessage', + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage', array( + '{{ ratio }}' => 1, + '{{ min_ratio }}' => 2, + )); + + $this->validator->validate($this->image, $constraint); + } + + public function testRatioTooBig() + { + $constraint = new Image(array( + 'maxRatio' => 0.5, + 'maxRatioMessage' => 'myMessage', + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage', array( + '{{ ratio }}' => 1, + '{{ max_ratio }}' => 0.5, + )); + + $this->validator->validate($this->image, $constraint); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testInvalidMinRatio() + { + $constraint = new Image(array( + 'minRatio' => '1abc', + )); + + $this->validator->validate($this->image, $constraint); + } + + /** + * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException + */ + public function testInvalidMaxRatio() + { + $constraint = new Image(array( + 'maxRatio' => '1abc', + )); + + $this->validator->validate($this->image, $constraint); + } + + public function testSquareNotAllowed() + { + $constraint = new Image(array( + 'allowSquare' => false, + 'allowSquareMessage' => 'myMessage', + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage', array( + '{{ width }}' => 2, + '{{ height }}' => 2, + )); + + $this->validator->validate($this->image, $constraint); + } + + public function testLandscapeNotAllowed() + { + $constraint = new Image(array( + 'allowLandscape' => false, + 'allowLandscapeMessage' => 'myMessage', + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage', array( + '{{ width }}' => 2, + '{{ height }}' => 1, + )); + + $this->validator->validate($this->imageLandscape, $constraint); + } + + public function testPortraitNotAllowed() + { + $constraint = new Image(array( + 'allowPortrait' => false, + 'allowPortraitMessage' => 'myMessage', + )); + + $this->context->expects($this->once()) + ->method('addViolation') + ->with('myMessage', array( + '{{ width }}' => 1, + '{{ height }}' => 2, + )); + + $this->validator->validate($this->imagePortrait, $constraint); + } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php index 406e222119e26..24ad0faf75b05 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanOrEqualValidatorTest.php @@ -39,6 +39,8 @@ public function provideValidComparisons() array(1, 1), array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01')), array(new \DateTime('2000-01-01'), new \DateTime('2020-01-01')), + array(new ComparisonTest_Class(4), new ComparisonTest_Class(5)), + array(new ComparisonTest_Class(5), new ComparisonTest_Class(5)), array('a', 'a'), array('a', 'z'), array(null, 1), @@ -53,6 +55,7 @@ public function provideInvalidComparisons() return array( array(2, 1, '1', 'integer'), array(new \DateTime('2010-01-01'), new \DateTime('2000-01-01'), '2000-01-01 00:00:00', 'DateTime'), + array(new ComparisonTest_Class(5), new ComparisonTest_Class(4), '4', __NAMESPACE__.'\ComparisonTest_Class'), array('c', 'b', "'b'", 'string') ); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php index b8f88a87cc52b..3b001926e9018 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/LessThanValidatorTest.php @@ -37,6 +37,7 @@ public function provideValidComparisons() return array( array(1, 2), array(new \DateTime('2000-01-01'), new \DateTime('2010-01-01')), + array(new ComparisonTest_Class(4), new ComparisonTest_Class(5)), array('22', '333'), array(null, 1), ); @@ -52,6 +53,8 @@ public function provideInvalidComparisons() array(2, 2, '2', 'integer'), array(new \DateTime('2010-01-01'), new \DateTime('2000-01-01'), '2000-01-01 00:00:00', 'DateTime'), array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01'), '2000-01-01 00:00:00', 'DateTime'), + array(new ComparisonTest_Class(5), new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'), + array(new ComparisonTest_Class(6), new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'), array('333', '22', "'22'", 'string'), ); } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php index 80a86a39b9b83..dcf46a668d81e 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotEqualToValidatorTest.php @@ -38,6 +38,7 @@ public function provideValidComparisons() array(1, 2), array('22', '333'), array(new \DateTime('2001-01-01'), new \DateTime('2000-01-01')), + array(new ComparisonTest_Class(6), new ComparisonTest_Class(5)), array(null, 1), ); } @@ -51,7 +52,8 @@ public function provideInvalidComparisons() array(3, 3, '3', 'integer'), array('2', 2, '2', 'integer'), array('a', 'a', "'a'", 'string'), - array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01'), '2000-01-01 00:00:00', 'DateTime') + array(new \DateTime('2000-01-01'), new \DateTime('2000-01-01'), '2000-01-01 00:00:00', 'DateTime'), + array(new ComparisonTest_Class(5), new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'), ); } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php index fe43078503670..28026c08745c7 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/NotIdenticalToValidatorTest.php @@ -50,11 +50,13 @@ public function provideValidComparisons() public function provideInvalidComparisons() { $date = new \DateTime('2000-01-01'); + $object = new ComparisonTest_Class(2); return array( array(3, 3, '3', 'integer'), array('a', 'a', "'a'", 'string'), - array($date, $date, '2000-01-01 00:00:00', 'DateTime') + array($date, $date, '2000-01-01 00:00:00', 'DateTime'), + array($object, $object, '2', __NAMESPACE__.'\ComparisonTest_Class'), ); } } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CallbackClass.php b/src/Symfony/Component/Validator/Tests/Fixtures/CallbackClass.php new file mode 100644 index 0000000000000..0f6a2f4ae3d9f --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CallbackClass.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\ExecutionContextInterface; + +/** + * @author Bernhard Schussek + */ +class CallbackClass +{ + public static function callback($object, ExecutionContextInterface $context) + { + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithValue.php b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithValue.php new file mode 100644 index 0000000000000..4ebd981eef924 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithValue.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\Constraint; + +/** @Annotation */ +class ConstraintWithValue extends Constraint +{ + public $property; + public $value; + + public function getDefaultOption() + { + return 'property'; + } + + public function getTargets() + { + return array(self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithValueAsDefault.php b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithValueAsDefault.php new file mode 100644 index 0000000000000..a975e0787fc97 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/ConstraintWithValueAsDefault.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\Constraint; + +/** @Annotation */ +class ConstraintWithValueAsDefault extends Constraint +{ + public $property; + public $value; + + public function getDefaultOption() + { + return 'value'; + } + + public function getTargets() + { + return array(self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php index e1cb3e04902ce..70bdc5aec6998 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/Entity.php @@ -12,10 +12,12 @@ namespace Symfony\Component\Validator\Tests\Fixtures; use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\ExecutionContextInterface; /** * @Symfony\Component\Validator\Tests\Fixtures\ConstraintA * @Assert\GroupSequence({"Foo", "Entity"}) + * @Assert\Callback({"Symfony\Component\Validator\Tests\Fixtures\CallbackClass", "callback"}) */ class Entity extends EntityParent implements EntityInterface { @@ -58,4 +60,18 @@ public function getData() { return 'Overridden data'; } + + /** + * @Assert\Callback + */ + public function validateMe(ExecutionContextInterface $context) + { + } + + /** + * @Assert\Callback + */ + public static function validateMeStatic($object, ExecutionContextInterface $context) + { + } } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php new file mode 100644 index 0000000000000..df2d9f4104ce8 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Mapping/Cache/DoctrineCacheTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Mapping\Cache; + +use Symfony\Component\Validator\Mapping\Cache\DoctrineCache; +use Doctrine\Common\Cache\ArrayCache; + +class DoctrineCacheTest extends \PHPUnit_Framework_TestCase +{ + private $cache; + + public function testWrite() + { + $meta = $this->getMockBuilder('Symfony\\Component\\Validator\\Mapping\\ClassMetadata') + ->disableOriginalConstructor() + ->setMethods(array('getClassName')) + ->getMock(); + + $meta->expects($this->once()) + ->method('getClassName') + ->will($this->returnValue('bar')); + + $this->cache->write($meta); + + $this->assertInstanceOf( + 'Symfony\\Component\\Validator\\Mapping\\ClassMetadata', + $this->cache->read('bar'), + 'write() stores metadata' + ); + } + + public function testHas() + { + $meta = $this->getMockBuilder('Symfony\\Component\\Validator\\Mapping\\ClassMetadata') + ->disableOriginalConstructor() + ->setMethods(array('getClassName')) + ->getMock(); + + $meta->expects($this->once()) + ->method('getClassName') + ->will($this->returnValue('bar')); + + $this->assertFalse($this->cache->has('bar'), 'has() returns false when there is no entry'); + + $this->cache->write($meta); + $this->assertTrue($this->cache->has('bar'), 'has() returns true when the is an entry'); + } + + public function testRead() + { + $meta = $this->getMockBuilder('Symfony\\Component\\Validator\\Mapping\\ClassMetadata') + ->disableOriginalConstructor() + ->setMethods(array('getClassName')) + ->getMock(); + + $meta->expects($this->once()) + ->method('getClassName') + ->will($this->returnValue('bar')); + + $this->assertFalse($this->cache->read('bar'), 'read() returns false when there is no entry'); + + $this->cache->write($meta); + + $this->assertInstanceOf( + 'Symfony\\Component\\Validator\\Mapping\\ClassMetadata', + $this->cache->read('bar'), + 'read() returns metadata' + ); + } + + protected function setUp() + { + $this->cache = new DoctrineCache(new ArrayCache); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php index 22a39c582e780..0d255b8fcad0d 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -13,6 +13,7 @@ use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\Validator\Constraints\All; +use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; @@ -23,13 +24,6 @@ class AnnotationLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Doctrine\Common\Annotations\AnnotationReader')) { - $this->markTestSkipped('The "Doctrine Common" library is not available'); - } - } - public function testLoadClassMetadataReturnsTrueIfSuccessful() { $reader = new AnnotationReader(); @@ -57,6 +51,9 @@ public function testLoadClassMetadata() $expected = new ClassMetadata('Symfony\Component\Validator\Tests\Fixtures\Entity'); $expected->setGroupSequence(array('Foo', 'Entity')); $expected->addConstraint(new ConstraintA()); + $expected->addConstraint(new Callback(array('Symfony\Component\Validator\Tests\Fixtures\CallbackClass', 'callback'))); + $expected->addConstraint(new Callback('validateMe')); + $expected->addConstraint(new Callback('validateMeStatic')); $expected->addPropertyConstraint('firstName', new NotNull()); $expected->addPropertyConstraint('firstName', new Range(array('min' => 3))); $expected->addPropertyConstraint('firstName', new All(array(new NotNull(), new Range(array('min' => 3))))); @@ -121,6 +118,9 @@ public function testLoadClassMetadataAndMerge() $expected->setGroupSequence(array('Foo', 'Entity')); $expected->addConstraint(new ConstraintA()); + $expected->addConstraint(new Callback(array('Symfony\Component\Validator\Tests\Fixtures\CallbackClass', 'callback'))); + $expected->addConstraint(new Callback('validateMe')); + $expected->addConstraint(new Callback('validateMeStatic')); $expected->addPropertyConstraint('firstName', new NotNull()); $expected->addPropertyConstraint('firstName', new Range(array('min' => 3))); $expected->addPropertyConstraint('firstName', new All(array(new NotNull(), new Range(array('min' => 3))))); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php index 7c6e355bd171f..82195405e6614 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; use Symfony\Component\Validator\Constraints\All; +use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; @@ -51,6 +52,9 @@ public function testLoadClassMetadata() $expected->setGroupSequence(array('Foo', 'Entity')); $expected->addConstraint(new ConstraintA()); $expected->addConstraint(new ConstraintB()); + $expected->addConstraint(new Callback('validateMe')); + $expected->addConstraint(new Callback('validateMeStatic')); + $expected->addConstraint(new Callback(array('Symfony\Component\Validator\Tests\Fixtures\CallbackClass', 'callback'))); $expected->addPropertyConstraint('firstName', new NotNull()); $expected->addPropertyConstraint('firstName', new Range(array('min' => 3))); $expected->addPropertyConstraint('firstName', new Choice(array('A', 'B'))); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php index 9e31cbb37d22e..0d9a0b62c3122 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; use Symfony\Component\Validator\Constraints\All; +use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Collection; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Range; @@ -23,13 +24,6 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase { - protected function setUp() - { - if (!class_exists('Symfony\Component\Yaml\Yaml')) { - $this->markTestSkipped('The "Yaml" component is not available'); - } - } - public function testLoadClassMetadataReturnsFalseIfEmpty() { $loader = new YamlFileLoader(__DIR__.'/empty-mapping.yml'); @@ -75,6 +69,9 @@ public function testLoadClassMetadata() $expected->setGroupSequence(array('Foo', 'Entity')); $expected->addConstraint(new ConstraintA()); $expected->addConstraint(new ConstraintB()); + $expected->addConstraint(new Callback('validateMe')); + $expected->addConstraint(new Callback('validateMeStatic')); + $expected->addConstraint(new Callback(array('Symfony\Component\Validator\Tests\Fixtures\CallbackClass', 'callback'))); $expected->addPropertyConstraint('firstName', new NotNull()); $expected->addPropertyConstraint('firstName', new Range(array('min' => 3))); $expected->addPropertyConstraint('firstName', new Choice(array('A', 'B'))); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml index dfac70d9cf6e2..1eee1cb18036a 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml @@ -21,6 +21,16 @@ + + validateMe + + validateMeStatic + + + Symfony\Component\Validator\Tests\Fixtures\CallbackClass + callback + + diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml index 38188dc5d976a..e52d3f04b2ced 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml @@ -11,6 +11,10 @@ Symfony\Component\Validator\Tests\Fixtures\Entity: - Symfony\Component\Validator\Tests\Fixtures\ConstraintA: ~ # Custom constraint with namespaces prefix - "custom:ConstraintB": ~ + # Callbacks + - Callback: validateMe + - Callback: validateMeStatic + - Callback: [Symfony\Component\Validator\Tests\Fixtures\CallbackClass, callback] properties: firstName: diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index d48d6b1fe4293..900243f31ad65 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -75,10 +75,6 @@ public function testAddMethodMappings() public function testEnableAnnotationMapping() { - if (!class_exists('Doctrine\Common\Annotations\AnnotationReader')) { - $this->markTestSkipped('Annotations is required for this test'); - } - $this->assertSame($this->builder, $this->builder->enableAnnotationMapping()); } diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index a5bfc1fdf63c5..e24a7071662e2 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Validator; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Mapping\ClassMetadataFactory; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; @@ -84,6 +86,11 @@ class ValidatorBuilder implements ValidatorBuilderInterface */ private $translationDomain; + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + /** * {@inheritdoc} */ @@ -198,8 +205,8 @@ public function enableAnnotationMapping(Reader $annotationReader = null) } if (null === $annotationReader) { - if (!class_exists('Doctrine\Common\Annotations\AnnotationReader')) { - throw new \RuntimeException('Requested a ValidatorFactory with an AnnotationLoader, but the AnnotationReader was not found. You should add Doctrine Common to your project.'); + if (!class_exists('Doctrine\Common\Annotations\AnnotationReader') || !class_exists('Doctrine\Common\Cache\ArrayCache')) { + throw new \RuntimeException('Enabling annotation based constraint mapping requires the packages doctrine/annotations and doctrine/cache to be installed.'); } $annotationReader = new CachedReader(new AnnotationReader(), new ArrayCache()); @@ -253,6 +260,10 @@ public function setMetadataCache(CacheInterface $cache) */ public function setConstraintValidatorFactory(ConstraintValidatorFactoryInterface $validatorFactory) { + if (null !== $this->propertyAccessor) { + throw new ValidatorException('You cannot set a validator factory after setting a custom property accessor. Remove the call to setPropertyAccessor() if you want to call setConstraintValidatorFactory().'); + } + $this->validatorFactory = $validatorFactory; return $this; @@ -278,6 +289,20 @@ public function setTranslationDomain($translationDomain) return $this; } + /** + * {@inheritdoc} + */ + public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) + { + if (null !== $this->validatorFactory) { + throw new ValidatorException('You cannot set a property accessor after setting a custom validator factory. Configure your validator factory instead.'); + } + + $this->propertyAccessor = $propertyAccessor; + + return $this; + } + /** * {@inheritdoc} */ @@ -319,7 +344,8 @@ public function getValidator() $metadataFactory = new ClassMetadataFactory($loader, $this->metadataCache); } - $validatorFactory = $this->validatorFactory ?: new ConstraintValidatorFactory(); + $propertyAccessor = $this->propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $validatorFactory = $this->validatorFactory ?: new ConstraintValidatorFactory($propertyAccessor); $translator = $this->translator ?: new DefaultTranslator(); return new Validator($metadataFactory, $validatorFactory, $translator, $this->translationDomain, $this->initializers); diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index 99f367b6aa1ae..92aaca756a3bc 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Mapping\Cache\CacheInterface; use Symfony\Component\Translation\TranslatorInterface; use Doctrine\Common\Annotations\Reader; @@ -159,6 +160,15 @@ public function setTranslator(TranslatorInterface $translator); */ public function setTranslationDomain($translationDomain); + /** + * Sets the property accessor for resolving property paths. + * + * @param PropertyAccessorInterface $propertyAccessor The property accessor. + * + * @return ValidatorBuilderInterface The builder object. + */ + public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor); + /** * Builds and returns a new validator object. * diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 951ef01526e9b..af14eabdb63a3 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -17,16 +17,20 @@ ], "require": { "php": ">=5.3.3", - "symfony/translation": "~2.0" + "symfony/translation": "~2.0", + "symfony/property-access": "~2.2" }, "require-dev": { "symfony/http-foundation": "~2.1", "symfony/intl": "~2.3", "symfony/yaml": "~2.0", - "symfony/config": "~2.2" + "symfony/config": "~2.2", + "doctrine/annotations": "~1.0", + "doctrine/cache": "~1.0" }, "suggest": { - "doctrine/common": "", + "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", + "doctrine/cache": "For using the default cached annotation reader and metadata cache.", "symfony/http-foundation": "", "symfony/intl": "", "symfony/yaml": "", @@ -39,7 +43,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } } diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index af5018f2026f9..dc497ca21c7c8 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -196,7 +196,6 @@ protected function getTestsForDump() '.Inf' => -log(0), '-.Inf' => log(0), "'686e444'" => '686e444', - '.Inf' => 646e444, '"foo\r\nbar"' => "foo\r\nbar", "'foo#bar'" => 'foo#bar', "'foo # bar'" => 'foo # bar', diff --git a/src/Symfony/Component/Yaml/composer.json b/src/Symfony/Component/Yaml/composer.json index 1a009c16d6a27..ceabb0c132ec8 100644 --- a/src/Symfony/Component/Yaml/composer.json +++ b/src/Symfony/Component/Yaml/composer.json @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" } } }