diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7ed7b20e228a..71c775edb8ae 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | "master" for new features / 2.3, 2.7, 2.8 or 3.0 for fixes +| Branch? | "master" for new features / 2.7, 2.8, 3.0 or 3.1 for fixes | Bug fix? | yes/no | New feature? | yes/no | BC breaks? | yes/no diff --git a/.php_cs b/.php_cs index 290e9bf5fbcb..604361481949 100644 --- a/.php_cs +++ b/.php_cs @@ -3,6 +3,10 @@ return Symfony\CS\Config\Config::create() ->setUsingLinter(false) ->setUsingCache(true) + ->fixers(array( + 'long_array_syntax', + 'php_unit_construct', + )) ->finder( Symfony\CS\Finder\DefaultFinder::create() ->in(__DIR__) @@ -12,6 +16,7 @@ return Symfony\CS\Config\Config::create() 'src/Symfony/Component/Routing/Tests/Fixtures/dumper', // fixture templates 'src/Symfony/Component/Templating/Tests/Fixtures/templates', + 'src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom', // resource templates 'src/Symfony/Bundle/FrameworkBundle/Resources/views/Form', )) diff --git a/.travis.yml b/.travis.yml index 3522207883bf..b5a28f71fb36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,12 +12,16 @@ addons: env: global: - - MIN_PHP=5.3.3 + - MIN_PHP=5.3.9 - SYMFONY_PROCESS_PHP_TEST_BINARY=~/.phpenv/versions/5.6/bin/php matrix: include: - - php: hhvm + # Use the newer stack for HHVM as HHVM does not support Precise anymore since a long time and so Precise has an outdated version + - php: hhvm-3.12 + sudo: required + dist: trusty + group: edge - php: 5.3 - php: 5.4 - php: 5.5 @@ -35,21 +39,23 @@ cache: services: mongodb before_install: + - stty cols 120 - PHP=$TRAVIS_PHP_VERSION # Matrix lines for intermediate PHP versions are skipped for pull requests - - if [[ ! $deps && ! $PHP = ${MIN_PHP%.*} && $PHP != hhvm && $TRAVIS_PULL_REQUEST != false ]]; then deps=skip; skip=1; fi + - if [[ ! $deps && ! $PHP = ${MIN_PHP%.*} && ! $PHP = hhvm* && $TRAVIS_PULL_REQUEST != false ]]; then deps=skip; skip=1; fi # A sigchild-enabled-PHP is used to test the Process component on the lowest PHP matrix line - if [[ ! $deps && $PHP = ${MIN_PHP%.*} && ! -d php-$MIN_PHP/sapi ]]; then wget http://museum.php.net/php5/php-$MIN_PHP.tar.bz2 -O - | tar -xj; (cd php-$MIN_PHP; ./configure --enable-sigchild --enable-pcntl; make -j2); fi - - if [[ $PHP != hhvm ]]; then INI_FILE=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; else INI_FILE=/etc/hhvm/php.ini; fi + - if [[ ! $PHP = hhvm* ]]; then INI_FILE=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; else INI_FILE=/etc/hhvm/php.ini; fi - if [[ ! $skip ]]; then echo memory_limit = -1 >> $INI_FILE; fi - if [[ ! $skip ]]; then echo session.gc_probability = 0 >> $INI_FILE; fi - if [[ ! $skip && $PHP = 5.* ]]; then echo extension = mongo.so >> $INI_FILE; fi - if [[ ! $skip && $PHP = 5.* ]]; then echo extension = memcache.so >> $INI_FILE; fi - if [[ ! $skip && $PHP = 5.* ]]; then (echo yes | pecl install -f apcu-4.0.10 && echo apc.enable_cli = 1 >> $INI_FILE); fi - if [[ ! $skip && $PHP = 7.* ]]; then (echo yes | pecl install -f apcu-5.1.2 && echo apc.enable_cli = 1 >> $INI_FILE); fi + - if [[ ! $deps && $PHP = 5.* ]]; then (cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo extension = $(pwd)/modules/symfony_debug.so >> $INI_FILE); fi - if [[ ! $skip && $PHP = 5.* ]]; then pecl install -f memcached-2.1.0; fi - - if [[ ! $skip && $PHP != hhvm ]]; then echo extension = ldap.so >> $INI_FILE; fi - - if [[ ! $skip && $PHP != hhvm ]]; then phpenv config-rm xdebug.ini; fi + - if [[ ! $skip && ! $PHP = hhvm* ]]; then echo extension = ldap.so >> $INI_FILE; fi + - if [[ ! $skip && ! $PHP = hhvm* ]]; then phpenv config-rm xdebug.ini; fi - if [[ ! $skip ]]; then composer self-update --stable; fi - if [[ ! $skip ]]; then cp .composer/* ~/.composer/; fi - if [[ ! $skip ]]; then ./phpunit install; fi @@ -67,12 +73,13 @@ install: - export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev - if [[ ! $deps ]]; then composer update; else export SYMFONY_DEPRECATIONS_HELPER=weak; fi - if [[ $TRAVIS_BRANCH = master ]]; then export SYMFONY_PHPUNIT_OVERLOAD=1; fi - - if [[ $PHP != hhvm ]]; then php -i; else hhvm --php -r 'print_r($_SERVER);print_r(ini_get_all());'; fi + - if [[ ! $PHP = hhvm* ]]; then php -i; else hhvm --php -r 'print_r($_SERVER);print_r(ini_get_all());'; fi script: - if [[ $skip ]]; then echo -e "\\n\\e[1;34mIntermediate PHP version $PHP is skipped for pull requests.\\e[0m"; fi - - if [[ ! $deps ]]; then echo "$COMPONENTS" | parallel --gnu '$PHPUNIT --exclude-group tty,benchmark,intl-data {}'; fi - - if [[ ! $deps ]]; then echo -e "\\nRunning tests requiring tty"; $PHPUNIT --group tty; fi + - if [[ ! $deps && ! $PHP = hhvm* ]]; then echo "$COMPONENTS" | parallel --gnu '$PHPUNIT --exclude-group tty,benchmark,intl-data {}'; fi + - if [[ ! $deps && ! $PHP = hhvm* ]]; then echo -e "\\nRunning tests requiring tty"; $PHPUNIT --group tty; fi + - if [[ ! $deps && $PHP = hhvm* ]]; then $PHPUNIT --exclude-group benchmark,intl-data; fi - if [[ ! $deps && $PHP = ${MIN_PHP%.*} ]]; then echo -e "1\\n0" | xargs -I{} sh -c 'echo "\\nPHP --enable-sigchild enhanced={}" && ENHANCE_SIGCHLD={} php-$MIN_PHP/sapi/cli/php .phpunit/phpunit-4.8/phpunit --colors=always src/Symfony/Component/Process/'; fi - if [[ $deps = high ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --ansi; $PHPUNIT --exclude-group tty,benchmark,intl-data'$LEGACY; fi - if [[ $deps = low ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --ansi --prefer-lowest --prefer-stable; $PHPUNIT --exclude-group tty,benchmark,intl-data'; fi diff --git a/CHANGELOG-2.4.md b/CHANGELOG-2.4.md new file mode 100644 index 000000000000..f1bbdc685396 --- /dev/null +++ b/CHANGELOG-2.4.md @@ -0,0 +1,401 @@ +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.9 (2014-09-03) + + * security #11832 CVE-2014-6072 (fabpot) + * security #11831 CVE-2014-5245 (stof) + * security #11830 CVE-2014-4931 (aitboudad, Jérémy Derussé) + * security #11829 CVE-2014-6061 (damz, fabpot) + * security #11828 CVE-2014-5244 (nicolas-grekas, larowlan) + * bug #10197 [FrameworkBundle] PhpExtractor bugfix and improvements (mtibben) + * bug #11772 [Filesystem] Add FTP stream wrapper context option to enable overwrite (Damian Sromek) + * bug #11788 [Yaml] fixed mapping keys containing a quoted # (hvt, fabpot) + * bug #11160 [DoctrineBridge] Abstract Doctrine Subscribers with tags (merk) + * bug #11768 [ClassLoader] Add a __call() method to XcacheClassLoader (tstoeckler) + * bug #11726 [Filesystem Component] mkdir race condition fix #11626 (kcassam) + * bug #11677 [YAML] resolve variables in inlined YAML (xabbuh) + * bug #11639 [DependencyInjection] Fixed factory service not within the ServiceReferenceGraph. (boekkooi) + * bug #11778 [Validator] Fixed wrong translations for Collection constraints (samicemalone) + * bug #11756 [DependencyInjection] fix @return anno created by PhpDumper (jakubkulhan) + * bug #11711 [DoctrineBridge] Fix empty parameter logging in the dbal logger (jakzal) + * bug #11692 [DomCrawler] check for the correct field type (xabbuh) + * bug #11672 [Routing] fix handling of nullable XML attributes (xabbuh) + * bug #11624 [DomCrawler] fix the axes handling in a bc way (xabbuh) + * bug #11676 [Form] Fixed #11675 ValueToDuplicatesTransformer accept "0" value (Nek-) + * bug #11695 [Validators] Fixed failing tests requiring ICU 52.1 which are skipped otherwise (webmozart) + * bug #11529 [WebProfilerBundle] Fixed double height of canvas (hason) + * bug #11641 [WebProfilerBundle ] Fix toolbar vertical alignment (blaugueux) + * bug #11559 [Validator] Convert objects to string in comparison validators (webmozart) + * feature #11510 [HttpFoundation] MongoDbSessionHandler supports auto expiry via configurable expiry_field (catchamonkey) + * bug #11408 [HttpFoundation] Update QUERY_STRING when overrideGlobals (yguedidi) + * bug #11633 [FrameworkBundle] add missing attribute to XSD (xabbuh) + * bug #11601 [Validator] Allow basic auth in url when using UrlValidator. (blaugueux) + * bug #11609 [Console] fixed style creation when providing an unknown tag option (fabpot) + * bug #10914 [HttpKernel] added an analyze of environment parameters for built-in server (mauchede) + * bug #11598 [Finder] Shell escape and windows support (Gordon Franke, gimler) + * bug #11499 [BrowserKit] Fixed relative redirects for ambiguous paths (pkruithof) + * bug #11516 [BrowserKit] Fix browser kit redirect with ports (dakota) + * bug #11545 [Bundle][FrameworkBundle] built-in server: exit when docroot does not exist (xabbuh) + * bug #11560 Plural fix (1emming) + * bug #11558 [DependencyInjection] Fixed missing 'factory-class' attribute in XmlDumper output (kerdany) + * bug #11548 [Component][DomCrawler] fix axes handling in Crawler::filterXPath() (xabbuh) + * bug #11422 [DependencyInjection] Self-referenced 'service_container' service breaks garbage collection (sun) + * bug #11428 [Serializer] properly handle null data when denormalizing (xabbuh) + * bug #10687 [Validator] Fixed string conversion in constraint violations (eagleoneraptor, webmozart) + * bug #11475 [EventDispatcher] don't count empty listeners (xabbuh) + * bug #11436 fix signal handling in wait() on calls to stop() (xabbuh, romainneutron) + * bug #11469 [BrowserKit] Fixed server HTTP_HOST port uri conversion (bcremer, fabpot) + * bug #11425 Fix issue described in #11421 (Ben, ben-rosio) + * bug #11423 Pass a Scope instance instead of a scope name when cloning a container in the GrahpvizDumper (jakzal) + * bug #11448 [MonologBridge] fixed Console handler priorities (fabpot) + * bug #11120 [Process] Reduce I/O load on Windows platform (romainneutron) + * bug #11342 [Form] Check if IntlDateFormatter constructor returned a valid object before using it (romainneutron) + * bug #11439 [ExpressionLanguage] Fixed double quoted string literals containing sharps (pylebecq) + * bug #11411 [Validator] Backported #11410 to 2.3: Object initializers are called only once per object (webmozart) + * bug #11403 [Translator][FrameworkBundle] Added @ to the list of allowed chars in Translator (takeit) + * bug #11381 [Process] Use correct test for empty string in UnixPipes (whs, romainneutron) + +* 2.4.8 (2014-07-15) + + * [Security] Forced validate of locales passed to the translator + * bug #11278 Remove Spaceless Blocks From Twig Templates (chrisguitarguy) + * feature #11367 [HttpFoundation] Fix to prevent magic bytes injection in JSONP responses... (CVE-2014-4671) (Andrew Moore) + * bug #11386 Remove Spaceless Blocks from Twig Form Templates (chrisguitarguy) + * bug #9719 [TwigBundle] fix configuration tree for paths (mdavis1982, cordoval) + * bug #11244 [HttpFoundation] Remove body-related headers when sending the response, if body is empty (SimonSimCity) + +* 2.4.7 (2014-07-08) + + * bug #11283 [SecurityBundle] Remove Expression Language services when the component is unavailable (thewilkybarkid) + * bug #11238 [Translation] Added unescaping of ids in PoFileLoader (JustBlackBird) + * bug #11194 [DomCrawler] Remove the query string and the anchor of the uri of a link (benja-M-1) + * bug #11272 [Console] Make sure formatter is the same. (akimsko) + * bug #11259 [Config] Fixed failed config schema loads due to libxml_disable_entity_loader usage (ccorliss) + * bug #11234 [ClassLoader] fixed PHP warning on PHP 5.3 (fabpot) + * bug #11179 [Process] Fix ExecutableFinder with open basedir (cs278) + * bug #11242 [CssSelector] Refactored the CssSelector to remove the circular object graph (stof) + * bug #11219 [DomCrawler] properly handle buttons with single and double quotes insid... (xabbuh) + * bug #11220 [Components][Serializer] optional constructor arguments can be omitted during the denormalization process (xabbuh) + * bug #11186 Added missing `break` statement (apfelbox) + * bug #11169 [Console] Fixed notice in DialogHelper (florianv) + * bug #11144 [HttpFoundation] Fixed Request::getPort returns incorrect value under IPv6 (kicken) + * bug #10966 PHP Fatal error when getContainer method of ContainerAwareCommand has be... (kevinvergauwen) + * bug #10981 [HttpFoundation] Fixed isSecure() check to be compliant with the docs (Jannik Zschiesche) + * bug #11092 [HttpFoundation] Fix basic authentication in url with PHP-FPM (Kdecherf) + * bug #10808 [DomCrawler] Empty select with attribute name="foo[]" bug fix (darles) + * bug #11063 [HttpFoundation] fix switch statement (Tobion) + * bug #11009 [HttpFoundation] smaller fixes for PdoSessionHandler (Tobion) + * bug #11041 Remove undefined variable $e (skydiablo) + +* 2.4.6 (2014-05-31) + + * bug #11014 [Validator] Remove property and method targets from the optional and required constraints (jakzal) + * bug #10983 [DomCrawler] Fixed charset detection in html5 meta charset tag (77web) + * Merge branch '2.3' into 2.4 + * bug #10979 Make rootPath part of regex greedy (artursvonda) + * bug #10995 [TwigBridge][Trans]set %count% only on transChoice from the current context. (aitboudad) + * bug #10987 [DomCrawler] Fixed a forgotten case of complex XPath queries (stof) + +* 2.4.5 (2014-05-22) + + * bug #10849 [WIP][Finder] Fix wrong implementation on sortable callback comparator (ProPheT777) + * bug #10929 [Process] Add validation on Process input (romainneutron) + * bug #10958 [DomCrawler] Fixed filterXPath() chaining loosing the parent DOM nodes (stof, robbertkl) + * bug #10953 [HttpKernel] fixed file uploads in functional tests without file selected (realmfoo) + * bug #10947 [PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations (webmozart) + * bug #10937 [HttpKernel] Fix "absolute path" when we look to the cache directory (BenoitLeveque) + * bug #10894 [HttpKernel] removed absolute paths from the generated container (fabpot) + * bug #10926 [DomCrawler] Fixed the initial state for options without value attribute (stof) + * bug #10925 [DomCrawler] Fixed the handling of boolean attributes in ChoiceFormField (stof) + * bug #10777 [Form] Automatically add step attribute to HTML5 time widgets to display seconds if needed (tucksaun) + * bug #10909 [PropertyAccess] Fixed plurals for -ves words (csarrazi) + * bug #10904 [HttpKernel] Replace sha1 with sha256 in recently added tests (jakzal) + * bug #10899 Explicitly define the encoding. (jakzal) + * bug #10897 [Console] Fix a console test (jakzal) + * bug #10896 [HttpKernel] Fixed cache behavior when TTL has expired and a default "global" TTL is defined (alquerci, fabpot) + * bug #10841 [DomCrawler] Fixed image input case sensitive (geoffrey-brier) + * bug #10714 [Console]Improve formatter for double-width character (denkiryokuhatsuden) + * bug #10872 [Form] Fixed TrimListenerTest as of PHP 5.5 (webmozart) + * bug #10762 [BrowserKit] Allow URLs that don't contain a path when creating a cookie from a string (thewilkybarkid) + * bug #10863 [Security] Add check for supported attributes in AclVoter (artursvonda) + * bug #10833 [TwigBridge][Transchoice] set %count% from the current context. (aitboudad) + * bug #10820 [WebProfilerBundle] Fixed profiler seach/homepage with empty token (tucksaun) + * bug #10815 Fixed issue #5427 (umpirsky) + * bug #10817 [Debug] fix #10313: FlattenException not found (nicolas-grekas) + * bug #10803 [Debug] fix ErrorHandlerTest when context is not an array (nicolas-grekas) + * bug #10801 [Debug] ErrorHandler: remove $GLOBALS from context in PHP5.3 fix #10292 (nicolas-grekas) + * bug #10797 [HttpFoundation] Allow File instance to be passed to BinaryFileResponse (anlutro) + * bug #10643 [TwigBridge] Removed strict check when found variables inside a translation (goetas) + * bug #10605 [ExpressionLanguage] Strict in_array check in Parser.php (parnas) + +* 2.4.4 (2014-04-27) + + * bug #10789 [Console] Fixed the rendering of exceptions on HHVM with a terminal width (stof) + * bug #10773 [WebProfilerBundle ] Fixed an edge case on WDT loading (tucksaun) + * bug #10784 [Security] removed $csrfTokenManager type hint from SimpleFormAuthenticationListener constructor argument (choonge) + * bug #10763 [Process] Disable TTY mode on Windows platform (romainneutron) + * bug #10772 [Finder] Fix ignoring of unreadable dirs in the RecursiveDirectoryIterator (jakzal) + * bug #10757 [Process] Setting STDIN while running should not be possible (romainneutron) + * bug #10749 Fixed incompatibility of x509 auth with nginx (alcaeus) + * bug #10735 [Translation] [PluralizationRules] Little correction for case 'ar' (klyk50) + * bug #10720 [HttpFoundation] Fix DbalSessionHandler (Tobion) + * bug #10721 [HttpFoundation] status 201 is allowed to have a body (Tobion) + * bug #10728 [Process] Fix #10681, process are failing on Windows Server 2003 (romainneutron) + * bug #10733 [DomCrawler] Textarea value should default to empty string instead of null. (Berdir) + * bug #10723 [Security] fix DBAL connection typehint (Tobion) + * bug #10715 [Debug] Fixed ClassNotFoundFatalErrorHandler on windows. (lyrixx) + * bug #10700 Fixes various inconsistencies in the code (fabpot) + * bug #10697 [Translation] Make IcuDatFileLoader/IcuResFileLoader::load invalid resource compatible with HHVM. (idn2104) + * bug #10652 [HttpFoundation] fix PDO session handler under high concurrency (Tobion) + * bug #10690 [Validator] Fix hack for nested Collection/All losing context (GromNaN) + * bug #10669 [Profiler] Prevent throwing fatal errors when searching timestamps or invalid dates (stloyd) + * bug #10670 [Templating] PhpEngine should propagate charset to its helpers (stloyd) + * bug #10665 [DependencyInjection] Fix ticket #10663 - Added setCharset method call to PHP templating engine (koku) + * bug #10654 Changed the typehint of the EsiFragmentRenderer to the interface (stof) + * bug #10649 [BrowserKit] Fix #10641 : BrowserKit is broken when using ip as host (romainneutron) + +* 2.4.3 (2014-04-04) + + * bug #10586 Fixes URL validator to accept single part urls (merk) + * bug #10591 [Form] Buttons are now disabled if their containing form is disabled (webmozart) + * bug #10587 [Process] Fix Process test suite (romainneutron) + * bug #10579 HHVM fixes (fabpot) + * bug #10564 fixed the profiler when an uncalled listener throws an exception when instantiated (fabpot) + * bug #10568 [Form] Fixed hashing of choice lists containing non-UTF-8 characters (webmozart) + * bug #10536 Avoid levenshtein comparison when using ContainerBuilder. (catch56) + * bug #10549 Fixed server values in BrowserKit (fabpot) + * bug #10540 [HttpKernel] made parsing controllers more robust (fabpot) + * bug #10545 [DependencyInjection] Fixed YamlFileLoader imports path (jrnickell) + * bug #10523 [Debug] Check headers sent before sending PHP response (GromNaN) + * bug #10363 [FrameworkBundle][Console] Fix issue #10345 container:debug --parameter="" not working anymore (FineWolf) + * bug #10275 [Validator] Fixed ACE domain checks on UrlValidator (#10031) (aeoris) + * bug #10123 handle array root element (greg0ire) + * bug #10532 Fixed regression when using Symfony on filesystems without chmod support (fabpot) + * bug #10502 [HttpKernel] Fix #10437: Catch exceptions when reloading a no-cache request (romainneutron) + * bug #10493 Fix libxml_use_internal_errors and libxml_disable_entity_loader usage (romainneutron) + * bug #9784 [HttpFoundation] Removed ini check to make Uploadedfile work on Google App Engine (micheleorselli) + * bug #10416 [Form] Allow options to be grouped by objects (felds) + * bug #10410 [Form] Fix "Array was modified outside object" in ResizeFormListener. (Chekote) + * bug #10494 [Validator] Minor fix in IBAN validator (sprain) + * bug #10491 Fixed bug that incorrectly causes the "required" attribute to be omitted from select even though it contains the "multiple" attribute (fabpot) + * bug #10479 [Process] Fix escaping on Windows (romainneutron) + * bug #10480 [Process] Fixed fatal errors in getOutput and getErrorOutput when process was not started (romainneutron) + * bug #10420 [Process] Make Process::start non-blocking on Windows platform (romainneutron) + * bug #10455 [Process] Fix random failures in test suite on TravisCI (romainneutron) + * bug #10448 [Process] Fix quoted arguments escaping (romainneutron) + * bug #10444 [DomCrawler] Fixed incorrect value name conversion in getPhpValues() and getPhpFiles() (romainneutron) + * bug #10423 [Config] XmlUtils::convertDomElementToArray does not handle '0' (bendavies) + * bug #10153 [Process] Fixed data in pipe being truncated if not read before process termination (astephens25) + * bug #10429 [Process] Fix #9160 : escaping an argument with a trailing backslash on windows fails (romainneutron) + * bug #10412 [Process] Fix process status in TTY mode (romainneutron) + * bug #10382 10158 get vary multiple (bbinkovitz) + * bug #10251 [Form] Fixes empty file-inputs getting treated as extra field. (jenkoian) + * bug #10284 [HttpKernel] Fix issue #10209 (stephpy) + * bug #10351 [HttpKernel] fix stripComments() normalizing new-lines (sstok) + * bug #10348 Update FileLoader to fix issue #10339 (msumme) + * bug #10306 [Serializer] Throw exception when unable to normalize embedded object (gquemener) + * bug #10338 [ExpressionLanguage] Fixed evaluation of short circuit operators (florianv) + * bug #10146 [WebProfilerBundle] fixed parsing Mongo DSN and added Test for it (malarzm) + * bug #10299 [Finder] () is also a valid delimiter (WouterJ) + * bug #10255 [FrameworkBundle] Fixed wrong redirect url if path contains some query parameters (pulzarraider) + * bug #10285 Bypass sigchild detection if phpinfo is not available (Seldaek) + * bug #10269 [Form] Revert "Fix "Array was modified outside object" in ResizeFormListener." (norzechowicz) + * bug #10231 [Console] removed problematic regex (fabpot) + * bug #10245 [DomCrawler] Added support for tags to be treated as links (shamess) + * bug #10232 [Form] Fix "Array was modified outside object" in ResizeFormListener. (Chekote) + +* 2.4.2 (2014-02-12) + + * bug #10215 [Routing] reduced recursion in dumper (arnaud-lb) + * bug #10207 [DomCrawler] Fixed filterXPath() chaining (robbertkl) + * bug #10205 [DomCrawler] Fixed incorrect handling of image inputs (robbertkl) + * bug #10191 [HttpKernel] fixed wrong reference in TraceableEventDispatcher (fabpot) + * bug #10195 [Debug] Fixed recursion level incrementing in FlattenException::flattenArgs(). (sun) + * bug #10151 [Form] Update DateTime objects only if the actual value has changed (peterrehm) + * bug #10140 allow the TextAreaFormField to be used with valid/invalid HTML (dawehner) + * bug #10131 added lines to exceptions for the trans and transchoice tags (fabpot) + * bug #10002 Routing condition bugfix (marco-jantke) + * bug #10119 [Validator] Minor fix in XmlFileLoader (florianv) + * bug #10078 [BrowserKit] add non-standard port to HTTP_HOST server param (kbond) + * bug #10095 [Security] fix DI for SimpleFormAuthenticationListener (Tobion) + * bug #10091 [Translation] Update PluralizationRules.php (guilhermeblanco) + * bug #10053 [Form] fixed allow render 0 numeric input value (dczech) + * bug #10067 [HttpKernel] allow null value in fragment handler (kbond) + * bug #10042 [Expression Language] fix foo[index] (schokocappucino) + * bug #10033 [HttpKernel] Bugfix - Logger Deprecation Notice (Rican7) + * bug #10023 [FrameworkBundle] Thrown an HttpException instead returning a Response in RedirectController::redirectAction() (jakzal) + * bug #9985 Prevent WDT from creating a session (mvrhov) + * bug #10010 [Twig Bridge] Fixed bug in ExpressionExtension (ricbra) + * bug #10000 [Console] Fixed the compatibility with HHVM (stof) + * bug #9979 [Doctrine Bridge][Validator] Fix for null values in assosiated properties when using UniqueEntityValidator (vpetrovych) + * bug #9983 [TwigBridge] Update min. version of Twig (stloyd) + * bug #9970 [CssSelector] fixed numeric attribute issue (jfsimon) + * bug #9747 [DoctrineBridge] Fix: Add type detection. Needed by pdo_dblib (iamluc) + * bug #9962 [Process] Fix #9861 : Revert TTY mode (romainneutron) + * bug #9960 [Form] Update minimal requirement in composer.json (stloyd) + * bug #9952 [Translator] Fix Empty translations with Qt files (vlefort) + * bug #9957 [Console] Fixed command name guessing if an alternative is an alias (jakzal) + * bug #9948 [WebProfilerBundle] Fixed profiler toolbar icons for XHTML. (rafalwrzeszcz) + * bug #9933 Propel1 exception message (jaugustin) + * bug #9949 [BrowserKit] Throw exception on invalid cookie expiration timestamp (anlutro) + +* 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/CHANGELOG-2.5.md b/CHANGELOG-2.5.md new file mode 100644 index 000000000000..3d1f6dc0d7e6 --- /dev/null +++ b/CHANGELOG-2.5.md @@ -0,0 +1,362 @@ +CHANGELOG for 2.5.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 2.5 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.5.0...v2.5.1 + +* 2.5.10 (2015-02-02) + + * bug #13528 [Validator] reject ill-formed strings (nicolas-grekas) + * bug #13525 [Validator] UniqueEntityValidator - invalidValue fixed. (Dawid Sajdak) + * bug #13549 [EventDispatcher] Wrong EventDispatcher instance injected in listeners (dosten) + * bug #13527 [Validator] drop grapheme_strlen in LengthValidator (nicolas-grekas) + * bug #13376 [FrameworkBundle][config] allow multiple fallback locales. (aitboudad) + * bug #12972 Make the container considered non-fresh if the environment parameters are changed (thewilkybarkid) + * bug #13309 [Console] fixed 10531 (nacmartin) + * bug #13352 [Yaml] fixed parse shortcut Key after unindented collection. (aitboudad) + * bug #13343 [FrameworkBundle] FormDataCollector should be loaded only if form config is enabled (hason) + * bug #13039 [HttpFoundation] [Request] fix baseUrl parsing to fix wrong path_info (rk3rn3r) + * bug #13250 [Twig][Bridge][TranslationDefaultDomain] add support of named arguments. (aitboudad) + * bug #13332 [Console] ArgvInput and empty tokens (Taluu) + * bug #13293 [EventDispatcher] Add missing checks to RegisterListenersPass (znerol) + * bug #13262 [Yaml] Improve YAML boolean escaping (petert82, larowlan) + * bug #13420 [Debug] fix loading order for legacy classes (nicolas-grekas) + * bug #13421 [FrameworkBundle] fix routing descriptor for options (Tobion) + * bug #13405 [TwigBridge] exit when Twig environment is not set in the LintCommand (xabbuh) + * bug #13371 fix missing comma in YamlDumper (garak) + * bug #13365 [HttpFoundation] Make use of isEmpty() method (xelaris) + * bug #13355 [Console] Helper\Table->addRow optimization (boekkooi) + * bug #13347 [Console] Helper\TableHelper->addRow optimization (boekkooi) + * bug #13346 [PropertyAccessor] Allow null value for a array (2.3) (boekkooi) + * bug #13170 [Form] Set a child type to text if added to the form without a type. (jakzal) + * bug #13334 [Yaml] Fixed #10597: Improved Yaml directive parsing (VictoriaQ) + * bug #13316 [Form] fixed the CSRF extension to allow using only the new interfaces (fabpot) + * bug #13305 [FrameworkBundle] fixed missing information in some descriptors (fabpot) + +* 2.5.9 (2015-01-07) + + * bug #13286 [Security] Don't destroy the session on buggy php releases. (derrabus) + * bug #12417 [HttpFoundation] Fix an issue caused by php's Bug #66606. (wusuopu) + * bug #13200 Don't add Accept-Range header on unsafe HTTP requests (jaytaph) + * bug #12491 [Security] Don't send remember cookie for sub request (blanchonvincent) + * bug #12574 [HttpKernel] Fix UriSigner::check when _hash is not at the end of the uri (nyroDev) + * bug #13185 Fixes Issue #13184 - incremental output getters now return empty strings (Bailey Parker) + * bug #13173 [Debug] fixes ClassNotFoundFatalErrorHandler to correctly handle class not found errors with Symfony ClassLoader component autoloaders. (hhamon) + * bug #13145 [DomCrawler] Fix behaviour with tag (dkop, WouterJ) + * bug #13027 fix #10054 - form data collector with dynamic fields (zulus) + * bug #13141 [TwigBundle] Moved the setting of the default escaping strategy from the Twig engine to the Twig environment (fabpot) + * bug #13114 [HttpFoundation] fixed error when an IP in the X-Forwarded-For HTTP head... (fabpot) + * bug #12572 [HttpFoundation] fix checkip6 (Neime) + * bug #13093 [TwigBundle] added missing absolute URL in Twig exceptions (fabpot) + * bug #12975 [FrameworkBundle] Allow custom services for validator mapping cache. (jakzal) + * bug #13075 [Config] fix error handler restoration in test (nicolas-grekas) + * bug #13085 [FrameworkBundle] Fix dependency on ExtensionInterface over implementation (xphere) + * bug #13081 [FrameworkBundle] forward error reporting level to insulated Client (nicolas-grekas) + * bug #13053 [FrameworkBundle] Fixed Translation loader and update translation command. (saro0h) + * bug #13048 [Security] Delete old session on auth strategy migrate (xelaris) + * bug #12999 [FrameworkBundle] fix cache:clear command (nicolas-grekas) + * bug #13004 add a limit and a test to FlattenExceptionTest. (Daniel Wehner) + * bug #12961 fix session restart on PHP 5.3 (Tobion) + * bug #12548 [Form] fixed a maxlength overring on a guessing (origaminal) + * bug #12761 [Filesystem] symlink use RealPath instead LinkTarget (aitboudad) + * bug #12848 [EventDispatcher] Fixed #12845 adding a listener to an event that is currently being dispatched (Pieter Jordaan) + * bug #12855 [DependencyInjection] Perf php dumper (nicolas-grekas) + * bug #12894 [FrameworkBundle][Template name] avoid error message for the shortcut n... (aitboudad) + * bug #12806 [Console] Removed the use of $this->getHelperSet() as it is null by default (saro0h) + * bug #12858 [ClassLoader] Fix undefined index in ClassCollectionLoader (szicsu) + +* 2.5.8 (2014-12-03) + + * bug #12811 Configure firewall's kernel exception listener with configured entry point or a default entry point (rjkip) + * bug #12784 [DependencyInjection] make paths relative to __DIR__ in the generated container (nicolas-grekas) + * bug #12716 [ClassLoader] define constant only if it wasn't defined before (xabbuh) + * bug #12553 [Debug] fix error message on double exception (nicolas-grekas) + * bug #12550 [FrameworkBundle] backport #12489 (xabbuh) + * bug #12570 Fix initialized() with aliased services (Daniel Wehner) + * bug #12267 [Form][WebProfiler] Empty form names fix (kix) + * bug #12137 [FrameworkBundle] cache:clear command fills *.php.meta files with wrong data (Strate) + +* 2.5.7 (2014-11-20) + + * bug #12525 [Bundle][FrameworkBundle] be smarter when guessing the document root (xabbuh) + * bug #12296 [SecurityBundle] Authentication entry point is only registered with firewall exception listener, not with authentication listeners (rjkip) + * bug #12489 [FrameworkBundle] Fix server run in case the router script does not exist (romainneutron) + * bug #12443 [HttpKernel] Adding support for invokable controllers in the RequestDataCollector (jameshalsall) + * bug #12393 [DependencyInjection] inlined factory not referenced (boekkooi) + * bug #12436 [Filesystem] Fixed case for empty folder (yosmanyga) + * bug #12397 [Routing] fix BC (nicolas-grekas) + * bug #12382 [Routing] removed errors from git (HeinZawHtet) + * bug #12370 [Yaml] improve error message for multiple documents (xabbuh) + * bug #12170 [Form] fix form handling with OPTIONS request method (Tobion) + * bug #12235 [Validator] Fixed Regex::getHtmlPattern() to work with complex and negated patterns (webmozart) + * bug #12326 [Session] remove invalid hack in session regenerate (Tobion) + * bug #12341 [Kernel] ensure session is saved before sending response (Tobion) + * bug #12329 [Routing] serialize the compiled route to speed things up (Tobion) + * bug #12291 [Form] Fixed usage of "name" variable in form_start block (webmozart) + * bug #12316 Break infinite loop while resolving aliases (chx) + * bug #12313 [Security][listener] change priority of switchuser (aitboudad) + +* 2.5.6 (2014-10-24) + + * bug #11696 [Form] Fix #11694 - Enforce options value type check in some form types (kix) + * bug #12225 [SecurityBundle] Add trust_resolver variable into expression (zulus) + * bug #12209 [FrameworkBundle] Fixed ide links (hason) + * bug #12208 Add missing argument (WouterJ) + * bug #12197 [TwigBundle] do not pass a template reference to twig (Tobion) + * bug #12196 [TwigBundle] show correct fallback exception template in debug mode (Tobion) + * bug #12187 [CssSelector] don't raise warnings when exception is thrown (xabbuh) + * bug #12151 [Framework][DX] Set the proper validator class according to the configured api version (peterrehm) + * bug #12128 [Console] clean handling of :: passed to find() (xabbuh) + * bug #11998 [Intl] Integrated ICU data into Intl component #2 (webmozart) + * bug #11920 [Intl] Integrated ICU data into Intl component #1 (webmozart) + +* 2.5.5 (2014-09-28) + + * bug #12016 [Validator] Added ConstraintValidator::buildViolation() helper for BC with the 2.4 API (webmozart) + * bug #12031 [Validator] Fixed LegacyValidator when only a constraint is validated (webmozart) + * bug #9453 [Form][DateTime] Propagate invalid_message & invalid_message_parameters to date & time (egeloen) + * bug #12030 Fix expression language in the container when using the "container" variable (fabpot) + * bug #12032 [Command] Set the process title as late as possible (lyrixx) + * bug #11058 [Security] bug #10242 Missing checkPreAuth from RememberMeAuthenticationProvider (glutamatt) + * bug #12004 [Form] Fixed ValidatorTypeGuesser to guess properties without constraints not to be required (webmozart) + * bug #11904 Make twig ExceptionController conformed with ExceptionListener (megazoll) + * bug #11924 [Form] Moved POST_MAX_SIZE validation from FormValidator to request handler (rpg600, webmozart) + * bug #12002 [Security] [Firewall] Bug fixed in SimplePreAuthenticationListener when createToken() not return TokenInterface object (adenkejawen, fabpot) + * bug #11079 Response::isNotModified returns true when If-Modified-Since is later than Last-Modified (skolodyazhnyy) + * bug #11989 [Finder][Urgent] Remove asterisk and question mark from folder name in test to prevent windows file system issues. (Adam) + * bug #11908 [Translation] [Config] Clear libxml errors after parsing xliff file (pulzarraider) + * bug #11839 [FrameworkBundle] check if the Validator component is present when forms are enabled (xabbuh) + * bug #11418 [JsonResponse] Silent only JSON errors (GromNaN) + * bug #11937 [HttpKernel] Make sure HttpCache is a trusted proxy (thewilkybarkid) + * bug #11970 [Finder] Escape location for regex searches (ymc-dabe) + * bug #11837 Use getPathname() instead of string casting to get BinaryFileReponse file path (nervo) + * bug #11513 [Translation] made XliffFileDumper support CDATA sections. (hhamon) + * bug #11928 [Validator] The ratio of the ImageValidator is rounded to two decimals now (webmozart) + * bug #11907 [Intl] Improved bundle reader implementations (webmozart) + * bug #11874 [Console] guarded against non-traversable aliases (thierrymarianne) + * bug #11799 [YAML] fix handling of empty sequence items (xabbuh) + * bug #11906 [Intl] Fixed a few bugs in TextBundleWriter (webmozart) + * bug #11459 [Form][Validator] All index items after children are to be considered grand-children when resolving ViolationPath (Andrew Moore) + * bug #11715 [Form] FormBuilder::getIterator() now deals with resolved children (issei-m) + * bug #11892 [SwiftmailerBridge] Bump allowed versions of swiftmailer (ymc-dabe) + * bug #11918 [DependencyInjection] remove `service` parameter type from XSD (xabbuh) + * bug #11905 [Intl] Removed non-working $fallback argument from ArrayAccessibleResourceBundle (webmozart) + * bug #11497 Use separated function to resolve command and related arguments (JJK801) + * bug #11374 [DI] Added safeguards against invalid config in the YamlFileLoader (stof) + * bug #11897 [FrameworkBundle] Remove invalid markup (flack) + * bug #11860 [Security] Fix usage of unexistent method in DoctrineAclCache. (mauchede) + * bug #11850 [YAML] properly mask escape sequences in quoted strings (xabbuh) + * bug #11856 [FrameworkBundle] backport more error information from 2.6 to 2.3 (xabbuh) + * bug #11843 [Yaml] improve error message when detecting unquoted asterisks (xabbuh) + +* 2.5.4 (2014-09-03) + + * security #11832 CVE-2014-6072 (fabpot) + * security #11831 CVE-2014-5245 (stof) + * security #11830 CVE-2014-4931 (aitboudad, Jérémy Derussé) + * security #11829 CVE-2014-6061 (damz, fabpot) + * security #11828 CVE-2014-5244 (nicolas-grekas, larowlan) + * bug #10197 [FrameworkBundle] PhpExtractor bugfix and improvements (mtibben) + * bug #11772 [Filesystem] Add FTP stream wrapper context option to enable overwrite (Damian Sromek) + * bug #11791 [Process] fix mustRun() in sigchild environments (xabbuh) + * bug #11788 [Yaml] fixed mapping keys containing a quoted # (hvt, fabpot) + * bug #11787 fixed DateComparator if file does not exist (avi123) + * bug #11160 [DoctrineBridge] Abstract Doctrine Subscribers with tags (merk) + * bug #11768 [ClassLoader] Add a __call() method to XcacheClassLoader (tstoeckler) + * bug #11739 [Validator] Pass strict argument into the strict email validator (brianfreytag) + * bug #11749 [TwigBundle] Remove hard dependency of RequestContext in AssetsExtension (pgodel) + * bug #11726 [Filesystem Component] mkdir race condition fix #11626 (kcassam) + * bug #11677 [YAML] resolve variables in inlined YAML (xabbuh) + * bug #11639 [DependencyInjection] Fixed factory service not within the ServiceReferenceGraph. (boekkooi) + * bug #11778 [Validator] Fixed wrong translations for Collection constraints (samicemalone) + * bug #11756 [DependencyInjection] fix @return anno created by PhpDumper (jakubkulhan) + * bug #11711 [DoctrineBridge] Fix empty parameter logging in the dbal logger (jakzal) + * bug #11692 [DomCrawler] check for the correct field type (xabbuh) + * bug #11672 [Routing] fix handling of nullable XML attributes (xabbuh) + * bug #11624 [DomCrawler] fix the axes handling in a bc way (xabbuh) + * bug #11676 [Form] Fixed #11675 ValueToDuplicatesTransformer accept "0" value (Nek-) + * bug #11695 [Validators] Fixed failing tests requiring ICU 52.1 which are skipped otherwise (webmozart) + * bug #11584 [FrameworkBundle] Fixed validator factory definition when the Validator API is "auto" for PHP < 5.3.9 (webmozart) + * bug #11645 [Form] Fixed ValidatorExtension to work with the 2.5 Validation API (webmozart) + * bug #11529 [WebProfilerBundle] Fixed double height of canvas (hason) + * bug #11666 [DIC] Fixed: anonymous services are always private (lyrixx) + * bug #11641 [WebProfilerBundle ] Fix toolbar vertical alignment (blaugueux) + * bug #11637 fix dependencies on HttpFoundation component (xabbuh) + * bug #11559 [Validator] Convert objects to string in comparison validators (webmozart) + * feature #11510 [HttpFoundation] MongoDbSessionHandler supports auto expiry via configurable expiry_field (catchamonkey) + * bug #11408 [HttpFoundation] Update QUERY_STRING when overrideGlobals (yguedidi) + * bug #11625 [FrameworkBundle] resolve parameters before the configs are processed in the config:debug command (xabbuh) + * bug #11633 [FrameworkBundle] add missing attribute to XSD (xabbuh) + * bug #11601 [Validator] Allow basic auth in url when using UrlValidator. (blaugueux) + * bug #11609 [Console] fixed style creation when providing an unknown tag option (fabpot) + * bug #10914 [HttpKernel] added an analyze of environment parameters for built-in server (mauchede) + * bug #11598 [Finder] Shell escape and windows support (Gordon Franke, gimler) + * bug #11582 [DoctrineBridge] Changed UniqueEntityValidator to use the 2.5 Validation API (webmozart) + +* 2.5.3 (2014-08-06) + + * bug #11571 [Form] Fixed FormValidator compatibility with the non-BC 2.5 Validation API (webmozart) + * bug #11499 [BrowserKit] Fixed relative redirects for ambiguous paths (pkruithof) + * bug #11516 [BrowserKit] Fix browser kit redirect with ports (dakota) + * bug #11545 [Bundle][FrameworkBundle] built-in server: exit when docroot does not exist (xabbuh) + * bug #11560 Plural fix (1emming) + * bug #11558 [DependencyInjection] Fixed missing 'factory-class' attribute in XmlDumper output (kerdany) + * bug #11498 [Validator] Made it possible (again) to pass a class name to validatePropertyValue() (webmozart) + * bug #11548 [Component][DomCrawler] fix axes handling in Crawler::filterXPath() (xabbuh) + * bug #11422 [DependencyInjection] Self-referenced 'service_container' service breaks garbage collection (sun) + * bug #11428 [Serializer] properly handle null data when denormalizing (xabbuh) + * bug #10687 [Validator] Fixed string conversion in constraint violations (eagleoneraptor, webmozart) + * bug #11412 [Validator] Made sure that context changes don't leak out of (Contextual)ValidatorInterface (webmozart) + * bug #11475 [EventDispatcher] don't count empty listeners (xabbuh) + * bug #11436 fix signal handling in wait() on calls to stop() (xabbuh, romainneutron) + * bug #11469 [BrowserKit] Fixed server HTTP_HOST port uri conversion (bcremer, fabpot) + * bug #11425 Fix issue described in #11421 (Ben, ben-rosio) + * bug #11423 Pass a Scope instance instead of a scope name when cloning a container in the GrahpvizDumper (jakzal) + * bug #11448 [MonologBridge] fixed Console handler priorities (fabpot) + * bug #11454 [Validator] Fixed memory leak in ValidatorBuilder (webmozart) + * bug #11120 [Process] Reduce I/O load on Windows platform (romainneutron) + * bug #11370 [FrameworkBundle] avoid raising unexpected RuntimeException when specifying $_SERVER['KERNEL_DIR'] (iteman) + * bug #11342 [Form] Check if IntlDateFormatter constructor returned a valid object before using it (romainneutron) + * bug #11439 [ExpressionLanguage] Fixed double quoted string literals containing sharps (pylebecq) + * bug #11410 [Validator] Fixed object initializers in 2.5 version of the Validator (webmozart) + * bug #11411 [Validator] Backported #11410 to 2.3: Object initializers are called only once per object (webmozart) + * bug #11403 [Translator][FrameworkBundle] Added @ to the list of allowed chars in Translator (takeit) + * bug #11381 [Process] Use correct test for empty string in UnixPipes (whs, romainneutron) + +* 2.5.2 (2014-07-15) + + * [Security] Forced validate of locales passed to the translator + * bug #11350 [Form] solved dependency to ValidatorInterface, fix #11036 (Sebastian Blum) + * bug #11278 Remove Spaceless Blocks From Twig Templates (chrisguitarguy) + * feature #11367 [HttpFoundation] Fix to prevent magic bytes injection in JSONP responses... (CVE-2014-4671) (Andrew Moore) + * bug #11284 [Console] Remove estimated field from debug_nomax (bburnichon) + * bug #11386 Remove Spaceless Blocks from Twig Form Templates (chrisguitarguy) + * bug #9719 [TwigBundle] fix configuration tree for paths (mdavis1982, cordoval) + * bug #11244 [HttpFoundation] Remove body-related headers when sending the response, if body is empty (SimonSimCity) + +* 2.5.1 (2014-07-08) + + * bug #11283 [SecurityBundle] Remove Expression Language services when the component is unavailable (thewilkybarkid) + * bug #11238 [Translation] Added unescaping of ids in PoFileLoader (JustBlackBird) + * bug #11194 [DomCrawler] Remove the query string and the anchor of the uri of a link (benja-M-1) + * bug #11272 [Console] Make sure formatter is the same. (akimsko) + * bug #11259 [Config] Fixed failed config schema loads due to libxml_disable_entity_loader usage (ccorliss) + * bug #11234 [ClassLoader] fixed PHP warning on PHP 5.3 (fabpot) + * bug #11179 [Process] Fix ExecutableFinder with open basedir (cs278) + * bug #11242 [CssSelector] Refactored the CssSelector to remove the circular object graph (stof) + * bug #11219 [DomCrawler] properly handle buttons with single and double quotes insid... (xabbuh) + * bug #11220 [Components][Serializer] optional constructor arguments can be omitted during the denormalization process (xabbuh) + * bug #11186 Added missing `break` statement (apfelbox) + * bug #11168 [YAML] fix merge node (<<) (Tobion) + * bug #11170 [Console] Fixed notice in QuestionHelper (florianv) + * bug #11169 [Console] Fixed notice in DialogHelper (florianv) + * bug #11144 [HttpFoundation] Fixed Request::getPort returns incorrect value under IPv6 (kicken) + * bug #11121 [Process] Do not redirect output to file handles when output is disabled, simply discard it (romainneutron) + * bug #10966 PHP Fatal error when getContainer method of ContainerAwareCommand has be... (kevinvergauwen) + * bug #10981 [HttpFoundation] Fixed isSecure() check to be compliant with the docs (Jannik Zschiesche) + * bug #11117 [Validator] Fix array notation in the PropertyPath::append() (jakzal) + * bug #11113 [HttpKernel] Fix event dispatcher dependency (hacfi) + * bug #11111 Fixed undefined ImageValidator::$suffices property when uploading an image during functional tests (OwlyCode) + * bug #11099 [Debug] work-around https://bugs.php.net/61272 (nicolas-grekas) + * bug #11092 [HttpFoundation] Fix basic authentication in url with PHP-FPM (Kdecherf) + * bug #11097 [Debug] simplify code path to remove potential blank pages (nicolas-grekas) + * bug #10808 [DomCrawler] Empty select with attribute name="foo[]" bug fix (darles) + * bug #11063 [HttpFoundation] fix switch statement (Tobion) + * bug #11054 [Serializer] Xml encoder whitespace fix (fieg) + * bug #11009 [HttpFoundation] smaller fixes for PdoSessionHandler (Tobion) + * bug #11047 #10862 loadClassMetadata vs loadValidatorMetadata: revert default config (phramz) + * bug #11042 [Debug] fix debug handlers config (nicolas-grekas) + * bug #11043 [Console] OutputFormatter Unset Bold has wrong id (DZunke) + * bug #11033 [Debug] fix wrong case mismatch exception (nicolas-grekas) + * bug #11044 [Serializer] Fix BC break since 2.5 (fieg) + * bug #11041 Remove undefined variable $e (skydiablo) + +* 2.5.0 (2014-05-31) + + * bug #11014 [Validator] Remove property and method targets from the optional and required constraints (jakzal) + * bug #10983 [DomCrawler] Fixed charset detection in html5 meta charset tag (77web) + +* 2.5.0-RC1 (2014-05-28) + + * bug #10979 Make rootPath part of regex greedy (artursvonda) + * bug #10995 [TwigBridge][Trans]set %count% only on transChoice from the current context. (aitboudad) + * bug #10989 [Debug] throw even in stacking mode to preserve code paths (nicolas-grekas) + * bug #10987 [DomCrawler] Fixed a forgotten case of complex XPath queries (stof) + * feature #10930 [Process] Deprecate using values that are not string for Process::setStdin and ProcessBuilder::setInput (romainneutron) + * bug #10971 [Process] Fix conflicts between latest 2.3 fix and 2.5 deprecation (romainneutron) + * feature #10932 [Process] Deprecate Process::setStdin in favor of Process::setInput (romainneutron) + * bug #10849 [WIP][Finder] Fix wrong implementation on sortable callback comparator (ProPheT777) + * bug #10929 [Process] Add validation on Process input (romainneutron) + * bug #10946 [PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations (webmozart) + * bug #10958 [DomCrawler] Fixed filterXPath() chaining loosing the parent DOM nodes (stof, robbertkl) + * bug #10953 [HttpKernel] fixed file uploads in functional tests without file selected (realmfoo) + * feature #10941 [Debug] cleanup interfaces before 2.5-final (nicolas-grekas) + * bug #10947 [PropertyAccess] Fixed getValue() when accessing non-existing indices of ArrayAccess implementations (webmozart) + * bug #10937 [HttpKernel] Fix "absolute path" when we look to the cache directory (BenoitLeveque) + * bug #10933 Changed the default value of checkbox and radio to match the HTML spec (stof) + * bug #10927 [DomCrawler] Changed typehints form DomNode to DomElement (stof) + * bug #10894 [HttpKernel] removed absolute paths from the generated container (fabpot) + * bug #10926 [DomCrawler] Fixed the initial state for options without value attribute (stof) + * bug #10925 [DomCrawler] Fixed the handling of boolean attributes in ChoiceFormField (stof) + * feature #10882 Fix issue #10867 (umpirsky) + * bug #10902 [Yaml] Fixed YAML Parser does not ignore duplicate keys, violating YAML spec. (sun) + * feature #10912 [Form] Added support for injecting HttpFoundation's Request in ServerParams for the Validator extension (csarrazi) + * bug #10777 [Form] Automatically add step attribute to HTML5 time widgets to display seconds if needed (tucksaun) + * bug #10909 [PropertyAccess] Fixed plurals for -ves words (csarrazi) + * bug #10904 [HttpKernel] Replace sha1 with sha256 in recently added tests (jakzal) + * bug #10899 Explicitly define the encoding. (jakzal) + * bug #10897 [Console] Fix a console test (jakzal) + * bug #10896 [HttpKernel] Fixed cache behavior when TTL has expired and a default "global" TTL is defined (alquerci, fabpot) + * bug #10841 [DomCrawler] Fixed image input case sensitive (geoffrey-brier) + * bug #10714 [Console]Improve formatter for double-width character (denkiryokuhatsuden) + * bug #10872 [Form] Fixed TrimListenerTest as of PHP 5.5 (webmozart) + * feature #10880 [DependencyInjection] GraphvizDumper now displays unresolved parameters (rosstuck) + * bug #10876 [Console] Make `Helper\Table::setStyle()` chainable again (stloyd) + * bug #10762 [BrowserKit] Allow URLs that don't contain a path when creating a cookie from a string (thewilkybarkid) + * bug #10861 [Debug] enhance perf of DebugClassLoader (nicolas-grekas) + * bug #10863 [Security] Add check for supported attributes in AclVoter (artursvonda) + * bug #10854 [Debug] fix handling deprecated warnings and stacked errors turned into exceptions (nicolas-grekas) + * feature #10843 [TwigBridge] Added compile-time issues checking in twig:lint command (maxromanovsky) + * feature #10829 Fix issue 9172 (umpirsky) + * bug #10833 [TwigBridge][Transchoice] set %count% from the current context. (aitboudad) + * bug #10820 [WebProfilerBundle] Fixed profiler seach/homepage with empty token (tucksaun) + * bug #10809 Fixed composer to include config component for mocks in phpunit (jpauli) + * bug #10815 Fixed issue #5427 (umpirsky) + * bug #10817 [Debug] fix #10313: FlattenException not found (nicolas-grekas) + +* 2.5.0-BETA2 (2014-04-29) + + * bug #10803 [Debug] fix ErrorHandlerTest when context is not an array (nicolas-grekas) + * bug #10801 [Debug] ErrorHandler: remove $GLOBALS from context in PHP5.3 fix #10292 (nicolas-grekas) + * bug #10799 [Debug] less intrusive work around for https://bugs.php.net/54275 (nicolas-grekas) + * bug #10797 [HttpFoundation] Allow File instance to be passed to BinaryFileResponse (anlutro) + * bug #10798 [Console] Fix #10795: Allow instancing Console Application when STDIN is not declared (romainneutron) + * bug #10643 [TwigBridge] Removed strict check when found variables inside a translation (goetas) + * bug #10605 [ExpressionLanguage] Strict in_array check in Parser.php (parnas) + * bug #10789 [Console] Fixed the rendering of exceptions on HHVM with a terminal width (stof) + * bug #10773 [WebProfilerBundle ] Fixed an edge case on WDT loading (tucksaun) + * feature #10786 [FrameworkBundle] removed support for HHVM built-in web server as it is deprecated now (fabpot) + * bug #10784 [Security] removed $csrfTokenManager type hint from SimpleFormAuthenticationListener constructor argument (choonge) + * bug #10776 [Debug] fix #10771 DebugClassLoader can't load PSR4 libs (nicolas-grekas) + * bug #10763 [Process] Disable TTY mode on Windows platform (romainneutron) + * bug #10772 [Finder] Fix ignoring of unreadable dirs in the RecursiveDirectoryIterator (jakzal) + * bug #10757 [Process] Setting STDIN while running should not be possible (romainneutron) + * bug #10749 Fixed incompatibility of x509 auth with nginx (alcaeus) + * feature #10725 [Debug] Handled errors (nicolas-grekas) + * bug #10735 [Translation] [PluralizationRules] Little correction for case 'ar' (klyk50) + * bug #10720 [HttpFoundation] Fix DbalSessionHandler (Tobion) + * bug #10721 [HttpFoundation] status 201 is allowed to have a body (Tobion) + * bug #10728 [Process] Fix #10681, process are failing on Windows Server 2003 (romainneutron) + * bug #10733 [DomCrawler] Textarea value should default to empty string instead of null. (Berdir) + * bug #10723 [Security] fix DBAL connection typehint (Tobion) + * bug #10715 [Debug] Fixed ClassNotFoundFatalErrorHandler on windows. (lyrixx) + * bug #10700 Fixes various inconsistencies in the code (fabpot) + * bug #10697 [Translation] Make IcuDatFileLoader/IcuResFileLoader::load invalid resource compatible with HHVM. (idn2104) + +* 2.5.0-BETA1 (2014-04-11) + + * first beta release + diff --git a/CHANGELOG-2.6.md b/CHANGELOG-2.6.md new file mode 100644 index 000000000000..e466668f8f88 --- /dev/null +++ b/CHANGELOG-2.6.md @@ -0,0 +1,334 @@ +CHANGELOG for 2.6.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 2.6 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.6.0...v2.6.1 + +* 2.6.10 (2015-07-13) + + * bug #15248 Added 'default' color (jaytaph) + * bug #15243 Reload the session after regenerating its id (jakzal) + * bug #15202 [Security] allow to use `method` in XML configs (xabbuh) + * bug #15218 [Twig][Bridge] replaced `extends` with `use` in bootstrap_3_horizontal_layout.html.twig (MatTheCat) + * bug #15223 [Finder] Command::addAtIndex() fails with Command instance argument (thunderer) + * bug #15220 [DependencyInjection] Freeze also FrozenParameterBag::remove (lyrixx) + * bug #15110 Add a way to reset the singleton (dawehner) + * bug #15115 [Validator] always evaluate binary format when changed (xabbuh) + * bug #15163 Update DateTimeToArrayTransformer.php (zhil) + * bug #15150 [Translation] Azerbaijani language pluralization rule is wrong (shehi) + * bug #15159 Towards 100% HHVM compat (nicolas-grekas) + * bug #15146 Towards 100% HHVM compat (nicolas-grekas) + * bug #15069 [Form] Fixed: Data mappers always receive forms indexed by their names (webmozart) + * bug #15137 [Security] Initialize SwitchUserEvent::targetUser on attemptExitUser (Rvanlaak, xabbuh) + * bug #15126 [Validator] Fix BC for Validator's validate method (michalmarcinkowski) + * bug #15083 [DependencyInjection] Fail when dumping a Definition with no class nor factory (nicolas-grekas) + * bug #15127 [Validator] fix validation for Maestro UK card numbers (xabbuh) + * bug #15128 DbalLogger: Small nonutf8 array fix (vpetrovych, weaverryan) + * bug #15048 [Translation][Form][choice] empty_value shouldn't be translated when it has an empty value (Restless-ET) + * bug #15117 [Form] fixed sending non array data on submit to ResizeListener (BruceWouaigne) + * bug #15102 [Translation][debug cmd] fixed failing tests. (aitboudad) + * bug #13750 [DependencyInjection] Fixed decoration of service for service with parent (hason) + * bug #15086 Fixed the regexp for the validator of Maestro-based credit/debit cards (javiereguiluz) + * bug #15058 [Console] Fix STDERR output text on IBM iSeries OS400 (johnkary) + * bug #14853 [Validator] more strict e-mail validation regex (xabbuh) + * bug #15065 [Form] Fixed: remove quoted strings from Intl date formats (e.g. es_ES full pattern) (webmozart) + * bug #15039 [Translation][update cmd] taken account into bundle overrides path. (aitboudad) + * bug #15038 [Translation][debug cmd] taken account into bundle overrides path. (aitboudad) + * bug #14964 [bugfix][MonologBridge] WebProcessor: passing $extraFields to BaseWebProcessor (MacDada) + * bug #15036 [VarDumper] Fix dump output for better readability (nicolas-grekas) + * bug #15027 [Form] Fixed: Filter non-integers when selecting entities by int ID (webmozart, nicolas-grekas) + * bug #15000 [Debug] Fix fatal-errors handling on HHVM (nicolas-grekas) + * bug #14999 [Debug] Fix fatal-errors handling on HHVM (nicolas-grekas, digitalkaoz) + * bug #14959 [Debug+VarDumper] Fix handling of PHP7 "Throwable" exceptions (nicolas-grekas) + * bug #15010 [Debug] Fix log level of stacked errors (nicolas-grekas) + * bug #15017 [VarDumper] Fix uninitialized id in HtmlDumper (nicolas-grekas) + * bug #14980 Fixed fluent interface (jaytaph) + * bug #14974 [Security][Translation] #14920 update translations (vincentaubert) + * bug #14930 Bug #14836 [HttpFoundation] Moves default JSON encoding assignment fr… (Incognito) + * bug #14897 Allow new lines in Messages translated with transchoice() (replacement for #14867) (azine) + * bug #14895 [Form] Support DateTimeImmutable in transform() (c960657) + * bug #14891 without this change allways the legacy code get called (dominikzogg) + * bug #14859 Improve the config validation in TwigBundle (stof) + * bug #14785 [BrowserKit] Fix bug when uri starts with http. (amouhzi) + * bug #14807 [Security][Acl] enforce string identifiers (xabbuh) + * bug #14808 [WebProfilerBundle][logger] added missing deprecation message. (aitboudad) + +* 2.6.9 (2015-05-30) + + * bug #14777 Avoid using the app global variable in the profiler templates (stof) + * bug #14262 [REVERTED] [TwigBundle] Refresh twig paths when resources change. (aitboudad) + +* 2.6.8 (2015-05-27) + + * security #14759 CVE-2015-4050 [HttpKernel] Do not call the FragmentListener if _controller is already defined (jakzal) + * bug #14743 [DebugBundle] Fix config XSD (nicolas-grekas) + * bug #14726 [Translation] fixed JSON loader on PHP 7 when file is empty (fabpot) + * bug #14715 [Form] Check instance of FormBuilderInterface instead of FormBuilder (dosten) + * bug #14678 [Security] AbstractRememberMeServices::encodeCookie() validates cookie parts (MacDada) + * bug #14635 [HttpKernel] Handle an array vary header in the http cache store (jakzal) + * bug #14513 [console][formater] allow format toString object. (aitboudad) + * bug #14335 [HttpFoundation] Fix baseUrl when script filename is contained in pathInfo (danez) + * bug #14593 [Security][Firewall] Avoid redirection to XHR URIs (asiragusa) + * bug #14618 [DomCrawler] Throw an exception if a form field path is incomplete (jakzal) + * bug #14699 Fix HTML escaping of to-source links (amenk, nicolas-grekas) + * bug #14698 Fix HTML escaping of to-source links (nicolas-grekas) + * bug #14690 [HttpFoundation] IpUtils::checkIp4() should allow `/0` networks (zerkms) + * bug #14696 Fix the rendering of deprecation log messages (stof) + * bug #14683 Fixed the indentation in the compiled template for the DumpNode (stof) + * bug #14262 [TwigBundle] Refresh twig paths when resources change. (aitboudad) + * bug #13633 [ServerBag] Handled bearer authorization header in REDIRECT_ form (Lance0312) + * bug #13637 [CSS] WebProfiler break words (nicovak) + * bug #14217 [WebProfilerBundle] Fix regexp (romqin) + * bug #14644 [Bridge\Twig] Adding a space between the icon and the error message (zmikael, nicolas-grekas) + * bug #14640 [DebugBundle] Allow alternative destination for dumps (nicolas-grekas) + * bug #14633 [EventDispatcher] make listeners removable from an executed listener (xabbuh) + * bug #14609 [DebugBundle] Remove inlined dumps on XHR (nicolas-grekas) + * bug #14605 [PropertyAccess] Fix setting public property on a class having a magic getter (lolautruche) + +* 2.6.7 (2015-05-11) + + * bug #14266 [HttpKernel] Check if "symfony/proxy-manager-bridge" package is installed (hason) + * bug #14478 [DebugBundle] Fix dump() output in API / No-Toolbar context (nicolas-grekas) + * bug #14501 [ProxyBridge] Fix proxy classnames generation (xphere) + * bug #14527 Fix getOrigin (WouterJ) + * bug #14498 [FrameworkBundle] Added missing log in server:run command (lyrixx) + * bug #14503 [Finder] Only use GLOB_BRACE when available (dosten) + * bug #14484 [SecurityBundle][WebProfiler] check authenticated user by tokenClass instead of username. (aitboudad) + * bug #14497 [HttpFoundation] Allow curly braces in trusted host patterns (sgrodzicki) + * bug #14480 [TwigBundle] Fix deprecated use of FlattenException (alOneh) + * bug #14469 [Debug] BaseException compatibility for PHP7 (nicolas-grekas) + * bug #14466 [WebProfiler] fix html syntax for input types (Tobion) + * bug #14436 Show a better error when the port is in use (dosten) + * bug #14463 [Validator] Fixed Choice when an empty array is used in the "choices" option (webmozart) + * bug #14446 [DependencyInjection] resolve circular reference (xabbuh) + * bug #14448 [Validator] Fixed Choice when an empty array is used in the "choices" option (webmozart) + * bug #14451 [Debug] Fix ClassNotFoundFatalErrorHandler candidates lookups (nicolas-grekas) + * bug #14355 [EventDispatcher] make listeners removable from an executed listener (xabbuh) + * bug #14402 [FrameworkBundle][Translation] Check for 'xlf' instead of 'xliff' (xelaris) + * bug #14272 [FrameworkBundle] Workaround php -S ignoring auto_prepend_file (nicolas-grekas) + * bug #14362 [Debug] Scream as LogLevel::DEBUG (but for fatal errors / uncaught exceptions) (nicolas-grekas) + * bug #14345 [FrameworkBundle] Fix Routing\DelegatingLoader resiliency to fatal errors (nicolas-grekas) + * bug #14381 [FrameworkBundle] Initialize translator with the default locale. (aitboudad) + * bug #14325 [Routing][DependencyInjection] Support .yaml extension in YAML loaders (thunderer) + * bug #14344 [Translation][fixed test] refresh cache when resources are no longer fresh. (aitboudad) + * bug #14346 [WebProfilerBundle] Fix resiliency to exceptions thrown by the url generator (nicolas-grekas) + * bug #14338 [FrameworkBundle] improve usage of Table helper (xabbuh) + * bug #14129 [FrameworkBundle] Fixed server:start --router relative path issue #14124 (abulford) + * bug #14271 [VarDumper] Fix call site detection (nicolas-grekas) + * bug #14277 [Translator] Cache does not take fallback locales into consideration (mpdude) + * bug #14268 [Translator] Cache does not take fallback locales into consideration (sf2.3) (mpdude) + * bug #14256 [Form] Fixed DateType/TimeType (webmozart) + * bug #14226 [Profiler][Logger] fixed cycle odd/even. (aitboudad) + * bug #14204 [VarDumper] Towards PHP7 support (nicolas-grekas) + * bug #14205 Fix currently broken tests (mpdude) + * bug #14192 [HttpKernel] Embed the original exception as previous to bounced exceptions (nicolas-grekas) + * bug #14102 [Enhancement] netbeans - force interactive shell when limited detection (cordoval) + * bug #14191 [StringUtil] Fixed singularification of 'movies' (GerbenWijnja) + * bug #14182 [FrameworkBundle] fixes displaying of deprecation notices. (hhamon) + * feature #14186 [Debug] Renamed "context" key to "scope_vars" to avoid any ambiguity (lyrixx) + * bug #14170 Fix the AJAX profiling (stof) + +* 2.6.6 (2015-04-01) + + * security #14167 CVE-2015-2308 (nicolas-grekas) + * security #14166 CVE-2015-2309 (neclimdul) + * bug #14126 [VarDumper] Fix toggle action to see source excerpt (nicolas-grekas) + * bug #14114 [VarDumper] Fix dumping references as properties (nicolas-grekas) + * bug #14074 [VarDumper] Fix dumping ThrowingCasterException (nicolas-grekas) + * bug #12948 [Form] [TwigBridge] Bootstrap layout whitespace control (mvar) + * bug #14046 [Security] StringUtils::equals() arguments in RememberMe Cookie based implementation are confused (zerkms) + * bug #14010 Replace GET parameters when changed in form (WouterJ) + * bug #13991 [Dependency Injection] Improve PhpDumper Performance for huge Containers (BattleRattle) + * bug #13886 [FrameworkBundle][debug:config] added support for dynamic configurations... (aitboudad) + * bug #14013 [DependencyInjection] prevent inlining service configurators (xabbuh) + * bug #14012 [DomCrawler] Improve namespace discovery performance (jakzal) + * bug #13997 [2.3+][Form][DoctrineBridge] Improved loading of entities and documents (guilhermeblanco) + * bug #13978 [WebProfilerBundle] Fix javascript toolbar on IE8 (romainneutron) + * bug #13987 [WebProfilerBundle] fixed undefined buttons variable. (aitboudad) + * bug #13953 [Translation][MoFileLoader] fixed load empty translation. (aitboudad) + * bug #13912 [DependencyInjection] Highest precedence for user parameters (lyrixx) + +* 2.6.5 (2015-03-17) + + * bug #13944 [HttpKernel] UriSigner::buildUrl - default params for http_build_query (Jakub Simon) + * bug #13896 [ExpressionLanguage] fixed issues when parsing postfix expressions (zerustech) + * bug #13914 [DependencyInjection] do not inline service factories (xabbuh) + * bug #13924 [DependencyInjection] resolve class parameters in service factories (xabbuh) + * bug #13927 Fixing wrong variable name from #13519 (weaverryan) + * bug #13519 [DependencyInjection] fixed service resolution for factories (fabpot) + * bug #13790 [acl][command][SecurityBundle] Fixed user input option mode to be an Array (benjaminlong) + * bug #13902 [Debug] reintroduce charset param to ExceptionHandler (nicolas-grekas) + * bug #13901 [Bundle] Fix charset config (nicolas-grekas, bamarni) + * bug #13911 [HttpFoundation] MongoDbSessionHandler::read() now checks for valid session age (bzikarsky) + * bug #13883 #13857 Added default button class (Piers Warmers) + * bug #13890 Fix XSS in Debug exception handler (fabpot) + * bug #13860 [VarDumper] Fix "next element is already occupied" (nicolas-grekas) + * bug #13806 [TwigBridge] Bootstrap Layout - Fix the label of checkbox cannot be empty (ogizanagi) + * bug #13835 [PropertyAccess] stop overwriting once a reference is reached (3rd) (bananer) + * bug #13814 [Twig] bootstrap_3_layout.html.twig is traitable (Dusan Kasan) + * bug #13816 [OptionsResolver] fix allowed values with null (Tobion) + * bug #13744 minor #13377 [Console] Change greater by greater or equal for isFresh in FileResource (bijibox) + * bug #13708 [HttpFoundation] fixed param order for Nginx's x-accel-mapping (phansys) + * bug #13767 [HttpKernel] Throw double-bounce exceptions (nicolas-grekas) + * bug #13785 [VarDumper] Workaround stringy numeric keys (nicolas-grekas) + * bug #13769 [Form] NativeRequestHandler file handling fix (mpajunen) + * bug #13779 [FrameworkBundle] silence E_USER_DEPRECATED in insulated clients (nicolas-grekas) + * bug #13715 Enforce UTF-8 charset for core controllers (WouterJ) + * bug #13683 [PROCESS] make sure /dev/tty is readable (staabm) + * bug #13733 [Process] Fixed PhpProcess::getCommandLine() result (francisbesset) + * bug #13729 Fix the toolbar JS for IE (stof) + * bug #13693 [HttpKernel] Fixed DumpDataCollector: dump.name for Windows file paths (King2500) + * bug #13618 [PropertyAccess] Fixed invalid feedback -> foodback singularization (WouterJ) + * bug #13685 [WebProfilerBundle] Fix for broken profiler layout (kbond) + * bug #13636 [WebProfilerBundle] Fixes event listener attaching error in IE (aik099) + * bug #13630 [Console] fixed ArrayInput, if array contains 0 key. (arima-ryunosuke) + * bug #13647 [FrameworkBundle] Fix title and placeholder rendering in php form templates (jakzal) + * bug #13608 Fix form icon position in web profiler (sadikoff) + * bug #13642 [Translator][Logging] implement TranslatorBagInterface. (aitboudad) + * bug #13607 [Console] Fixed output bug, if escaped string in a formatted string. (tronsha) + * bug #13611 [Console] “console help” ignores --raw option (c960657) + * bug #13466 [Security] Remove ContextListener's onKernelResponse listener as it is used (davedevelopment) + * bug #12864 [Console][Table] Fix cell padding with multi-byte (ttsuruoka) + * bug #13201 [FrameworkBundle][config cmd] initialize extension. (aitboudad) + * bug #13375 [YAML] Fix one-liners to work with multiple new lines (Alex Pott) + * bug #13545 fixxed order of usage (OskarStark) + * bug #13577 [HttpKernel] Added use of instantiateController method provided in (#12022) (stavichenko) + * bug #13567 [Routing] make host matching case-insensitive (Tobion) + +* 2.6.4 (2015-02-02) + + * bug #13489 [VarDumper] CSS fix (Arrilot) + * bug #13441 [VarDumper] fix handling of non-UTF8 strings (nicolas-grekas) + * bug #13528 [Validator] reject ill-formed strings (nicolas-grekas) + * bug #13525 [Validator] UniqueEntityValidator - invalidValue fixed. (Dawid Sajdak) + * bug #13549 [EventDispatcher] Wrong EventDispatcher instance injected in listeners (dosten) + * bug #13527 [Validator] drop grapheme_strlen in LengthValidator (nicolas-grekas) + * bug #12649 [Hackday] [Toolbar] Fix info position and icons on small screens (stefanosala) + * bug #13376 [FrameworkBundle][config] allow multiple fallback locales. (aitboudad) + * bug #13473 [FrameworkBundle][xsd] added missing logging attribute. (aitboudad) + * bug #12972 Make the container considered non-fresh if the environment parameters are changed (thewilkybarkid) + * bug #13309 [Console] fixed 10531 (nacmartin) + * bug #13352 [Yaml] fixed parse shortcut Key after unindented collection. (aitboudad) + * bug #13501 [TwigBridge] Fix bootstrap rendering when user explicitly use form_label (lyrixx) + * bug #13487 [VarDumper] Fixed HtmlDumper with long string (alOneh) + * bug #13343 [FrameworkBundle] FormDataCollector should be loaded only if form config is enabled (hason) + * bug #12258 [FrameworkBundle] print error message if server couldn't be started (xabbuh) + * bug #13039 [HttpFoundation] [Request] fix baseUrl parsing to fix wrong path_info (rk3rn3r) + * bug #13250 [Twig][Bridge][TranslationDefaultDomain] add support of named arguments. (aitboudad) + * bug #13332 [Console] ArgvInput and empty tokens (Taluu) + * bug #13353 [DependencyInjection] Fix missing ExpressionLanguageProviders (szicsu) + * bug #13293 [EventDispatcher] Add missing checks to RegisterListenersPass (znerol) + * bug #13262 [Yaml] Improve YAML boolean escaping (petert82, larowlan) + * bug #13420 [Debug] fix loading order for legacy classes (nicolas-grekas) + * bug #13421 [FrameworkBundle] fix routing descriptor for options (Tobion) + * bug #13405 [TwigBridge] exit when Twig environment is not set in the LintCommand (xabbuh) + * bug #13351 [VarDumper] fix very special vars handling (nicolas-grekas) + * bug #13371 fix missing comma in YamlDumper (garak) + * bug #13365 [HttpFoundation] Make use of isEmpty() method (xelaris) + * bug #13355 [Console] Helper\Table->addRow optimization (boekkooi) + * bug #13347 [Console] Helper\TableHelper->addRow optimization (boekkooi) + * bug #13346 [PropertyAccessor] Allow null value for a array (2.3) (boekkooi) + * bug #13170 [Form] Set a child type to text if added to the form without a type. (jakzal) + * bug #13334 [Yaml] Fixed #10597: Improved Yaml directive parsing (VictoriaQ) + * bug #13198 [Form] Fixed check of violation constraint #12792 (xelaris) + * bug #13316 [Form] fixed the CSRF extension to allow using only the new interfaces (fabpot) + * bug #13307 added missing support for factories in console descriptions (fabpot) + * bug #13305 [FrameworkBundle] fixed missing information in some descriptors (fabpot) + +* 2.6.3 (2015-01-07) + + * bug #13286 [Security] Don't destroy the session on buggy php releases. (derrabus) + +* 2.6.2 (2015-01-07) + + * feature #13241 [Form] add back model_timezone and view_timezone options (xabbuh) + * bug #13297 [Process] Fix input reset in WindowsPipes (mpajunen) + * bug #12417 [HttpFoundation] Fix an issue caused by php's Bug #66606. (wusuopu) + * bug #13200 Don't add Accept-Range header on unsafe HTTP requests (jaytaph) + * bug #12491 [Security] Don't send remember cookie for sub request (blanchonvincent) + * bug #12574 [HttpKernel] Fix UriSigner::check when _hash is not at the end of the uri (nyroDev) + * bug #13185 Fixes Issue #13184 - incremental output getters now return empty strings (Bailey Parker) + * bug #13153 [TwigBridge] bootstrap_3_layout.html.twig inline form rendering button problem fix #13150 (edvinasme) + * bug #13183 [DependencyInjection] force ExpressionLanguage version >= 2.6 (xabbuh) + * bug #13173 [Debug] fixes ClassNotFoundFatalErrorHandler to correctly handle class not found errors with Symfony ClassLoader component autoloaders. (hhamon) + * bug #13166 Fix a web profiler form issue with fields added to the form after the form was built (jakzal) + * bug #12911 Fix wrong DateTransformer timezone param for non-UTC configuration (Soullivaneuh) + * bug #13145 [DomCrawler] Fix behaviour with tag (dkop, WouterJ) + * bug #13027 fix #10054 - form data collector with dynamic fields (zulus) + * bug #13141 [TwigBundle] Moved the setting of the default escaping strategy from the Twig engine to the Twig environment (fabpot) + * bug #13114 [HttpFoundation] fixed error when an IP in the X-Forwarded-For HTTP head... (fabpot) + * bug #12572 [HttpFoundation] fix checkip6 (Neime) + * bug #13109 [Filesystem] restore ability to create broken symlinks (nicolas-grekas) + * bug #13093 [TwigBundle] added missing absolute URL in Twig exceptions (fabpot) + * bug #13087 [DependencyInjection] use/fix newest Definition::setFactory (nicolas-grekas) + * bug #12975 [FrameworkBundle] Allow custom services for validator mapping cache. (jakzal) + * bug #13068 Add LegacyPdoSessionHandler class (jeremylivingston) + * bug #13075 [Config] fix error handler restoration in test (nicolas-grekas) + * bug #13073 [VarDumper] fix and test PdoCaster (nicolas-grekas) + * bug #13085 [FrameworkBundle] Fix dependency on ExtensionInterface over implementation (xphere) + * bug #13081 [FrameworkBundle] forward error reporting level to insulated Client (nicolas-grekas) + * bug #13053 [FrameworkBundle] Fixed Translation loader and update translation command. (saro0h) + * bug #12900 [WebProfilerBundle] Fixed IE8 support (korotovsky) + * bug #13047 [FrameworkBundle][Logging Translator] skip if param "translator.logging" doesn't exist. (aitboudad) + * bug #13048 [Security] Delete old session on auth strategy migrate (xelaris) + * bug #13035 Added the function providers as container resources (stof) + * bug #13021 [FrameworkBundle] skip compiler pass if interface doesn't exist (xabbuh) + * bug #12999 [FrameworkBundle] fix cache:clear command (nicolas-grekas) + * bug #13004 add a limit and a test to FlattenExceptionTest. (Daniel Wehner) + * bug #13013 Unify the way to provide expression functions for the DI container (stof) + * bug #13009 [DebugBundle] fix link format handling with disabled templating (xabbuh) + * bug #12996 [WebProfilerBundle] Fix placeholder date format (mvar) + * bug #12961 fix session restart on PHP 5.3 (Tobion) + * bug #12548 [Form] fixed a maxlength overring on a guessing (origaminal) + * bug #12761 [Filesystem] symlink use RealPath instead LinkTarget (aitboudad) + * bug #12848 [EventDispatcher] Fixed #12845 adding a listener to an event that is currently being dispatched (Pieter Jordaan) + * bug #12935 [Security] Fixed ExpressionVoter - addExpressionLanguageProvider (Luca Genuzio) + * bug #12855 [DependencyInjection] Perf php dumper (nicolas-grekas) + * bug #12899 [WebProfiler] Tweaked ajax requests toolbar css reset (1ed) + * bug #12913 Fix missing space in label_attr (garak) + * bug #12894 [FrameworkBundle][Template name] avoid error message for the shortcut n... (aitboudad) + * bug #12806 [Console] Removed the use of $this->getHelperSet() as it is null by default (saro0h) + * bug #12858 [ClassLoader] Fix undefined index in ClassCollectionLoader (szicsu) + +* 2.6.1 (2014-12-03) + + * bug #12823 [DependencyInjection] fix PhpDumper (nicolas-grekas) + * bug #12811 Configure firewall's kernel exception listener with configured entry point or a default entry point (rjkip) + * bug #12770 [Filesystem] fix lock file permissions (nicolas-grekas) + * bug #12784 [DependencyInjection] make paths relative to __DIR__ in the generated container (nicolas-grekas) + * bug #12716 [ClassLoader] define constant only if it wasn't defined before (xabbuh) + +* 2.6.0 (2014-11-28) + + * bug #12553 [Debug] fix error message on double exception (nicolas-grekas) + * bug #12550 [FrameworkBundle] backport #12489 (xabbuh) + * bug #12437 [Validator] make DateTime objects represented as strings in the violation message (hhamon) + * bug #12575 [WebProfilerBundle] Remove usage of app.request in search bar template (jeromemacias) + * bug #12570 Fix initialized() with aliased services (Daniel Wehner) + +* 2.6.0-BETA2 (2014-11-23) + + * bug #12555 [Debug] fix ENT_SUBSTITUTE usage (nicolas-grekas) + * feature #12538 [FrameworkBundle] be smarter when guessing the document root (xabbuh) + * bug #12539 [TwigBundle] properly set request attributes in controller test (xabbuh) + * bug #12267 [Form][WebProfiler] Empty form names fix (kix) + * bug #12137 [FrameworkBundle] cache:clear command fills *.php.meta files with wrong data (Strate) + * bug #12525 [Bundle][FrameworkBundle] be smarter when guessing the document root (xabbuh) + * bug #12296 [SecurityBundle] Authentication entry point is only registered with firewall exception listener, not with authentication listeners (rjkip) + * bug #12446 [Twig/DebugBundle] move dump extension registration (nicolas-grekas) + * bug #12489 [FrameworkBundle] Fix server run in case the router script does not exist (romainneutron) + * feature #12404 [Form] Remove timezone options from DateType and TimeType (jakzal) + * bug #12487 [DomCrawler] Added support for 'link' tags in the Link class (StephaneSeng) + * bug #12490 [FrameworkBundle] Fix server start in case the PHP binary is not found (romainneutron) + * bug #12443 [HttpKernel] Adding support for invokable controllers in the RequestDataCollector (jameshalsall) + * bug #12393 [DependencyInjection] inlined factory not referenced (boekkooi) + * bug #12411 [VarDumper] Use Unicode Control Pictures (nicolas-grekas) + * bug #12436 [Filesystem] Fixed case for empty folder (yosmanyga) + +* 2.6.0-BETA1 (2014-11-03) + + * first beta release + diff --git a/CHANGELOG-2.7.md b/CHANGELOG-2.7.md new file mode 100644 index 000000000000..235f166764c2 --- /dev/null +++ b/CHANGELOG-2.7.md @@ -0,0 +1,695 @@ +CHANGELOG for 2.7.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 2.7 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.7.0...v2.7.1 + +* 2.7.14 (2016-06-06) + + * bug #18908 [DependencyInjection] force enabling the external XML entity loaders (xabbuh) + * bug #18893 [DependencyInjection] Skip deep reference check for 'service_container' (RobertMe) + * bug #18812 Catch \Throwable (fprochazka) + * bug #18821 [Form] Removed UTC specification with timestamp (francisbesset) + * bug #18861 Fix for #18843 (inso) + * bug #18907 [Routing] Fix the annotation loader taking a class constant as a beginning of a class name (jakzal, nicolas-grekas) + * bug #18879 [Console] SymfonyStyle: Align multi-line/very-long-line blocks (chalasr) + * bug #18864 [Console][DX] Fixed ambiguous error message when using a duplicate option shortcut (peterrehm) + * bug #18883 Fix js comment in profiler (linnaea) + * bug #18844 [Yaml] fix exception contexts (xabbuh) + * bug #18840 [Yaml] properly handle unindented collections (xabbuh) + * bug #18813 Catch \Throwable (fprochazka) + * bug #18839 People - person singularization (Keeo) + * bug #18828 [Yaml] chomp newlines only at the end of YAML documents (xabbuh) + * bug #18814 Fixed server status command when port has been omitted (peterrehm) + * bug #18799 Use levenshtein level for better Bundle matching (j0k3r) + * bug #18413 [WebProfilerBundle] Fix CORS ajax security issues (romainneutron) + * bug #18507 [BUG] Delete class 'control-group' in bootstrap 3 (Philippe Degeeter) + * bug #18747 [Form] Modified iterator_to_array's 2nd parameter to false in ViolationMapper (issei-m) + * bug #18635 [Console] Prevent fatal error when calling Command::getHelper without helperSet (chalasr) + * bug #18686 [console][table] adjust width of colspanned cell. (aitboudad) + * bug #18761 [Form] Modified iterator_to_array's 2nd parameter to false in ViolationMapper (issei-m) + * bug #18737 [Debug] Fix fatal error handlers on PHP 7 (nicolas-grekas) + +* 2.7.13 (2016-05-09) + + * security #18733 limited the maximum length of a submitted username (fabpot) + * bug #18730 [FrameworkBundle] prevent calling get() for service_container service (xabbuh) + * bug #18709 [DependencyInjection] top-level anonymous services must be public (xabbuh) + * bug #18692 add @Event annotation for KernelEvents (Haehnchen) + * bug #18246 [DependencyInjection] fix ambiguous services schema (backbone87) + +* 2.7.12 (2016-04-29) + + * bug #18180 [Form] fixed BC break with pre selection of choices with `ChoiceType` and its children (HeahDude) + * bug #18562 [WebProfilerBunde] Give an absolute url in case the request occured from another domain (romainneutron) + * bug #18603 [PropertyAccess] ->getValue() should be read-only (nicolas-grekas) + * bug #18593 [VarDumper] Fix dumping type hints for non-existing parent classes (nicolas-grekas) + * bug #18581 [Console] [TableHelper] make it work with SymfonyStyle. (aitboudad) + * bug #18280 [Routing] add query param if value is different from default (Tobion) + * bug #18496 [Console] use ANSI escape sequences in ProgressBar overwrite method (alekitto) + * bug #18491 [DependencyInjection] anonymous services are always private (xabbuh) + * bug #18515 [Filesystem] Better error handling in remove() (nicolas-grekas) + * bug #18449 [PropertyAccess] Fix regression (nicolas-grekas) + * bug #18429 [Console] Correct time formatting. (camporter) + * bug #18467 [DependencyInjection] Resolve aliases before removing abstract services + add tests (nicolas-grekas) + * bug #18460 [DomCrawler] Fix select option with empty value (Matt Wells) + * bug #18425 [Security] Fixed SwitchUserListener when exiting an impersonation with AnonymousToken (lyrixx) + * bug #18317 [Form] fix "prototype" not required when parent form is not required (HeahDude) + * bug #18439 [Logging] Add support for Firefox (43+) in ChromePhpHandler (arjenm) + * bug #18385 Detect CLI color support for Windows 10 build 10586 (mlocati) + * bug #18426 [EventDispatcher] Try first if the event is Stopped (lyrixx) + * bug #18394 [FrameworkBundle] Return the invokable service if its name is the class name (dunglas) + * bug #18265 Optimize ReplaceAliasByActualDefinitionPass (ajb-in) + * bug #18349 [Process] Fix stream_select priority when writing to stdin (nicolas-grekas) + * bug #18358 [Form] NumberToLocalizedStringTransformer should return floats when possible (nicolas-grekas) + * bug #17926 [DependencyInjection] Enable alias for service_container (hason) + * bug #18352 [Debug] Fix case sensitivity checks (nicolas-grekas) + * bug #18336 [Debug] Fix handling of php7 throwables (nicolas-grekas) + * bug #18354 [FrameworkBundle][TwigBridge] fix high deps tests (xabbuh) + * bug #18312 [ClassLoader] Fix storing not-found classes in APC cache (nicolas-grekas) + * bug #18298 [Validator] do not treat payload as callback (xabbuh) + +* 2.7.11 (2016-03-25) + + * bug #18255 [HttpFoundation] Fix support of custom mime types with parameters (Ener-Getick) + * bug #18272 [Bridge\PhpUnit] Workaround old phpunit bug, no colors in weak mode, add tests (nicolas-grekas) + * bug #18259 [PropertyAccess] Backport fixes from 2.7 (nicolas-grekas) + * bug #18261 [PropertyAccess] Fix isPropertyWritable not using the reflection cache (nicolas-grekas) + * bug #18224 [PropertyAccess] Remove most ref mismatches to improve perf (nicolas-grekas) + * bug #18210 [PropertyAccess] Throw an UnexpectedTypeException when the type do not match (dunglas, nicolas-grekas) + * bug #18216 [Intl] Fix invalid numeric literal on PHP 7 (nicolas-grekas) + * bug #18147 [Validator] EmailValidator cannot extract hostname if email contains multiple @ symbols (natechicago) + * bug #18023 [Process] getIncrementalOutput should work without calling getOutput (romainneutron) + * bug #18175 [Translation] Add support for fuzzy tags in PoFileLoader (nud) + * bug #18179 [Form] Fix NumberToLocalizedStringTransformer::reverseTransform with big integers (ovrflo, nicolas-grekas) + * bug #18164 [HttpKernel] set s-maxage only if all responses are cacheable (xabbuh) + * bug #18150 [Process] Wait a bit less on Windows (nicolas-grekas) + * bug #18130 [Debug] Replaced logic for detecting filesystem case sensitivity (Dan Blows) + * bug #18080 [HttpFoundation] Set the Content-Range header if the requested Range is unsatisfied (jakzal) + * bug #18084 [HttpFoundation] Avoid warnings when checking malicious IPs (jakzal) + * bug #18066 [Process] Fix pipes handling (nicolas-grekas) + * bug #18078 [Console] Fix an autocompletion question helper issue with non-sequentially indexed choices (jakzal) + * bug #18048 [HttpKernel] Fix mem usage when stripping the prod container (nicolas-grekas) + * bug #18065 [Finder] Partially revert #17134 to fix a regression (jakzal) + * bug #18018 [HttpFoundation] exception when registering bags for started sessions (xabbuh) + * bug #18054 [Filesystem] Fix false positive in ->remove() (nicolas-grekas) + * bug #18049 [Validator] Fix the locale validator so it treats a locale alias as a valid locale (jakzal) + * bug #18019 [Intl] Update ICU to version 55 (jakzal) + * bug #18015 [Process] Fix memory issue when using large input streams (romainneutron) + * bug #16656 [HttpFoundation] automatically generate safe fallback filename (xabbuh) + * bug #15794 [Console] default to stderr in the console helpers (alcohol) + * bug #17984 Allow to normalize \Traversable when serializing xml (Ener-Getick) + * bug #17434 Improved the error message when a template is not found (rvanginneken, javiereguiluz) + * bug #17687 Improved the error message when using "@" in a decorated service (javiereguiluz) + * bug #17744 Improve error reporting in router panel of web profiler (javiereguiluz) + * bug #17894 [FrameworkBundle] Fix a regression in handling absolute template paths (jakzal) + * bug #17990 [DoctrineBridge][Form] Fix performance regression in EntityType (kimlai) + * bug #17595 [HttpKernel] Remove _path from query parameters when fragment is a subrequest (cmenning) + * bug #17986 [DomCrawler] Dont use LIBXML_PARSEHUGE by default (nicolas-grekas) + * bug #17668 add 'guid' to list of exception to filter out (garak) + * bug #17615 Ensure backend slashes for symlinks on Windows systems (cpsitgmbh) + * bug #17626 Try to delete broken symlinks (IchHabRecht) + * bug #17978 [Yaml] ensure dump indentation to be greather than zero (xabbuh) + * bug #16886 [Form] [ChoiceType] Prefer placeholder to empty_value (boite) + * bug #17976 [WebProfilerBundle] fix debug toolbar rendering by removing inadvertently added links (craue) + * bug #17971 Variadic controller params (NiR-, fabpot) + * bug #17568 Improved Bootstrap form theme for hidden fields (javiereguiluz) + * bug #17925 [Bridge] The WebProcessor now forwards the client IP (magnetik) + +* 2.7.10 (2016-02-28) + + * bug #17947 Fix - #17676 (backport #17919 to 2.3) (Ocramius) + * bug #17942 Fix bug when using an private aliased factory service (WouterJ) + * bug #17798 [Form] Fix BC break by allowing 'choice_label' option to be 'false' in ChoiceType (HeahDude) + * bug #17542 ChoiceFormField of type "select" could be "disabled" (bouland) + * bug #17602 [HttpFoundation] Fix BinaryFileResponse incorrect behavior with if-range header (bburnichon) + * bug #17760 [Form] fix choice value "false" in ChoiceType (HeahDude) + * bug #17914 [Console] Fix escaping of trailing backslashes (nicolas-grekas) + * bug #17074 Fix constraint validator alias being required (Triiistan) + * bug #17866 [DependencyInjection] replace alias in factories (xabbuh) + * bug #17867 [DependencyInjection] replace alias in factory services (xabbuh) + * bug #17569 [FrameworkBundle] read commands from bundles when accessing list (havvg) + * bug #16987 [FileSystem] Windows fix (flip111) + * bug #17787 [Form] Fix choice placeholder edge cases (Tobion) + * bug #17835 [Yaml] fix default timezone to be UTC (xabbuh) + * bug #17823 [DependencyInjection] fix dumped YAML string (xabbuh) + * bug #17818 [Console] InvalidArgumentException is thrown under wrong condition (robinkanters) + * bug #17819 [HttpKernel] Prevent a fatal error when DebugHandlersListener is used with a kernel with no terminateWithException() method (jakzal) + * bug #17814 [DependencyInjection] fix dumped YAML snytax (xabbuh) + * bug #17099 [Form] Fixed violation mapping if multiple forms are using the same (or part of the same) property path (alekitto) + * bug #17694 [DoctrineBridge] [Form] fix choice_value in EntityType (HeahDude) + * bug #17719 [DependencyInjection] fixed exceptions thrown by get method of ContainerBuilder (lukaszmakuch) + * bug #17742 [DependencyInjection] Fix #16461 Container::set() replace aliases (mnapoli) + * bug #17745 Added more exceptions to singularify method (javiereguiluz) + * bug #17691 Fixed (string) catchable fatal error for PHP Incomplete Class instances (yceruto) + * bug #17766 Fixed (string) catchable fatal error for PHP Incomplete Class instances (yceruto) + * bug #17757 [HttpFoundation] BinaryFileResponse sendContent return as parent. (2.3) (SpacePossum) + * bug #17702 [TwigBridge] forward compatibility with Yaml 3.1 (xabbuh) + * bug #17672 [DependencyInjection][Routing] add files used in FileResource objects (xabbuh) + * bug #17600 Fixed the Bootstrap form theme for inlined checkbox/radio (javiereguiluz) + * bug #17596 [Translation] Add resources from fallback locale to parent catalogue (c960657) + * bug #17605 [FrameworkBundle] remove default null value for asset version (xabbuh) + * bug #17606 [DependencyInjection] pass triggerDeprecationError arg to parent class (xabbuh) + * bug #16956 [DependencyInjection] XmlFileLoader: enforce tags to have a name (xabbuh) + * bug #16265 [BrowserKit] Corrected HTTP_HOST logic (Naktibalda) + * bug #17554 [DependencyInjection] resolve aliases in factories (xabbuh) + * bug #17555 [DependencyInjection] resolve aliases in factory services (xabbuh) + * bug #17511 [Form] ArrayChoiceList can now deal with a null in choices (issei-m) + * bug #17430 [Serializer] Ensure that groups are strings (dunglas) + * bug #15272 [FrameworkBundle] Fix template location for PHP templates (jakzal) + * bug #11232 [Routing] Fixes fatal errors with object resources in AnnotationDirectoryLoader::supports (Tischoi) + * bug #17526 Escape the delimiter in Glob::toRegex (javiereguiluz) + * bug #17527 fixed undefined variable (fabpot) + * bug #15706 [framework-bundle] Added support for the `0.0.0.0/0` trusted proxy (zerkms) + * bug #16274 [HttpKernel] Lookup the response even if the lock was released after two second wait (jakzal) + * bug #17355 [DoctrineBridge][Validator] >= 2.3 Pass association instead of ID as argument (xavismeh) + * bug #17454 Allow absolute URLs to be displayed in the debug toolbar (javiereguiluz) + * bug #16736 [Request] Ignore invalid IP addresses sent by proxies (GromNaN) + * bug #17486 [FrameworkBundle] Throw for missing container extensions (kix) + * bug #16873 Able to load big xml files with DomCrawler (zorn-v) + * bug #16897 [Form] Fix constraints could be null if not set (DZunke) + * bug #16912 [Translation][Writer] avoid calling setBackup if the dumper is not FileDumper (aitboudad) + * bug #17505 sort bundles in config:dump-reference command (xabbuh) + * bug #17514 [Asset] Add defaultNull to version configuration (ewgRa) + * bug #16511 [Asset] Ability to set empty version strategy in packages (ewgRa) + * bug #17503 [Asset] CLI: use request context to generate absolute URLs (xabbuh) + * bug #17478 [HttpFoundation] Do not overwrite the Authorization header if it is already set (jakzal) + * bug #17461 [Yaml] tag for dumped PHP objects must be a local one (xabbuh) + * bug #17456 [DX] Remove default match from AbstractConfigCommand::findExtension (kix) + * bug #17424 [Process] Update in 2.7 for stream-based output storage (romainneutron) + * bug #17423 [Process] Use stream based storage to avoid memory issues (romainneutron) + * bug #17406 [Form] ChoiceType: Fix a notice when 'choices' normalizer is replaced (paradajozsef) + * bug #17433 [FrameworkBundle] Don't log twice with the error handler (nicolas-grekas) + * bug #17418 Fixed Bootstrap form theme form "reset" buttons (javiereguiluz) + * bug #17404 fix merge 2.3 into 2.7 for SecureRandom dependency (Tobion) + * bug #17373 [SecurityBundle] fix SecureRandom service constructor args (Tobion) + * bug #17380 [TwigBridge] Use label_format option for checkbox and radio labels (enumag) + * bug #17377 Fix performance (PHP5) and memory (PHP7) issues when using token_get_all (nicolas-grekas, peteward) + * bug #17389 [Routing] Fixed correct class name in thrown exception (fixes #17388) (robinvdvleuten) + * bug #17358 [ClassLoader] Use symfony/polyfill-apcu (nicolas-grekas) + * bug #17370 [HttpFoundation][Cookie] Cookie DateTimeInterface fix (wildewouter) + +* 2.7.9 (2016-01-14) + + * security #17359 do not ship with a custom rng implementation (xabbuh, fabpot) + * bug #17314 Fix max width for multibyte keys in choice question (mheki) + * bug #17326 [Console] Display console application name even when no version set (polc) + * bug #17328 [Serializer] Allow to use proxies in object_to_populate (dunglas) + * bug #17347 Workaround https://bugs.php.net/63206 (nicolas-grekas) + * bug #17140 [Serializer] Remove normalizer cache in Serializer class (jvasseur) + * bug #17307 [FrameworkBundle] Fix paths with % in it (like urlencoded) (scaytrase) + * bug #17078 [Bridge] [Doctrine] [Validator] Added support \IteratorAggregate for UniqueEntityValidator (Disparity) + * bug #17298 [FrameworkBundle] Use proper class to fetch $versionStrategy property (dosten) + * bug #17287 [HttpKernel] Forcing string comparison on query parameters sort in UriSigner (Tim van Densen) + * bug #17279 [FrameworkBundle] Add case in Kernel directory guess for PHPUnit (tgalopin) + * bug #17278 [FrameworkBundle] Add case in Kernel directory guess for PHPUnit (tgalopin) + * bug #17275 [PhpUnitBridge] Re-enable the garbage collector (nicolas-grekas) + * bug #17276 [Process] Fix potential race condition (nicolas-grekas) + * bug #17183 [FrameworkBundle] Set the kernel.name properly after a cache warmup (jakzal) + * bug #17159 [Yaml] recognize when a block scalar is left (xabbuh) + * bug #17195 bug #14246 [Filesystem] dumpFile() non atomic (Hidde Boomsma) + * feature #16747 [Form] Improved performance of ChoiceType and its subtypes (webmozart) + * bug #17177 [Process] Fix potential race condition leading to transient tests (nicolas-grekas) + * bug #17163 [Form] fix Catchable Fatal Error if choices is not an array (Gladhon, nicolas-grekas) + * bug #17119 [Form] improve deprecation message for "empty_value" and "choice_list" options. (hhamon) + +* 2.7.8 (2015-12-26) + + * bug #16864 [Yaml] fix indented line handling in folded blocks (xabbuh) + * bug #17052 Fixed flatten exception recursion with errors (GrahamCampbell) + * bug #16826 Embedded identifier support (mihai-stancu) + * bug #17079 Also transform inline mappings to objects (WouterJ) + * bug #17129 [Config] Fix array sort on normalization in edge case (romainneutron) + * bug #17094 [Process] More robustness and deterministic tests (nicolas-grekas) + * bug #17112 [PropertyAccess] Reorder elements array after PropertyPathBuilder::replace (alekitto) + * bug #16797 [Filesystem] Recursively widen non-executable directories (Slamdunk) + * bug #17040 [Console] Avoid extra blank lines when rendering exceptions (ogizanagi) + * bug #17055 [Security] Verify if a password encoded with bcrypt is no longer than 72 characters (jakzal) + * bug #16959 [Form] fix #15544 when a collection type attribute "required" is false, "prototype" should too (HeahDude) + * bug #16860 [Yaml] do not remove "comments" in scalar blocks (xabbuh) + * bug #17002 [Console][Table] fixed render row that contains multiple cells. (aitboudad) + * bug #16971 [HttpFoundation] Added the ability of using BinaryFileResponse with stream wrappers (jakzal, Sander-Toonen) + * bug #17048 Fix the logout path when not using the router (stof) + * bug #17049 Fix the logout path when not using the router (stof) + * bug #17057 [FrameworkBundle][HttpKernel] the finder is required to discover bundle commands (xabbuh) + * bug #17006 [Form] Fix casting regression in DoctrineChoiceLoader (bendavies) + * bug #16915 [Process] Enhance compatiblity with --enable-sigchild (nicolas-grekas) + * bug #16829 [FrameworkBundle] prevent cache:clear creating too long paths (Tobion) + * bug #16921 Fix short array syntax for php 5.3 (ewgRa) + * bug #16450 [Serializer] Fixed `array_unique` on array of objects in `getAllowedAttributes`. (CornyPhoenix) + * bug #16757 [FrameworkBundle] [Translation] Fixed translations not written when no translations directory in update command (jeremyFreeAgent) + * bug #16871 [FrameworkBundle] Disable built-in server commands when Process component is missing (gnugat, xabbuh) + * bug #16870 [FrameworkBundle] Disable the server:run command when Process component is missing (gnugat, xabbuh) + * bug #16742 [Console][ProgressBar] redrawFrequency should never be 0 (dritter) + * bug #16799 Improve error message for undefined DIC aliases (mpdude) + * bug #16825 [VarDumper] fix .sf-dump z-index (debug bar conflict) (Antoine LA) + * bug #16772 Refactoring EntityUserProvider::__construct() to not do work, cause cache warm error (weaverryan) + * bug #16753 [Process] Fix signaling/stopping logic on Windows (nicolas-grekas) + * bug #16733 [Console] do not encode backslashes in console default description (Tobion) + * bug #16312 [HttpKernel] clearstatcache() so the Cache sees when a .lck file has been released (mpdude) + * bug #16351 [WIP] [Form] [TwigBridge] Bootstrap horizontal theme missing tests (pieter2627) + * bug #16685 [Form] Fixed: Duplicate choice labels are remembered when using "choices_as_values" = false (webmozart) + * bug #16705 [Form] Deprecated setting "choices_as_values" to "false" (webmozart) + * bug #16695 [SecurityBundle] disable the init:acl command if ACL is not used (Tobion) + * bug #16679 [Form] Disabled view data validation if "data_class" is set to null (webmozart) + * bug #16676 [HttpFoundation] Workaround HHVM rewriting HTTP response line (nicolas-grekas) + * bug #16668 [ClassLoader] Fix parsing namespace when token_get_all() is missing (nicolas-grekas) + * bug #16386 Bug #16343 [Router] Too many Routes ? (jelte) + * bug #16651 [Debug] Ensure class declarations are loaded only once (nicolas-grekas) + +* 2.7.7 (2015-11-23) + + * security #16631 CVE-2015-8124: Session Fixation in the "Remember Me" Login Feature (xabbuh) + * security #16630 CVE-2015-8125: Potential Remote Timing Attack Vulnerability in Security Remember-Me Service (xabbuh) + * bug #16588 Sent out a status text for unknown HTTP headers. (dawehner) + * bug #16295 [DependencyInjection] Unescape parameters for all types of injection (Nicofuma) + * bug #16574 [Process] Fix PhpProcess with phpdbg runtime (nicolas-grekas) + * bug #16578 [Console] Fix bug in windows detection (kbond) + * bug #16546 [Serializer] ObjectNormalizer: don't serialize static methods and props (dunglas) + * bug #16352 Fix the server variables in the router_*.php files (leofeyer) + * bug #16537 [Validator] Allow an empty path with a non empty fragment or a query (jakzal) + * bug #16528 [Translation] Add support for Armenian pluralization. (marcosdsanchez) + * bug #16510 [Process] fix Proccess run with pts enabled (ewgRa) + * bug #16292 fix race condition at mkdir (#16258) (ewgRa) + * bug #15945 [Form] trigger deprecation warning when using empty_value (xabbuh) + * bug #16384 [FrameworkBundle] JsonDescriptor - encode container params only once (xabbuh) + * bug #16480 [VarDumper] Fix PHP7 type-hints compat (nicolas-grekas) + * bug #16463 [PropertyAccess] Port of the performance optimization from 2.3 (dunglas) + * bug #16462 [PropertyAccess] Fix dynamic property accessing. (dunglas) + * bug #16454 [Serializer] GetSetNormalizer shouldn't set/get static methods (boekkooi) + * bug #16453 [Serializer] PropertyNormalizer shouldn't set static properties (boekkooi) + * bug #16471 [VarDumper] Fix casting for ReflectionParameter (nicolas-grekas) + * bug #16294 [PropertyAccess] Major performance improvement (dunglas) + * bug #16331 fixed Twig deprecation notices (fabpot) + * bug #16306 [DoctrineBridge] Fix issue which prevent the profiler to explain a query (Baachi) + * bug #16359 Use mb_detect_encoding with $strict = true (nicolas-grekas) + * bug #16144 [Security] don't allow to install the split Security packages (xabbuh) + +* 2.7.6 (2015-10-27) + + * bug #16338 [VarDumper] Fix anonymous class dumping (nicolas-grekas) + * bug #16288 [Process] Inherit env vars by default in PhpProcess (nicolas-grekas) + * bug #16302 [DoctrineBridge] Fix required guess of boolean fields (enumag) + * bug #16298 Changed one console output style to avoid visual issues (javiereguiluz) + * bug #16291 [VarDumper] Fix return type and anonymous classes dumping (nicolas-grekas) + * bug #16177 [HttpFoundation] Fixes /0 subnet handling in IpUtils (ultrafez) + * bug #16262 [TwigBundle] Fix Twig cache is not properly warmed (tucksaun) + * bug #16259 [Validator] Allow an empty path in a URL with only a fragment or a query (jakzal) + * bug #16226 [filesystem] makeRelativePath does not work correctly from root (jaytaph, fabpot) + * bug #16108 [Security] #15764. Use SessionAuthenticationStrategy on RememberMe login (s12v) + * bug #16196 [Console] Fix progress bar formatting when max is set on start() and some other edge cases (vsychov, fabpot) + * bug #16183 [VarDumper] Fix wordwrap with Bootstrap (ogizanagi) + * bug #16182 [Process] Workaround buggy PHP warning (cbj4074) + * bug #16095 [Console] Add additional ways to detect OS400 platform (johnkary) + * bug #15793 [Yaml] Allow tabs before comments at the end of a line (superdav42) + * bug #16152 Fix URL validator failure with empty string (fabpot, bocharsky-bw) + * bug #15121 fixed #15118 [Filesystem] mirroring a symlink copies absolute file path (danepowell) + * bug #15161 avoid duplicated path with addPrefix (remicollet) + * bug #16146 [Security] sync translations and add a test for it (xabbuh) + * bug #16133 compatibility with Security component split (xabbuh) + * bug #16123 Command list ordering fix (spdionis, fabpot) + * bug #14842 [Security][bugfix] "Remember me" cookie cleared on logout with custom "secure"/"httponly" config options (MacDada) + * bug #13627 [Security] InMemoryUserProvider now concerns whether user's password is changed when refreshing (issei-m) + * bug #16090 Fix PropertyAccessor modifying array in object when array key does no… (pierredup) + * bug #16092 [Process] Throw exception if tempnam returns false (pierredup) + * bug #16111 Throw exception if tempnam returns false in ProcessPipes (pierredup) + * bug #16087 Fixing typo in variable name (yceruto) + * bug #16053 [Console] use PHP_OS instead of php_uname('s') (xabbuh) + * bug #15860 [Yaml] Fix improper comments removal (ogizanagi) + * bug #16050 [TwigBundle] fix useless and failing test (Tobion) + * bug #16028 [DomCrawler] always pass base href to subcrawlers (xabbuh) + * bug #15482 [Yaml] Improve newline handling in folded scalar blocks (teohhanhui) + * bug #15976 [Console] do not make the getHelp() method smart (xabbuh) + * bug #15799 [HttpFoundation] NativeSessionStorage `regenerate` method wrongly sets storage as started (iambrosi) + * bug #15446 [Twig][Bridge] force space between widget and label in checkbox_radio_label (MatTheCat) + * bug #15533 [Console] Fix input validation when required arguments are missing (jakzal) + * bug #15915 Detect Mintty for color support on Windows (stof) + * bug #15906 Forbid serializing a Crawler (stof) + * bug #15682 [Form] Added exception when setAutoInitialize() is called when locked (jaytaph) + * bug #15776 [TwigBridge] fix with_minutes option in time widget (arduanov) + * bug #15846 [FrameworkBundle] Advanced search templates of bundles (yethee) + * bug #15895 [Security] Allow user providers to be defined in many files (lyrixx) + +* 2.7.5 (2015-09-25) + + * bug #15866 [VarDumper] Fix dump comparison on large arrays (romainneutron) + * bug #15878 [OptionsResolver] Fix catched exception along the dependency tree mistakenly detects cyclic dependencies (lemoinem) + * bug #15795 [Console] Default to stderr for the console helpers (2.7+) (alcohol) + * bug #15821 [EventDispatcher] fix memory leak in getListeners (Tobion) + * bug #15859 [Config] Fix enum default value in Yaml dumper (romainneutron) + * bug #15826 [Finder] Optimize the hot-path (nicolas-grekas) + * bug #15804 [VarDumper] Fix HtmlDumper constructor calling CliDumper's (nicolas-grekas) + * bug #15802 [Finder] Handle filtering of recursive iterators and use it to skip looping over excluded directories (nicolas-grekas) + * bug #15803 [Finder] Exclude files based on path before applying the sorting (stof) + * feature #13761 Automatically process extensions when they implement CompilerPassInterface (WouterJ) + * bug #13794 [DomCrawler] Invalid uri created from forms if base tag present (danez) + * bug #15728 Use stderr by default when a specific output is not injected (Seldaek) + * bug #15637 Use ObjectManager interface instead of EntityManager (gnat42) + * bug #15783 [Debug] Fix case mismatch detection (nicolas-grekas) + * bug #14802 [HttpKernel] fix broken multiline (sstok) + * bug #14841 [DoctrineBridge] Fixed #14840 (saksmt) + * bug #15770 [Yaml] Fix the parsing of float keys (jmgq) + * bug #15771 [Console] Ensure the console output is only detected as decorated when both stderr and stdout support colors (Seldaek) + * bug #15750 Add tests to the recently added exceptions thrown from YamlFileLoaders (jakzal) + * bug #15763 [HttpKernel] fix DumpDataCollector compat with Twig 2.0 (nicolas-grekas) + * bug #15718 Fix that two DirectoryResources with different patterns would be deduplicated (mpdude) + * bug #15516 [Translator][warmup][fallback locales] fixed missing cache file generation. (aitboudad) + * bug #14916 [WebProfilerBundle] Added tabindex="-1" to not interfer with normal UX (drAlberT) + * bug #15725 Dispatch console.terminate *after* console.exception (Seldaek) + * bug #15731 improve exceptions when parsing malformed files (xabbuh) + * bug #15737 Fix the injection of the container in invokable controllers (stof) + * bug #15729 [Kernel] Integer version constants (Tobion) + +* 2.7.4 (2015-09-08) + + * bug #15552 [ExpressionLanguage] Fixed expressions cache key generation (inso) + * bug #15527 [Translator][fallback catalogues] fixed circular reference. (aitboudad) + * bug #15662 [Form][ Choice type] fixed groups with empty array. (aitboudad) + * bug #15601 [console] Use the description when no help is available (Nicofuma) + * bug #15649 [VarDumper] Fix missing support for dumping PHP7 return type (nicolas-grekas) + * bug #15603 [HttpKernel] Do not normalize the kernel root directory path #15567 (leofeyer) + * bug #15647 [Debug] Ignore silencing for deprecations (nicolas-grekas) + * bug #15625 Various fixes esp. on Windows (nicolas-grekas) + * bug #15428 Fix the validation of form resources to register the default theme (stof) + * bug #15623 Fix the retrieval of the value with property path when using a loader (stof) + * bug #15619 [Translation] Fix the string casting in the XliffFileLoader (stof) + * bug #15575 Add appveyor.yml for C.I. on Windows (nicolas-grekas) + * bug #15493 [VarDumper] fixed HtmlDumper to target specific the head tag (SaschaDens) + * bug #15611 [Translation][Xliff Loader] Support omitting the node in an .xlf file. (leofeyer) + * bug #15608 [Form] Fix the handling of values for multiple choice types (stof) + * bug #15549 [FrameworkBundle] Fix precedence of xdebug.file_link_format (nicolas-grekas) + * bug #15589 made Symfony compatible with both Twig 1.x and 2.x (fabpot) + * bug #15590 Made Symfony 2.7 compatible with Twig 2.0 (fabpot) + * bug #15535 made Symfony compatible with both Twig 1.x and 2.x (fabpot) + * bug #15561 [Form] only use PropertyPath if not already callable (Tobion) + * bug #15588 [WebProfilerBundle] add import for Twig macro (xabbuh) + * bug #15546 [Form] fixed BC-break on grouped choice lists (origaminal) + * bug #15515 [Console] Fixed warning when command alias is longer than command name (dosten) + * bug #15251 [DoctrineBridge][Form] Fix IdReader when indexing by primary foreign key (giosh94mhz) + * bug #14372 [DoctrineBridge][Form] fix EntityChoiceList when indexing by primary foreign key (giosh94mhz) + * bug #15514 removed _self usage when not needed (fabpot) + * bug #15489 Implement the support of timezone objects in the stub IntlDateFormatter (stof) + * bug #15426 [Serializer] Add support for variadic arguments in the GetSetNormalizer (stof) + * bug #15480 [Yaml] Nested merge keys (mathroc) + * bug #15443 [Debug] Enhance DebugClassLoader performance on MacOSX (nicolas-grekas) + * bug #15445 do not remove space between attributes (greg0ire) + * bug #15263 [HttpFoundation] fixed the check of 'proxy-revalidate' in Response::mustRevalidate() (axiac) + * bug #15425 [Routing] Fix the retrieval of the default value for variadic arguments in the annotation loader (wdalmut, stof) + * bug #15074 Fixing DbalSessionHandler to work with a Oracle "limitation" or bug? (nuncanada) + * bug #13828 [Validator] Improve Iban Validation (afurculita) + * bug #15380 do not dump leading backslashes in class names (xabbuh) + * bug #15376 [ClassMapGenerator] Skip ::class constant (WouterJ) + * bug #15389 [securityBundle] Compare roles strictly when computing inherited roles (bokonet) + * bug #15170 [Config] type specific check for emptiness (xabbuh) + * bug #15411 Fix the handling of null as locale in the stub intl classes (stof) + +* 2.7.3 (2015-07-31) + + * bug #15413 Fix the return value on error for intl methods returning arrays (stof) + * bug #15392 Fix missing _route parameter notice in RouterListener logging case (Haehnchen) + * bug #15390 [php7] Fix for substr() always returning a string (nicolas-grekas) + * bug #15386 [php7] Fix for substr() always returning a string (nicolas-grekas) + * bug #15355 [Security] Do not save the target path in the session for a stateless firewall (lyrixx) + * bug #15306 [HttpKernel] [HttpCache] Fix deprecated error in HttpCache#getSurrogate (m14t) + * bug #15369 [TwigBridge] type-dependent path discovery (marcosdsanchez, xabbuh) + * bug #15361 [Yaml] throw a ParseException on invalid data type (xabbuh) + * bug #15345 [Twig+FrameworkBundle] Fix forward compat with Form 2.8 (nicolas-grekas) + * bug #15330 [Console] Fix console output with closed stdout (jakzal) + * bug #15339 [Serializer] Fix 2 bugs regarding private setters (dunglas) + * bug #15326 [Security] fix check for empty usernames (xabbuh) + * bug #15291 [HttpFoundation] Fix Response::closeOutputBuffers() for HHVM 3.3 (nicolas-grekas) + * bug #15249 [HttpFoundation] [PSR-7] Allow to use resources as content body and to return resources from string content (dunglas) + * bug #15282 [HttpFoundation] Behaviour change in PHP7 for substr (Nicofuma) + * bug #15277 [Form] Fix a BC break in the entity (jakzal) + * bug #15271 fix broken ChoiceQuestion (sstok) + * bug #15250 [PropertyAccess] BC Break since 2.6.5 (Nicolas Macherey) + +* 2.7.2 (2015-07-13) + + * bug #15248 Added 'default' color (jaytaph) + * bug #15243 Reload the session after regenerating its id (jakzal) + * bug #15176 [Serializer] Fix ClassMetadata::sleep() (dunglas) + * bug #15202 [Security] allow to use `method` in XML configs (xabbuh) + * bug #15218 [Twig][Bridge] replaced `extends` with `use` in bootstrap_3_horizontal_layout.html.twig (MatTheCat) + * bug #15223 [Finder] Command::addAtIndex() fails with Command instance argument (thunderer) + * bug #15220 [DependencyInjection] Freeze also FrozenParameterBag::remove (lyrixx) + * bug #15110 Add a way to reset the singleton (dawehner) + * bug #15183 [TwigBridge] fix for legacy asset() with EmptyVersionStrategy (xabbuh) + * bug #15115 [Validator] always evaluate binary format when changed (xabbuh) + * bug #15163 Update DateTimeToArrayTransformer.php (zhil) + * bug #15150 [Translation] Azerbaijani language pluralization rule is wrong (shehi) + * bug #15159 Towards 100% HHVM compat (nicolas-grekas) + * bug #15146 Towards 100% HHVM compat (nicolas-grekas) + * bug #15061 [Form] Fixed handling of choices passed in choice groups (webmozart) + * bug #15145 [Bridge/PhpUnit] Enforce a consistent locale (nicolas-grekas) + * bug #15069 [Form] Fixed: Data mappers always receive forms indexed by their names (webmozart) + * bug #15137 [Security] Initialize SwitchUserEvent::targetUser on attemptExitUser (Rvanlaak, xabbuh) + * bug #15142 Fix choice translation domain for expanded choice widget (jvasseur) + * bug #15126 [Validator] Fix BC for Validator's validate method (michalmarcinkowski) + * bug #15101 [Form] Fixed compatibility with FormTypeInterface implementations that don't extend AbstractType (webmozart) + * bug #15083 [DependencyInjection] Fail when dumping a Definition with no class nor factory (nicolas-grekas) + * bug #15127 [Validator] fix validation for Maestro UK card numbers (xabbuh) + * bug #15128 DbalLogger: Small nonutf8 array fix (vpetrovych, weaverryan) + * bug #15048 [Translation][Form][choice] empty_value shouldn't be translated when it has an empty value (Restless-ET) + * bug #15117 [Form] fixed sending non array data on submit to ResizeListener (BruceWouaigne) + * bug #15122 [Console] respect multi-character shortcuts (xabbuh) + * bug #15012 [Validator] don't trigger deprecation with empty group array (xabbuh) + * bug #15102 [Translation][debug cmd] fixed failing tests. (aitboudad) + * bug #13750 [DependencyInjection] Fixed decoration of service for service with parent (hason) + * bug #15086 Fixed the regexp for the validator of Maestro-based credit/debit cards (javiereguiluz) + * bug #15058 [Console] Fix STDERR output text on IBM iSeries OS400 (johnkary) + * bug #14853 [Validator] more strict e-mail validation regex (xabbuh) + * bug #15064 [Form] Fixed: Support objects with __toString() in choice groups (webmozart) + * bug #15065 [Form] Fixed: remove quoted strings from Intl date formats (e.g. es_ES full pattern) (webmozart) + * bug #15039 [Translation][update cmd] taken account into bundle overrides path. (aitboudad) + * bug #15038 [Translation][debug cmd] taken account into bundle overrides path. (aitboudad) + * bug #14964 [bugfix][MonologBridge] WebProcessor: passing $extraFields to BaseWebProcessor (MacDada) + * bug #14989 [FrameworkBundle] Reuse PropertyAccessor service for ObjectNormalizer (dunglas) + * bug #15036 [VarDumper] Fix dump output for better readability (nicolas-grekas) + * bug #15031 [PhpUnitBridge] Enforce @-silencing of deprecation notices according to new policy (nicolas-grekas) + * bug #15027 [Form] Fixed: Filter non-integers when selecting entities by int ID (webmozart, nicolas-grekas) + * bug #15000 [Debug] Fix fatal-errors handling on HHVM (nicolas-grekas) + * bug #14999 [Debug] Fix fatal-errors handling on HHVM (nicolas-grekas, digitalkaoz) + * bug #14959 [Debug+VarDumper] Fix handling of PHP7 "Throwable" exceptions (nicolas-grekas) + * bug #15010 [Debug] Fix log level of stacked errors (nicolas-grekas) + * bug #15017 [VarDumper] Fix uninitialized id in HtmlDumper (nicolas-grekas) + * bug #14980 Fixed fluent interface (jaytaph) + * bug #14974 [Security][Translation] #14920 update translations (vincentaubert) + * bug #14950 [Form] Fixed: Filter non-integers when selecting entities by int ID (webmozart) + * bug #14930 Bug #14836 [HttpFoundation] Moves default JSON encoding assignment fr… (Incognito) + +* 2.7.1 (2015-06-11) + + * bug #14835 [DependencyInjection] Fixed resolving of service configurators containing Definition objects (webmozart) + * bug #14816 [TwigBridge] Make AppVariable check if security.context exists (ogizanagi) + * bug #14897 Allow new lines in Messages translated with transchoice() (replacement for #14867) (azine) + * bug #14887 [Form] Swap new ChoiceView constructor arguments to ease migrating from the deprecated one (nicolas-grekas) + * bug #14900 Silence deprecation warnings by default (reecefowell) + * bug #14739 [Console] SymfonyStyle : fix blocks wordwrapping (ogizanagi) + * bug #14740 [Console] SymfonyStyle : fix blocks output is broken on windows cmd (ogizanagi) + * bug #14623 [Console] SymfonyStyle : fix & automate block gaps. (ogizanagi) + * bug #14895 [Form] Support DateTimeImmutable in transform() (c960657) + * bug #14891 without this change allways the legacy code get called (dominikzogg) + * bug #14888 [Console] Fix ask and askHidden methods (dosten) + * bug #14705 [Translator] avoid serialize unserializable resources. (aitboudad) + * bug #14846 [console][TableCell] get cell width without decoration. (aitboudad) + * bug #14852 [VarDumper] Fix generic casters calling order (nicolas-grekas) + * bug #14855 [VarDumper] Changed tooltip to expand-all keybinding in OS X (taylankasap) + * bug #14854 [Bridge\PhpUnit] Fix composer installed phpunit detection (nicolas-grekas) + * bug #14859 Improve the config validation in TwigBundle (stof) + * bug #14837 [SecurityBundle] Remove SecurityContext from Compile (Zander Baldwin) + * bug #14793 [FrameworkBundle] make `templating.helper.router` service available again for BC reasons (xabbuh) + * bug #14785 [BrowserKit] Fix bug when uri starts with http. (amouhzi) + * bug #14807 [Security][Acl] enforce string identifiers (xabbuh) + * bug #14808 [WebProfilerBundle][logger] added missing deprecation message. (aitboudad) + +* 2.7.0 (2015-05-30) + + * bug #14777 Avoid using the app global variable in the profiler templates (stof) + * bug #14262 [REVERTED] [TwigBundle] Refresh twig paths when resources change. (aitboudad) + * security #14759 CVE-2015-4050 [HttpKernel] Do not call the FragmentListener if _controller is already defined (jakzal) + * bug #14745 [Serializer] AbstractNormalizer::instantiateObject allow default values when not optional (boekkooi) + * bug #14743 [DebugBundle] Fix config XSD (nicolas-grekas) + * bug #14711 [Serializer] AbstractNormalizer instantiateObject avoid `null` rejection (boekkooi) + * bug #14726 [Translation] fixed JSON loader on PHP 7 when file is empty (fabpot) + * bug #14715 [Form] Check instance of FormBuilderInterface instead of FormBuilder (dosten) + * bug #14654 [Console] SymfonyStyle : fix blocks failed when $messages is null (ogizanagi) + * bug #14708 [TwigBridge] use proper class to fetch asset version strategy property (xabbuh) + * bug #14678 [Security] AbstractRememberMeServices::encodeCookie() validates cookie parts (MacDada) + * bug #14635 [HttpKernel] Handle an array vary header in the http cache store (jakzal) + * bug #14513 [console][formater] allow format toString object. (aitboudad) + * bug #14335 [HttpFoundation] Fix baseUrl when script filename is contained in pathInfo (danez) + * bug #14593 [Security][Firewall] Avoid redirection to XHR URIs (asiragusa) + * bug #14576 [DoctrineBridge][Form] Fix BC break in DoctrineType (malarzm) + * bug #14551 [Form] Fixed ChoiceType with legacy ChoiceList (xelaris) + * bug #14648 [Console] Fix first choice was invalid when using value (ogizanagi) + * bug #14618 [DomCrawler] Throw an exception if a form field path is incomplete (jakzal) + * bug #14699 Fix HTML escaping of to-source links (amenk, nicolas-grekas) + * bug #14698 Fix HTML escaping of to-source links (nicolas-grekas) + * bug #14690 [HttpFoundation] IpUtils::checkIp4() should allow `/0` networks (zerkms) + * bug #14696 Fix the rendering of deprecation log messages (stof) + * bug #14683 Fixed the indentation in the compiled template for the DumpNode (stof) + * bug #14262 [TwigBundle] Refresh twig paths when resources change. (aitboudad) + * bug #13633 [ServerBag] Handled bearer authorization header in REDIRECT_ form (Lance0312) + * bug #13637 [CSS] WebProfiler break words (nicovak) + * bug #14217 [WebProfilerBundle] Fix regexp (romqin) + * bug #14644 [Bridge\Twig] Adding a space between the icon and the error message (zmikael, nicolas-grekas) + * bug #14645 [WebProfilerBundle] Fix compatiblity with HttpKernel < 2.7 (GromNaN) + * bug #14640 [DebugBundle] Allow alternative destination for dumps (nicolas-grekas) + * bug #14600 [Console] SymfonyStyle: fix block rpadding when escaping '<' (ogizanagi) + * bug #14633 [EventDispatcher] make listeners removable from an executed listener (xabbuh) + +* 2.7.0-BETA2 (2015-05-13) + + * bug #14609 [DebugBundle] Remove inlined dumps on XHR (nicolas-grekas) + * bug #14605 [PropertyAccess] Fix setting public property on a class having a magic getter (lolautruche) + * bug #14266 [HttpKernel] Check if "symfony/proxy-manager-bridge" package is installed (hason) + * bug #14465 [Form] Fixed regression: Empty values were not accepted anymore for collapsed, optional choice fields (webmozart) + * bug #14478 [DebugBundle] Fix dump() output in API / No-Toolbar context (nicolas-grekas) + * bug #14501 [ProxyBridge] Fix proxy classnames generation (xphere) + * bug #14540 [Serializer] Ignore \Traversable in default normalizers. (dunglas) + * bug #14527 Fix getOrigin (WouterJ) + * bug #14395 [Validator] Property paths starting with 0 are broken. (fago) + * feature #14472 [Debug] Trigger deprecation notices when using PHP7 reserved class names (nicolas-grekas) + * bug #14498 [FrameworkBundle] Added missing log in server:run command (lyrixx) + * bug #14503 [Finder] Only use GLOB_BRACE when available (dosten) + * bug #14484 [SecurityBundle][WebProfiler] check authenticated user by tokenClass instead of username. (aitboudad) + * bug #14497 [HttpFoundation] Allow curly braces in trusted host patterns (sgrodzicki) + * bug #14480 [TwigBundle] Fix deprecated use of FlattenException (alOneh) + * bug #14469 [Debug] BaseException compatibility for PHP7 (nicolas-grekas) + * bug #14483 [VarDumper] Fix #14481 : Open tree recursively on OSX (romainneutron) + * bug #14466 [WebProfiler] fix html syntax for input types (Tobion) + * bug #14436 Show a better error when the port is in use (dosten) + * bug #14463 [Validator] Fixed Choice when an empty array is used in the "choices" option (webmozart) + * bug #14415 [Serializer] Fix a bug when using groups together with a name converter (dunglas) + * bug #14301 [FrameworkBundle][Translation] skip warmUp when cache is not used. (aitboudad) + * bug #14446 [DependencyInjection] resolve circular reference (xabbuh) + * feature #14187 [Serializer] Supports hassers and setters for groups annotations (dunglas) + * feature #14462 Revert "[HttpKernel] Throw a LogicException when kernel.exception does not led to a Response" (nicolas-grekas) + * bug #14448 [Validator] Fixed Choice when an empty array is used in the "choices" option (webmozart) + * bug #14451 [Debug] Fix ClassNotFoundFatalErrorHandler candidates lookups (nicolas-grekas) + * bug #14422 Update DebugClassLoader.php (userfriendly) + * bug #14419 inject asset packages in assets helper service (xabbuh) + * bug #14355 [EventDispatcher] make listeners removable from an executed listener (xabbuh) + * bug #14397 [Debug] Map PHP errors to LogLevel::CRITICAL (nicolas-grekas) + * bug #14402 [FrameworkBundle][Translation] Check for 'xlf' instead of 'xliff' (xelaris) + * bug #14272 [FrameworkBundle] Workaround php -S ignoring auto_prepend_file (nicolas-grekas) + * bug #14362 [Debug] Scream as LogLevel::DEBUG (but for fatal errors / uncaught exceptions) (nicolas-grekas) + * bug #14345 [FrameworkBundle] Fix Routing\DelegatingLoader resiliency to fatal errors (nicolas-grekas) + * bug #14381 [FrameworkBundle] Initialize translator with the default locale. (aitboudad) + * bug #14325 [Routing][DependencyInjection] Support .yaml extension in YAML loaders (thunderer) + * bug #14344 [Translation][fixed test] refresh cache when resources are no longer fresh. (aitboudad) + * bug #14346 [WebProfilerBundle] Fix resiliency to exceptions thrown by the url generator (nicolas-grekas) + * bug #14338 [FrameworkBundle] improve usage of Table helper (xabbuh) + * bug #14315 [Translation] Revert inlining fallback catalogues as it might cause inconsistent results when a cache is used (mpdude) + * bug #14129 [FrameworkBundle] Fixed server:start --router relative path issue #14124 (abulford) + * bug #14313 [HttpKernel] fixed a regression when no exception listeners are registered (fabpot) + * bug #14268 [Translator] Cache does not take fallback locales into consideration (sf2.3) (mpdude) + +* 2.7.0-BETA1 (2015-04-10) + + * feature #14229 [WebProfilerBundle] AJAX links (romqin) + * feature #13220 [Console] Made output docopt compatible (WouterJ) + * feature #14178 [Config] Delegate creation of ConfigCache instances to a factory. (mpdude) + * feature #13443 [Translation][Command][FrameworkBundle] Enable translation debugging in directories (xelaris) + * feature #14198 Automatically start server:run if server:start failed (WouterJ) + * feature #13651 [Form][choice] added choice_translation_domain to avoid trans options. (aitboudad) + * feature #14185 [Translation][Profiler]added the number of times a translation has been used. (aitboudad) + * feature #13717 Deprecated precision option in favor of scale (WouterJ) + * feature #14159 [Debug] Add symfony_debug_backtrace() and use it when dealing with fatal errors (jpauli, nicolas-grekas) + * feature #14192 [HttpKernel] Embed the original exception as previous to bounced exceptions (nicolas-grekas) + * feature #13626 [WebProfilerBundle] Added feedback about the current symfony version (WouterJ) + * feature #13554 [TwigBundle] make date formats and number formats configurable (xabbuh) + * feature #14196 Tweaked some console command styles (javiereguiluz) + * feature #14181 [Debug] Updated the default log level when a PHP error occurs (lyrixx) + * feature #14186 [Debug] Renamed "context" key to "scope_vars" to avoid any ambiguity (lyrixx) + * feature #13942 [Translation] generate translation cache at warmup (xavierleune) + * feature #14116 [FrameworkBundle] Move lint commands to lint namespace. (aitboudad) + * feature #14052 [FrameworkBundle] added a protected shortcut getParameter() method in the base Controller class. (hhamon) + * feature #14080 [VarDumper] Add casters for Reflection* classes (nicolas-grekas) + * feature #14050 [Form] Refactored choice lists to support dynamic label, value, index and attribute generation (webmozart) + * feature #14079 [VarDumper] Add and use Caster::PREFIX_* consts (nicolas-grekas) + * feature #14057 [RFC][Console] Added console style guide helpers (v2) (kbond) + * feature #14077 [VarDumper] Add VarDumperTestCase and related trait (nicolas-grekas) + * feature #14058 [VarDumper] Add filters to casters (nicolas-grekas) + * feature #14003 [Translation][Profiler] Added a Translation profiler. (aitboudad) + * feature #14002 [Translation][Extractor] Allow extracting an array of files besides extracting a directory (marcosdsanchez) + * feature #13438 [Console][Table] Add support for colspan/rowspan + multiple header lines (aitboudad) + * feature #14071 [VarDumper] Ctrl+click toggles-all and fix IE8 support (larsborn, nicolas-grekas) + * feature #13981 [Translation] merge all fallback catalogues messages into current catalo... (aitboudad) + * feature #14006 [VarDumper] with-er interface for Cloner\Data (nicolas-grekas) + * feature #14034 [VarDumper] add caster for MongoCursor objects (nicolas-grekas) + * feature #14030 [DependencyInjection] make it possible to dump inlined services to XML (xabbuh) + * feature #14016 Remove the API version in the validator component (saro0h, fabpot, stof) + * feature #13960 [VarDumper] Add Caster for XML-parser resources (nicolas-grekas) + * feature #13937 [FrameworkBundle] Allow to disable Kernel reboot (sroze) + * feature #13892 [DependencyInjection] Improved yaml syntax (hason) + * feature #14000 [SECURITY][ACL] fixed Base ACL exceptions on the RuntimeException (Neophy7e) + * feature #14001 [Security] [ACL] Improved MaskBuilder and PermissionMap (AlexDpy) + * feature #13959 [VarDumper] Add catch-all-objects hook for casters (nicolas-grekas) + * feature #13980 [VarDumper] Added support for amqp (lyrixx) + * feature #12818 [SecurityBundle] Added a command to encode a password (saro0h) + * feature #13107 [FrameworkBundle] Serializer groups support (dunglas) + * feature #13872 [FrameworkBundle] Added domain column when debugging translations (hiddewie) + * feature #13780 [HttpKernel] Throw a LogicException when kernel.exception does not lead to a Response (nicolas-grekas) + * feature #13897 [translation][performances] move loading resources into Translator initialize. (aitboudad) + * feature #13864 Entity type: loader caching by query builder instance (dominikzogg) + * feature #13855 Read validation contraints from Resources/config/validation/ sub-dir (GromNaN) + * feature #13257 [Serializer] ObjectNormalizer (dunglas) + * feature #13795 [Serializer] Refactoring of metadata (dunglas) + * feature #13840 [WebProfilerBundle] Update ajax calls in toolbar to add the css error class (rubenrua) + * feature #13809 [OptionsResolver] add missing deprecation triggers (Tobion) + * feature #13398 [PhpUnit] new PhpUnit bridge (nicolas-grekas) + * feature #13615 [FrameworkBundle] Made ServerParams a service (rpg600) + * feature #12526 Add an auto_alias compiler pass (Daniel Wehner) + * feature #13665 [Debug] generalize deprecated interfaces tracking (nicolas-grekas) + * feature #13656 removed Propel bridge from Symfony Core (fabpot) + * feature #13500 [Serializer] Normalizers can serialize collections and scalars (dunglas) + * feature #13463 [WebProfilerBundle] Replaced raster PNG icons with vector SVG icons (sgrodzicki) + * feature #13234 [Asset] added the component (fabpot) + * feature #11379 Added new Forwarded header support for Request::getClientIps (tony-co) + * feature #9782 [Security] added string representation for core Users (tobiassjosten) + * feature #12174 [TwigBundle] Add loader priority (wizhippo) + * feature #13074 [Translation] Refresh catalogues when resources change (iamluc) + * feature #13294 [PropertyAccess] Show property path in all exception messages (mpajunen) + * feature #13548 [TwigBridge] Added support for passing more files to twig:lint command (sustmi) + * feature #13120 [Serializer] Name converter support (dunglas) + * feature #13428 Added a Twig profiler (fabpot) + * feature #11129 Added i18n support to ConfirmationQuestion (WouterJ) + * feature #13034 [HttpKernel] [WebProfilerBundle] added HTTP status to profiler search result (xelaris) + * feature #13475 [SecurityBundle] decouple the logout PHP helper and Twig extension (fabpot) + * feature #12891 [Form] Deprecated setDefaultOptions() in favor of configureOptions() (peterrehm) + * feature #13342 [security] Fetching current stored context when not explicitly specified (jaytaph) + * feature #12960 [FrameworkBundle] Container parameters in Route#condition (nikita2206) + * feature #13418 [DX] Attempt to improve logging messages with parameters (iltar) + * feature #13320 [HttpKernel] Add request uri to Logger context (Rvanlaak) + * feature #13401 [TwigBundle] use the new Twig autoescaping strategy (fabpot) + * feature #13361 [Routing] apply deprecation triggers and fix tests (Tobion) + * feature #13378 lazy-load fragment renderers (fabpot) + * feature #13354 Twig decoupling from Templating (fabpot) + * feature #13264 URL manipulations as a Twig extension (fabpot) + * feature #13289 [DependencyInjection] deprecated synchronized services (fabpot) + * feature #13323 [Security] removed usage of the deprecated SecurityContextInterface (fabpot) + * feature #13241 [Form] add back model_timezone and view_timezone options (xabbuh) + * feature #13252 [Serializer] Refactoring and object_to_populate support. (dunglas) + * feature #13255 [Serializer] Add circular reference handling to the PropertyNormalizer (dunglas) + * feature #13259 Deprecate the translator implementation in the Validator component (stof) + * feature #12956 [Validator] Added checkDNS option on URL validator (saro0h) + * feature #13230 [TwigBundle] removed the Container dependency on ActionsExtension (fabpot) + * feature #12602 Add type aliases for allowed types in OptionsResolver (henrikbjorn) + * feature #12594 [DX] [HttpKernel] Use "context" argument when logging route in RouterListener (iltar) + * feature #12653 [Filesystem] Keep executable permission when a file is copied (joelwurtz) + * feature #13164 [Debug] track and report deprecated classes and interfaces (nicolas-grekas) + * feature #13157 [Security] Updated ACL generateSql.php (jaytaph) + * feature #13105 [FrameworkBundle] added a test router for the built-in web server (fabpot) + * feature #12092 [Serializer] Serialization groups support (dunglas) + * feature #13031 [Serializer] Add xml_format_output context option. Close #12517. (dunglas) + * feature #12862 [Console] Allowing the user answering key or value of the autocompleterValues (saro0h) + * feature #12469 [Security] Added the triggering of the security.interactive_login event in SimplePreAuthenticationListener (saro0h) + * feature #12896 [DX][Profiler] Show the inherited roles in the web profiler (peterrehm) + * feature #12295 [FrameworkBundle] make GetSetMethodNormalizer available by default (dunglas) + * feature #12666 [Hackday][Stopwatch] added __toString on StopwatchEvent (damienalexandre) + diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 94711183ff11..5fc732e26ff9 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -39,8 +39,8 @@ Symfony is the result of the work of many people who made the code better - Maxime Steinhausser (ogizanagi) - Alexandre Salomé (alexandresalome) - William Durand (couac) - - ornicar - Jules Pietri (heah) + - ornicar - stealth35 ‏ (stealth35) - Alexander Mols (asm89) - Francis Besset (francisbesset) @@ -65,9 +65,9 @@ Symfony is the result of the work of many people who made the code better - Dariusz Górecki (canni) - Arnout Boks (aboks) - Iltar van der Berg (kjarli) + - Charles Sarrazin (csarrazi) - Ener-Getick (energetick) - Douglas Greenshields (shieldo) - - Charles Sarrazin (csarrazi) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) @@ -187,6 +187,7 @@ Symfony is the result of the work of many people who made the code better - Loïc Faugeron - Jannik Zschiesche (apfelbox) - Marco Pivetta (ocramius) + - Teoh Han Hui (teohhanhui) - julien pauli (jpauli) - Michael Lee (zerustech) - Lorenz Schori @@ -213,6 +214,7 @@ Symfony is the result of the work of many people who made the code better - Pierre-Yves LEBECQ (pylebecq) - Daniel Espendiller - Jakub Kucharovic (jkucharovic) + - Robin Chalas (chalas_r) - Eugene Leonovich (rybakit) - Filippo Tessarotto - Joseph Rouff (rouffj) @@ -275,7 +277,6 @@ Symfony is the result of the work of many people who made the code better - janschoenherr - Thomas Schulz (king2500) - Berny Cantos (xphere81) - - Teoh Han Hui (teohhanhui) - Ricard Clau (ricardclau) - Mark Challoner (markchalloner) - Gregor Harlan (gharlan) @@ -310,7 +311,6 @@ Symfony is the result of the work of many people who made the code better - Philipp Kräutli (pkraeutli) - Kirill chEbba Chebunin (chebba) - Greg Thornton (xdissent) - - Robin Chalas (chalas_r) - Costin Bereveanu (schniper) - Loïc Chardonnet (gnusat) - Marek Kalnik (marekkalnik) @@ -1414,6 +1414,7 @@ Symfony is the result of the work of many people who made the code better - Damon Jones (damon__jones) - David Badura (davidbadura) - Daniel Londero (dlondero) + - Sebastian Landwehr (dword123) - Adel ELHAIBA (eadel) - Damián Nohales (eagleoneraptor) - Elliot Anderson (elliot) diff --git a/README.md b/README.md index de0feaacd0e9..dca4c3cbdeec 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,10 @@ to high traffic ones like Dailymotion or Yahoo! Answers. Requirements ------------ -Symfony is only supported on PHP 5.3.3 and up. +Symfony is only supported on PHP 5.3.9 and up. -Be warned that PHP versions before 5.3.8 are known to be buggy and might not -work for you: - - * before PHP 5.3.4, if you get "Notice: Trying to get property of - non-object", you've hit a known PHP bug (see - https://bugs.php.net/bug.php?id=52083 and - https://bugs.php.net/bug.php?id=50027); - - * before PHP 5.3.8, if you get an error involving annotations, you've hit a - known PHP bug (see https://bugs.php.net/bug.php?id=55156). - - * PHP 5.3.16 has a major bug in the Reflection subsystem and is not suitable to - run Symfony (https://bugs.php.net/bug.php?id=62715) +Be warned that PHP 5.3.16 has a major bug in the Reflection subsystem and is +not suitable to run Symfony (https://bugs.php.net/bug.php?id=62715) Installation ------------ diff --git a/UPGRADE-2.2.md b/UPGRADE-2.2.md index f203b208fd95..ff3dc0a1860e 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 000000000000..d7e7bcdd48e6 --- /dev/null +++ b/UPGRADE-2.4.md @@ -0,0 +1,50 @@ +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. + +EventDispatcher +---------------- + + * The `getDispatcher()` and `getName()` methods from `Symfony\Component\EventDispatcher\Event` + are deprecated, the event dispatcher instance and event name can be received in the listener call instead. + + Before: + + ```php + use Symfony\Component\EventDispatcher\Event; + + class Foo + { + public function myFooListener(Event $event) + { + $dispatcher = $event->getDispatcher(); + $eventName = $event->getName(); + $dispatcher->dispatch('log', $event); + + // ... more code + } + } + ``` + + After: + + ```php + use Symfony\Component\EventDispatcher\Event; + use Symfony\Component\EventDispatcher\EventDispatcherInterface; + + class Foo + { + public function myFooListener(Event $event, $eventName, EventDispatcherInterface $dispatcher) + { + $dispatcher->dispatch('log', $event); + + // ... more code + } + } + ``` diff --git a/UPGRADE-2.5.md b/UPGRADE-2.5.md new file mode 100644 index 000000000000..fc5ad61fadf7 --- /dev/null +++ b/UPGRADE-2.5.md @@ -0,0 +1,270 @@ +UPGRADE FROM 2.4 to 2.5 +======================= + +FrameworkBundle +--------------- + +* The `Symfony\Bundle\FrameworkBundle\Console\Descriptor\Descriptor::renderTable()` + method expects the table to be an instance of `Symfony\Component\Console\Helper\Table` + instead of `Symfony\Component\Console\Helper\TableHelper`. + +Routing +------- + + * Added a new optional parameter `$requiredSchemes` to `Symfony\Component\Routing\Generator\UrlGenerator::doGenerate()` + +Form +---- + + * The method `FormInterface::getErrors()` now returns an instance of + `Symfony\Component\Form\FormErrorIterator` instead of an array. This object + is traversable, countable and supports array access. However, you can not + pass it to any of PHP's `array_*` functions anymore. You should use + `iterator_to_array()` in those cases where you did. + + Before: + + ``` + $errors = array_map($callback, $form->getErrors()); + ``` + + After: + + ``` + $errors = array_map($callback, iterator_to_array($form->getErrors())); + ``` + + * The method `FormInterface::getErrors()` now has two additional, optional + parameters. Make sure to add these parameters to the method signatures of + your implementations of that interface. + + Before: + + ``` + public function getErrors() + { + ``` + + After: + + ``` + public function getErrors($deep = false, $flatten = true) + { + ``` + + Before: + + ``` + {% if form.vars.errors %} + ``` + + After: + + ``` + {% if form.vars.errors|length %} + ``` + +PropertyAccess +-------------- + + * The methods `isReadable()` and `isWritable()` were added to + `PropertyAccessorInterface`. If you implemented this interface in your own + code, you should add these two methods. + + * The methods `getValue()` and `setValue()` now throw an + `NoSuchIndexException` instead of a `NoSuchPropertyException` when an index + is accessed on an object that does not implement `ArrayAccess`. If you catch + this exception in your code, you should adapt the catch statement: + + Before: + + ```php + $object = new \stdClass(); + + try { + $propertyAccessor->getValue($object, '[index]'); + $propertyAccessor->setValue($object, '[index]', 'New value'); + } catch (NoSuchPropertyException $e) { + // ... + } + ``` + + After: + + ```php + $object = new \stdClass(); + + try { + $propertyAccessor->getValue($object, '[index]'); + $propertyAccessor->setValue($object, '[index]', 'New value'); + } catch (NoSuchIndexException $e) { + // ... + } + ``` + + A `NoSuchPropertyException` is still thrown when a non-existing property is + accessed on an object or an array. + +Validator +--------- + + * EmailValidator has changed to allow `non-strict` and `strict` email validation + + Before: + + Email validation was done with php's `filter_var()` + + After: + + Default email validation is now done via a simple regex which may cause invalid emails (not RFC compliant) to be + valid. This is the default behaviour. + + Strict email validation has to be explicitly activated in the configuration file by adding + + ``` + framework: + //... + validation: + strict_email: true + //... + + ``` + + Also you have to add to your composer.json: + + ``` + "egulias/email-validator": "~1.2" + ``` + + * `ClassMetadata::getGroupSequence()` now returns `GroupSequence` instances + instead of an array. The sequence implements `\Traversable`, `\ArrayAccess` + and `\Countable`, so in most cases you should be fine. If you however use the + sequence with PHP's `array_*()` functions, you should cast it to an array + first using `iterator_to_array()`: + + Before: + + ``` + $sequence = $metadata->getGroupSequence(); + $result = array_map($callback, $sequence); + ``` + + After: + + ``` + $sequence = iterator_to_array($metadata->getGroupSequence()); + $result = array_map($callback, $sequence); + ``` + + * The array type hint in `ClassMetadata::setGroupSequence()` was removed. If + you overwrite this method, make sure to remove the type hint as well. The + method should now accept `GroupSequence` instances just as well as arrays. + + Before: + + ``` + public function setGroupSequence(array $groups) + { + // ... + } + ``` + + After: + + ``` + public function setGroupSequence($groupSequence) + { + // ... + } + ``` + + * The validation engine in `Symfony\Component\Validator\Validator` was replaced + by a new one in `Symfony\Component\Validator\Validator\RecursiveValidator`. + With that change, several classes were deprecated that will be removed in + Symfony 3.0. Also, the API of the validator was slightly changed. More + details about that can be found in UPGRADE-3.0. + + You can choose the desired API via the new "api" entry in + app/config/config.yml: + + ``` + framework: + validation: + enabled: true + api: auto + ``` + + When running PHP 5.3.9 or higher, Symfony will then use an implementation + that supports both the old API and the new one: + + ``` + framework: + validation: + enabled: true + api: 2.5-bc + ``` + + When running PHP lower than 5.3.9, that compatibility layer is not supported. + On those versions, the old implementation will be used instead: + + ``` + framework: + validation: + enabled: true + api: 2.4 + ``` + + If you develop a new application that doesn't rely on the old API, you can + also set the API to 2.5. In that case, the backwards compatibility layer + will not be activated: + + ``` + framework: + validation: + enabled: true + api: 2.5 + ``` + + When using the validator outside of the Symfony full-stack framework, the + desired API can be selected using `setApiVersion()` on the validator builder: + + ``` + // Previous implementation + $validator = Validation::createValidatorBuilder() + ->setApiVersion(Validation::API_VERSION_2_4) + ->getValidator(); + + // New implementation with backwards compatibility support + $validator = Validation::createValidatorBuilder() + ->setApiVersion(Validation::API_VERSION_2_5_BC) + ->getValidator(); + + // New implementation without backwards compatibility support + $validator = Validation::createValidatorBuilder() + ->setApiVersion(Validation::API_VERSION_2_5) + ->getValidator(); + ``` + + +Yaml Component +-------------- + + * The way Yaml handles duplicate keys in an array was changed from `rewrite with the + last element` behavior to ignoring all the elements with the same key after the first one. + + Example: + + ``` + parentElement: + firstChild: foo + secondChild: 123 + firstChild: bar + ``` + + Before: + + This would be parsed in an array like this: `["parentElement" => ["firstChild" => "bar", "secondChild" => 123]]` + + After: + + The first value is used: `["parentElement" => ["firstChild" => "foo", "secondChild" => 123]]` diff --git a/UPGRADE-2.6.md b/UPGRADE-2.6.md new file mode 100644 index 000000000000..2339eea368aa --- /dev/null +++ b/UPGRADE-2.6.md @@ -0,0 +1,428 @@ +UPGRADE FROM 2.5 to 2.6 +======================= + +Known Backwards-Compatibility Breaks +------------------------------------ + +* If you use the `PdoSessionHandler`, the session table now has a different + schema and must be modified. Look below for more details. + +Form +---- + + * The "empty_value" option in the types "choice", "date", "datetime" and "time" + was deprecated and replaced by a new option "placeholder". You should use + the option "placeholder" together with the view variables "placeholder" and + "placeholder_in_choices" now. + + The option "empty_value" and the view variables "empty_value" and + "empty_value_in_choices" will be removed in Symfony 3.0. + + Before: + + ```php + $form->add('category', 'choice', array( + 'choices' => array('politics', 'media'), + 'empty_value' => 'Select a category...', + )); + ``` + + After: + + ```php + $form->add('category', 'choice', array( + 'choices' => array('politics', 'media'), + 'placeholder' => 'Select a category...', + )); + ``` + + Before: + + ``` + {{ form.vars.empty_value }} + + {% if form.vars.empty_value_in_choices %} + ... + {% endif %} + ``` + + After: + + ``` + {{ form.vars.placeholder }} + + {% if form.vars.placeholder_in_choices %} + ... + {% endif %} + ``` + +Validator +--------- + + * The internal method `setConstraint()` was added to + `Symfony\Component\Validator\Context\ExecutionContextInterface`. With + this method, the context is informed about the constraint that is currently + being validated. + + If you implement this interface, make sure to add the method to your + implementation. The easiest solution is to just implement an empty method: + + ```php + public function setConstraint(Constraint $constraint) + { + } + ``` + + * Prior to 2.6 `Symfony\Component\Validator\Constraints\ExpressionValidator` + would not execute the Expression if it was attached to a property on an + object and that property was set to `null` or an empty string. + + To emulate the old behaviour change your expression to something like + this: + + ``` + value == null or (YOUR_EXPRESSION) + ``` + +Security +-------- + + * The `SecurityContextInterface` is marked as deprecated in favor of the + `Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface` and + `Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface`. + ``` + isGranted => AuthorizationCheckerInterface + getToken => TokenStorageInterface + setToken => TokenStorageInterface + ``` + The Implementations have moved too, The `SecurityContext` is marked as + deprecated and has been split to use the `AuthorizationCheckerInterface` + and `TokenStorage`. This change is 100% Backwards Compatible as the SecurityContext + delegates the methods. + + * The service `security.context` is deprecated along with the above change. Recommended + to use instead: + ``` + @security.authorization_checker => isGranted() + @security.token_storage => getToken() + @security.token_storage => setToken() + ``` + +HttpFoundation +-------------- + + * The `PdoSessionHandler` to store sessions in a database changed significantly. + This introduced a **backwards-compatibility** break in the schema of the + session table. The following changes must be made to your session table: + + - Add a new integer column called `sess_lifetime`. Assuming you have the + default column and table names, in MySQL this would be: + ALTER TABLE `session` ADD `sess_lifetime` INT NOT NULL ; + - Change the data column (default: `sess_value`) to be a Blob type. In + MySQL this would be: + ALTER TABLE `session` CHANGE `sess_value` `session_value` BLOB NOT NULL; + + There is also an [issue](https://github.com/symfony/symfony/issues/12834) + that affects Windows servers. + + A legacy class, `LegacyPdoSessionHandler` has been created to ease backwards-compatibility issues when upgrading. + + The changes to the `PdoSessionHandler` are: + - By default, it now implements session locking to prevent loss of data by concurrent access to the same session. + - It does so using a transaction between opening and closing a session. For this reason, it's not + recommended to use the same database connection that you also use for your application logic. + Otherwise you have to make sure to access your database after the session is closed and committed. + Instead of passing an existing connection to the handler, you can now also pass a DSN string which + will be used to lazy-connect when a session is started. + - Since accessing a session now blocks when the same session is still open, it is best practice to + save the session as soon as you don't need to write to it anymore. For example, read-only AJAX + request to a session can save the session immediately after opening it to increase concurrency. + - As alternative to transactional locking you can also use advisory locks which do not require a transaction. + Additionally, you can also revert back to no locking in case you have custom logic to deal with race conditions + like an optimistic concurrency control approach. The locking strategy can be chosen by passing the corresponding + constant as `lock_mode` option, e.g. `new PdoSessionHandler($pdoOrDsn, array('lock_mode' => PdoSessionHandler::LOCK_NONE))`. + For more information please read the class documentation. + - The expected schema of the table changed. + - Session data is binary text that can contain null bytes and thus should also be saved as-is in a + binary column like BLOB. For this reason, the handler does not base64_encode the data anymore. + - A new column to store the lifetime of a session is required. This allows to have different + lifetimes per session configured via session.gc_maxlifetime ini setting. + - You would need to migrate the table manually if you want to keep session information of your users. + - You could use `PdoSessionHandler::createTable` to initialize a correctly defined table depending on + the used database vendor. + +OptionsResolver +--------------- + + * The "array" type hint was removed from the `OptionsResolverInterface` methods + `setRequired()`, `setAllowedValues()`, `addAllowedValues()`, + `setAllowedTypes()` and `addAllowedTypes()`. You must remove the type hint + from your implementations. + + * The interface `OptionsResolverInterface` was deprecated, since + `OptionsResolver` instances are not supposed to be shared between classes. + You should type hint against `OptionsResolver` instead. + + Before: + + ```php + protected function configureOptions(OptionsResolverInterface $resolver) + { + // ... + } + ``` + + After: + + ```php + protected function configureOptions(OptionsResolver $resolver) + { + // ... + } + ``` + + * `OptionsResolver::isRequired()` now returns `true` if a required option has + a default value set. The new method `isMissing()` exhibits the old + functionality of `isRequired()`. + + Before: + + ```php + $resolver->setRequired(array('port')); + + $resolver->isRequired('port'); + // => true + + $resolver->setDefaults(array('port' => 25)); + + $resolver->isRequired('port'); + // => false + ``` + + After: + + ```php + $resolver->setRequired(array('port')); + + $resolver->isRequired('port'); + // => true + $resolver->isMissing('port'); + // => true + + $resolver->setDefaults(array('port' => 25)); + + $resolver->isRequired('port'); + // => true + $resolver->isMissing('port'); + // => false + ``` + + * `OptionsResolver::replaceDefaults()` was deprecated. Use `clear()` and + `setDefaults()` instead. + + Before: + + ```php + $resolver->replaceDefaults(array( + 'port' => 25, + )); + ``` + + After: + + ```php + $resolver->clear(); + $resolver->setDefaults(array( + 'port' => 25, + )); + ``` + + * `OptionsResolver::setOptional()` was deprecated. Use `setDefined()` instead. + + Before: + + ```php + $resolver->setOptional(array('port')); + ``` + + After: + + ```php + $resolver->setDefined('port'); + ``` + + * `OptionsResolver::isKnown()` was deprecated. Use `isDefined()` instead. + + Before: + + ```php + if ($resolver->isKnown('port')) { + // ... + } + ``` + + After: + + ```php + if ($resolver->isDefined('port')) { + // ... + } + ``` + + * The methods `setAllowedValues()`, `addAllowedValues()`, `setAllowedTypes()` + and `addAllowedTypes()` were changed to modify one option at a time instead + of batch processing options. The old API exists for backwards compatibility, + but will be removed in Symfony 3.0. + + Before: + + ```php + $resolver->setAllowedValues(array( + 'method' => array('POST', 'GET'), + )); + ``` + + After: + + ```php + $resolver->setAllowedValues('method', array('POST', 'GET')); + ``` + + * The class `Options` was merged into `OptionsResolver`. If you instantiated + this class manually, you should instantiate `OptionsResolver` now. + `Options` is now a marker interface implemented by `OptionsResolver`. + + Before: + + ```php + $options = new Options(); + ``` + + After: + + ```php + $resolver = new OptionsResolver(); + ``` + + * Normalizers for defined but unset options are not executed anymore. If you + want to have them executed, you should define a default value. + + Before: + + ```php + $resolver->setOptional(array('port')); + $resolver->setNormalizers(array( + 'port' => function ($options, $value) { + // return normalized value + } + )); + + $options = $resolver->resolve($options); + ``` + + After: + + ```php + $resolver->setDefault('port', null); + $resolver->setNormalizer('port', function ($options, $value) { + // return normalized value + }); + + $options = $resolver->resolve($options); + ``` + + * When undefined options are passed, `resolve()` now throws an + `UndefinedOptionsException` instead of an `InvalidOptionsException`. + `InvalidOptionsException` is only thrown when option values fail their + validation constraints. + + Before: + + ```php + $resolver->setDefaults(array( + 'transport' => 'smtp', + 'port' => 25, + )); + $resolver->setAllowedTypes(array( + 'port' => 'integer', + )); + + // throws InvalidOptionsException + $resolver->resolve(array('foo' => 'bar')); + + // throws InvalidOptionsException + $resolver->resolve(array('port' => '25')); + ``` + + After: + + ```php + $resolver->setDefaults(array( + 'transport' => 'smtp', + 'port' => 25, + )); + $resolver->setAllowedTypes(array( + 'port' => 'integer', + )); + + // throws UndefinedOptionsException + $resolver->resolve(array('foo' => 'bar')); + + // throws InvalidOptionsException + $resolver->resolve(array('port' => '25')); + ``` + +VarDumper and DebugBundle +------------------------- + +The component and the bundle are new to Symfony 2.6. We encourage you +to enable the bundle in your `app/AppKernel.php` for the *dev* or *test* +environments. Just add this line before loading the `WebProfilerBundle`: + +```php +$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); +``` + +Then enjoy dumping variables by calling `dump($var)` anywhere in your PHP +and `{% dump var %}` or `{{ dump(var) }}` in Twig. Dumps are displayed +**in the web debug toolbar**. + +Translation +----------- + +With `LoggingTranslator`, a new translator class is introduced with Symfony +2.6. By default, the `@translator` service is referring to this class in the +debug environment. + +If you have your own services that depend on the `@translator` service and expect +this service to be an instance of either +`Symfony\Component\Translation\Translator` or +`Symfony\Bundle\FrameworkBundle\Translation\Translator`, e.g. by type-hinting +for either of these classes, you will need to change that type hint. You can +use the `TranslatorInterface` to be on the safe side for future changes. + +Before: + +```php +use Symfony\Component\Translation\Translator; + +class MyService { + public function __construct(Translator $translator) + { + ... + } +} +``` + +After: + +```php +use Symfony\Component\Translation\TranslatorInterface; + +class MyService { + public function __construct(TranslatorInterface $translator) + { + ... + } +} +``` diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md new file mode 100644 index 000000000000..5de67ebede36 --- /dev/null +++ b/UPGRADE-2.7.md @@ -0,0 +1,721 @@ +UPGRADE FROM 2.6 to 2.7 +======================= + +Global +------ + + * Deprecation notices - + `@trigger_error('... is deprecated ...', E_USER_DEPRECATED)` - + are now triggered when using any deprecated functionality. + + By default these notices are silenced, so they won't appear in the PHP logs of + your production server. However, these notices are still visible in the web + debug toolbar, so you can know where your code needs an upgrade. + + In addition, it's strongly recommended to enable the [phpunit-bridge](https://github.com/symfony/phpunit-bridge) + so that you can deal with deprecation notices in your test suite. + +Router +------ + + * Route conditions now support container parameters which + can be injected into condition using `%parameter%` notation. + Due to the fact that it works by replacing all parameters + with their corresponding values before passing condition + expression for compilation there can be BC breaks where you + could already have used percentage symbols. Single percentage symbol + usage is not affected in any way. Conflicts may occur where + you might have used `%` as a modulo operator, here's an example: + `foo%bar%2` which would be compiled to `$foo % $bar % 2` in 2.6 + but in 2.7 you would get an error if `bar` parameter + doesn't exist or unexpected result otherwise. + + * The `getMatcherDumperInstance()` and `getGeneratorDumperInstance()` methods in the + `Symfony\Component\Routing\Router` have been changed from `protected` to `public`. + If you override these methods in a subclass, you will need to change your + methods to `public` as well. Note however that this is a temporary change needed for + PHP 5.3 compatibility only. It will be reverted in Symfony 3.0. + +Form +---- + + * In form types and extension overriding the "setDefaultOptions" of the + AbstractType or AbstractExtensionType has been deprecated in favor of + overriding the new "configureOptions" method. + + The method "setDefaultOptions(OptionsResolverInterface $resolver)" will + be renamed in Symfony 3.0 to "configureOptions(OptionsResolver $resolver)". + + Before: + + ```php + use Symfony\Component\OptionsResolver\OptionsResolverInterface; + + class TaskType extends AbstractType + { + // ... + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'AppBundle\Entity\Task', + )); + } + } + ``` + + After: + + ```php + use Symfony\Component\OptionsResolver\OptionsResolver; + + class TaskType extends AbstractType + { + // ... + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'AppBundle\Entity\Task', + )); + } + } + ``` + + * The "choice_list" option of ChoiceType was deprecated. You should use + "choices_as_values" or "choice_loader" now. + + Before: + + ```php + $form->add('status', 'choice', array( + 'choice_list' => new ObjectChoiceList(array( + Status::getInstance(Status::ENABLED), + Status::getInstance(Status::DISABLED), + Status::getInstance(Status::IGNORED), + )), + )); + ``` + + After: + + ```php + $form->add('status', 'choice', array( + 'choices' => array( + Status::getInstance(Status::ENABLED), + Status::getInstance(Status::DISABLED), + Status::getInstance(Status::IGNORED), + ), + 'choices_as_values' => true, + )); + ``` + + * You should flip the keys and values of the "choices" option in ChoiceType + and set the "choices_as_values" option to `true`. The default value of that + option will be switched to `true` in Symfony 3.0. + + Before: + + ```php + $form->add('status', 'choice', array( + 'choices' => array( + Status::ENABLED => 'Enabled', + Status::DISABLED => 'Disabled', + Status::IGNORED => 'Ignored', + )), + )); + ``` + + After: + + ```php + $form->add('status', 'choice', array( + 'choices' => array( + 'Enabled' => Status::ENABLED, + 'Disabled' => Status::DISABLED, + 'Ignored' => Status::IGNORED, + ), + 'choices_as_values' => true, + // important if you rely on your option value attribute (e.g. for JavaScript) + // this will keep the same functionality as before + 'choice_value' => function ($choice) { + return $choice; + }, + )); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\ChoiceListInterface` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface; + + public function doSomething(ChoiceListInterface $choiceList) + { + // ... + } + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + + public function doSomething(ChoiceListInterface $choiceList) + { + // ... + } + ``` + + * `Symfony\Component\Form\Extension\Core\View\ChoiceView` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\View\ChoiceView` instead. + The constructor arguments of the new class are in the same order than in the + deprecated one (this was not true in 2.7.0 but has been fixed in 2.7.1). + + * `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; + + $choiceList = new ChoiceList( + array(Status::ENABLED, Status::DISABLED, Status::IGNORED), + array('Enabled', 'Disabled', 'Ignored'), + // Preferred choices + array(Status::ENABLED), + ); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choices = array(Status::ENABLED, Status::DISABLED, Status::IGNORED); + $labels = array('Enabled', 'Disabled', 'Ignored'); + + $choiceList = $factory->createListFromChoices($choices); + + $choiceListView = $factory->createView( + $choiceList, + // Preferred choices + array(Status::ENABLED), + // Labels + function ($choice, $key) use ($labels) { + return $labels[$key]; + } + ); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory::createListFromLoader()` + together with an implementation of + `Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList; + + class MyLazyChoiceList extends LazyChoiceList + { + public function loadChoiceList() + { + // load $choiceList + + return $choiceList; + } + } + + $choiceList = new MyLazyChoiceList(); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; + + class MyChoiceLoader implements ChoiceLoaderInterface + { + // ... + } + + $factory = new DefaultChoiceListFactory(); + + $choiceList = $factory->createListFromLoader(new MyChoiceLoader()); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; + + $choiceList = new ObjectChoiceList( + array(Status::getInstance(Status::ENABLED), Status::getInstance(Status::DISABLED)), + // Label property + 'name' + ); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choiceList = $factory->createListFromChoices(array( + Status::getInstance(Status::ENABLED), + Status::getInstance(Status::DISABLED), + )); + + $choiceListView = $factory->createView( + $choiceList, + // Preferred choices + array(), + // Label property + 'name' + ); + ``` + + * `Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; + + $choiceList = new SimpleChoiceList(array( + Status::ENABLED => 'Enabled', + Status::DISABLED => 'Disabled', + )); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choices = array(Status::ENABLED, Status::DISABLED); + $labels = array('Enabled', 'Disabled'); + + $choiceList = $factory->createListFromChoices($choices); + + $choiceListView = $factory->createView( + $choiceList, + // Preferred choices + array(), + // Label + function ($choice, $key) use ($labels) { + return $labels[$key]; + } + ); + ``` + + * The "property" option of `DoctrineType` was deprecated. You should use the + new inherited option "choice_label" instead, which has the same effect. + + Before: + + ```php + $form->add('tags', 'entity', array( + 'class' => 'Acme\Entity\MyTag', + 'property' => 'name', + )) + ``` + + After: + + ```php + $form->add('tags', 'entity', array( + 'class' => 'Acme\Entity\MyTag', + 'choice_label' => 'name', + )) + ``` + + * The "loader" option of `DoctrineType` was deprecated and will be removed in + Symfony 3.0. You should override the `getLoader()` method instead in a custom + type. + + Before: + + ```php + $form->add('tags', 'entity', array( + 'class' => 'Acme\Entity\MyTag', + 'loader' => new MyEntityLoader(), + )) + ``` + + After: + + ```php + class MyEntityType extends DoctrineType + { + // ... + + public function getLoader() + { + return new MyEntityLoader(); + } + } + ``` + + * `Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList` was + deprecated and will be removed in Symfony 3.0. You should use + `Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader` instead. + + Before: + + ```php + use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList; + + $choiceList = new EntityChoiceList($em, 'Acme\Entity\MyEntity'); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; + + $factory = new DefaultChoiceListFactory(); + + $choices = array(Status::ENABLED, Status::DISABLED); + $labels = array('Enabled', 'Disabled'); + + $choiceLoader = new DoctrineChoiceLoader($factory, $em, 'Acme\Entity\MyEntity'); + $choiceList = $factory->createListFromLoader($choiceLoader); + ``` + + * Passing a query builder closure to `ORMQueryBuilderLoader` was deprecated and + will not be supported anymore in Symfony 3.0. You should pass resolved query + builders only. + + Consequently, the arguments `$manager` and `$class` of `ORMQueryBuilderLoader` + have been deprecated as well. + + Note that the "query_builder" option of `DoctrineType` *does* support + closures, but the closure is now resolved in the type instead of in the + loader. + + Before: + + ```php + use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; + + $queryBuilder = function () { + // return QueryBuilder + }; + $loader = new ORMQueryBuilderLoader($queryBuilder); + ``` + + After: + + ```php + use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; + + // create $queryBuilder + $loader = new ORMQueryBuilderLoader($queryBuilder); + ``` + + * The classes `ChoiceToBooleanArrayTransformer`, + `ChoicesToBooleanArrayTransformer`, `FixRadioInputListener` and + `FixCheckboxInputListener` were deprecated and will be removed in Symfony 3.0. + Their functionality is covered by the new classes `RadioListMapper` and + `CheckboxListMapper`. + + * The ability to translate Doctrine type entries by the translator component + is now disabled by default and to enable it you must explicitly set the option + "choice_translation_domain" to true + + Before: + + ```php + $form->add('products', 'entity', array( + 'class' => 'AppBundle/Entity/Product', + )); + ``` + + After: + + ```php + $form->add('products', 'entity', array( + 'class' => 'AppBundle/Entity/Product', + 'choice_translation_domain' => true, + )); + ``` + + * In the block `choice_widget_options` the `translation_domain` has been replaced + with the `choice_translation_domain` option. + + Before: + + ```jinja + {{ choice.label|trans({}, translation_domain) }} + ``` + + After: + + ```jinja + {{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }} + ``` + +Serializer +---------- + + * The `setCamelizedAttributes()` method of the + `Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer` and + `Symfony\Component\Serializer\Normalizer\PropertyNormalizer` classes is marked + as deprecated in favor of the new NameConverter system. + + Before: + + ```php + $normalizer->setCamelizedAttributes(array('foo_bar', 'bar_foo')); + ``` + + After: + + ```php + use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; + + $nameConverter = new CamelCaseToSnakeCaseNameConverter(array('fooBar', 'barFoo')); + $normalizer = new GetSetMethodNormalizer(null, $nameConverter); + ``` + + * `Symfony\Component\Serializer\Exception\ExceptionInterface` is the new name for the now + deprecated `Symfony\Component\Serializer\Exception\Exception` interface. + +PropertyAccess +-------------- + + * `UnexpectedTypeException` now expects three constructor arguments: The invalid property value, + the `PropertyPathInterface` object and the current index of the property path. + + Before: + + ```php + use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; + + new UnexpectedTypeException($value, $expectedType); + ``` + + After: + + ```php + use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; + + new UnexpectedTypeException($value, $path, $pathIndex); + ``` + +Config +------ + + * The `__toString()` method of the `\Symfony\Component\Config\ConfigCache` is marked as + deprecated in favor of the new `getPath()` method. + +Validator +--------- + + * The PHP7-incompatible constraints (Null, True, False) and related validators + (NullValidator, TrueValidator, FalseValidator) are marked as deprecated + in favor of their `Is`-prefixed equivalent. + +Console +------- + + * The `Symfony\Component\Console\Input\InputDefinition::getSynopsis()` method + now has an optional argument (it previously had no arguments). If you override + this method, you'll need to add this argument so that your signature matches: + + Before: + + ```php + public function getSynopsis() + { + // ... + } + ``` + + After: + + ```php + public function getSynopsis($short = false) + { + // ... + } + ``` + +TwigBundle +---------- + + * The `Symfony\Bundle\TwigBundle\TwigDefaultEscapingStrategy` is deprecated and no longer + used in favor of `Twig_FileExtensionEscapingStrategy`. This means that CSS files automatically + use the CSS escape strategy. This can cause different behaviour when outputting reserved + characters. + + Before: + + ```css + {# styles.css.twig #} + + {# with brand_color: '#123456' #} + body { + background: {{ brand_color }}; + } + ``` + + After: + + ```css + {# styles.css.twig #} + + {# with brand_color: '#123456' #} + body { + background: {{ brand_color|raw }}; + } + ``` + +FrameworkBundle +--------------- + + * The `templating.helper.assets` service was refactored and now returns an object of type + `Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper` instead of + `Symfony\Component\Templating\Helper\CoreAssetsHelper`. You can update your class definition + or use the `assets.packages` service instead. Using the `assets.packages` service is the recommended + way. + + Before: + + ```php + use Symfony\Component\Templating\Helper\CoreAssetsHelper; + + class DemoService + { + private $assetsHelper; + + public function __construct(CoreAssetsHelper $assetsHelper) + { + $this->assetsHelper = $assetsHelper; + } + + public function testMethod() + { + return $this->assetsHelper->getUrl('thumbnail.png', null, $this->assetsHelper->getVersion()); + } + } + ``` + + After: + + ```php + use Symfony\Component\Asset\Packages; + + class DemoService + { + private $assetPackages; + + public function __construct(Packages $assetPackages) + { + $this->assetPackages = $assetPackages; + } + + public function testMethod() + { + return $this->assetPackages->getUrl('thumbnail.png').$this->assetPackages->getVersion(); + } + } + ``` + +Security +--------------- + + * Injection of the `security.context` service has been reduced to a bare minimum. This means + that arguments that once hinted `SecurityContext` or `SecurityContextInterface` will have + to be updated accordingly to either the `TokenStorageInterface` or `AuthorizationCheckerInterface`. + The following classes now require the `security.token_storage` service instead of the `security.context`, + please update your extending implementations accordingly. + + * `AbstractAuthenticationListener` + * `AnonymousAuthenticationListener` + * `ContextListener` + * `SimplePreAuthenticationListener` + * `X509AuthenticationListener` + * `RemoteUserAuthenticationListener` + * `BasicAuthenticationListener` + * `DigestAuthenticationListener` + * `ExceptionListener` + * `SwitchUserListener` + * `AccessListener` + * `RememberMeListener` + +UPGRADE FROM 2.7.1 to 2.7.2 +=========================== + +Form +---- + + * In order to fix a few regressions in the new `ChoiceList` implementation, + a few details had to be changed compared to 2.7. + + The legacy `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface` + now does not extend the new `Symfony\Component\Form\ChoiceList\ChoiceListInterface` + anymore. If you pass an implementation of the old interface in a context + where the new interface is required, wrap the list into a + `LegacyChoiceListAdapter`: + + Before: + + ```php + use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + + function doSomething(ChoiceListInterface $choiceList) + { + // ... + } + + doSomething($legacyList); + ``` + + After: + + ```php + use Symfony\Component\Form\ChoiceList\ChoiceListInterface; + use Symfony\Component\Form\ChoiceList\LegacyChoiceListAdapter; + + function doSomething(ChoiceListInterface $choiceList) + { + // ... + } + + doSomething(new LegacyChoiceListAdapter($legacyList)); + ``` + + The new `ChoiceListInterface` now has two additional methods + `getStructuredValues()` and `getOriginalKeys()`. You should add these methods + if you implement this interface. See their doc blocks and the implementation + of the core choice lists for inspiration. + + The method `ArrayKeyChoiceList::toArrayKey()` was marked as internal. This + method was never supposed to be used outside the class. + + The method `ChoiceListFactoryInterface::createView()` does not accept arrays + and `Traversable` instances anymore for the `$groupBy` parameter. Pass a + callable instead. diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 09b2938edd46..467c13e6448c 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -20,6 +20,12 @@ UPGRADE FROM 2.x to 3.0 `DebugClassLoader`. The difference is that the constructor now takes a loader to wrap. + +### Config + + * The `__toString()` method of the `\Symfony\Component\Config\ConfigCache` class + was removed in favor of the new `getPath()` method. + ### Console * The `dialog` helper has been removed in favor of the `question` helper. @@ -95,6 +101,30 @@ UPGRADE FROM 2.x to 3.0 been removed in favor of `Definition::setFactory()`. Services defined using YAML or XML use the same syntax as configurators. + * Synchronized services are deprecated and the following methods have been + removed: `ContainerBuilder::synchronize()`, `Definition::isSynchronized()`, + and `Definition::setSynchronized()`. + +### DoctrineBridge + + * The `property` option of `DoctrineType` was removed in favor of the `choice_label` option. + + * The `loader` option of `DoctrineType` was removed. You now have to override the `getLoader()` + method in your custom type. + + * The `Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList` was removed in favor + of `Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader`. + + * Passing a query builder closure to `ORMQueryBuilderLoader` is not supported anymore. + You should pass resolved query builders only. + + Consequently, the arguments `$manager` and `$class` of `ORMQueryBuilderLoader` + have been removed as well. + + Note that the `query_builder` option of `DoctrineType` *does* support + closures, but the closure is now resolved in the type instead of in the + loader. + ### EventDispatcher * The interface `Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface` @@ -102,6 +132,37 @@ UPGRADE FROM 2.x to 3.0 ### Form + * The `max_length` option was removed. Use the `attr` option instead by setting it to + an `array` with a `maxlength` key. + + * The `ChoiceToBooleanArrayTransformer`, `ChoicesToBooleanArrayTransformer`, + `FixRadioInputListener`, and `FixCheckboxInputListener` classes were removed. + + * The `choice_list` option of `ChoiceType` was removed. + + * The option "precision" was renamed to "scale". + + Before: + + ```php + $builder->add('length', 'number', array( + 'precision' => 3, + )); + ``` + + After: + + ```php + $builder->add('length', 'number', array( + 'scale' => 3, + )); + ``` + + * The method `AbstractType::setDefaultOptions(OptionsResolverInterface $resolver)` and + `AbstractTypeExtension::setDefaultOptions(OptionsResolverInterface $resolver)` have been + renamed. You should use `AbstractType::configureOptions(OptionsResolver $resolver)` and + `AbstractTypeExtension::configureOptions(OptionsResolver $resolver)` instead. + * The methods `Form::bind()` and `Form::isBound()` were removed. You should use `Form::submit()` and `Form::isSubmitted()` instead. @@ -244,17 +305,17 @@ UPGRADE FROM 2.x to 3.0 `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 `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface` was + removed in favor of `Symfony\Component\Form\ChoiceList\ChoiceListInterface`. + + * `Symfony\Component\Form\Extension\Core\View\ChoiceView` was removed in favor of + `Symfony\Component\Form\ChoiceList\View\ChoiceView`. * 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`" + * The options "`csrf_provider`" and "`intention`" were renamed to "`csrf_token_generator`" and "`csrf_token_id`". * The method `Form::getErrorsAsString()` was removed. Use `Form::getErrors()` @@ -287,6 +348,11 @@ UPGRADE FROM 2.x to 3.0 ### FrameworkBundle + * The `config:debug`, `container:debug`, `router:debug`, `translation:debug` + and `yaml:lint` commands have been deprecated since Symfony 2.7 and will + be removed in Symfony 3.0. Use the `debug:config`, `debug:container`, + `debug:router`, `debug:translation` and `lint:yaml` commands instead. + * 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. @@ -377,6 +443,21 @@ UPGRADE FROM 2.x to 3.0 * The `RouterApacheDumperCommand` was removed. + * The `templating.helper.router` service was moved to `templating_php.xml`. You + have to ensure that the PHP templating engine is enabled to be able to use it: + + ```yaml + framework: + templating: + engines: ['php'] + ``` + + * The `form.csrf_provider` service is removed as it implements an adapter for + the new token manager to the deprecated + `Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface` + interface. + The `security.csrf.token_manager` should be used instead. + ### HttpKernel * The `Symfony\Component\HttpKernel\Log\LoggerInterface` has been removed in @@ -452,7 +533,7 @@ UPGRADE FROM 2.x to 3.0 * Some route settings have been renamed: - * The `pattern` setting for a route has been deprecated in favor of `path` + * The `pattern` setting has been removed in favor of `path` * The `_scheme` and `_method` requirements have been moved to the `schemes` and `methods` settings Before: @@ -505,10 +586,23 @@ UPGRADE FROM 2.x to 3.0 the performance gains were minimal and it's hard to replicate the behaviour of PHP implementation. + * The `getMatcherDumperInstance()` and `getGeneratorDumperInstance()` methods in the + `Symfony\Component\Routing\Router` have been changed from `public` to `protected`. + ### Security * The `Resources/` directory was moved to `Core/Resources/` +### Serializer + + * The `setCamelizedAttributes()` method of the + `Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer` and + `Symfony\Component\Serializer\Normalizer\PropertyNormalizer` classes + was removed. + + * The `Symfony\Component\Serializer\Exception\Exception` interface was removed + in favor of the new `Symfony\Component\Serializer\Exception\ExceptionInterface`. + ### Translator * The `Translator::setFallbackLocale()` method has been removed in favor of @@ -516,6 +610,9 @@ UPGRADE FROM 2.x to 3.0 ### Twig Bridge + * The `twig:lint` command has been deprecated since Symfony 2.7 and will be + removed in Symfony 3.0. Use the `lint:twig` command instead. + * The `render` tag is deprecated in favor of the `render` function. * The `form_enctype` helper was removed. You should use the new `form_start` @@ -568,8 +665,20 @@ UPGRADE FROM 2.x to 3.0 {{ form_end(form) }} ``` +### TwigBundle + + * The `Symfony\Bundle\TwigBundle\TwigDefaultEscapingStrategy` was removed + in favor of `Twig_FileExtensionEscapingStrategy`. + + * The `twig:debug` command has been deprecated since Symfony 2.7 and will be + removed in Symfony 3.0. Use the `debug:twig` command instead. + ### Validator + * The PHP7-incompatible constraints (`Null`, `True`, `False`) and their related + validators (`NullValidator`, `TrueValidator`, `FalseValidator`) have been + removed in favor of their `Is`-prefixed equivalent. + * The class `Symfony\Component\Validator\Mapping\Cache\ApcCache` has been removed in favor of `Symfony\Component\Validator\Mapping\Cache\DoctrineCache`. @@ -985,6 +1094,22 @@ UPGRADE FROM 2.x to 3.0 $plural = $violation->getPlural(); ``` + * The class `Symfony\Component\Validator\DefaultTranslator` was removed. You + should use `Symfony\Component\Translation\IdentityTranslator` instead. + + Before: + + ```php + $translator = new \Symfony\Component\Validator\DefaultTranslator(); + ``` + + After: + + ```php + $translator = new \Symfony\Component\Translation\IdentityTranslator(); + $translator->setLocale('en'); + ``` + ### Yaml * The ability to pass file names to `Yaml::parse()` has been removed. diff --git a/appveyor.yml b/appveyor.yml index aa992d719eea..5521a0a26b78 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ cache: init: - SET PATH=c:\php;%PATH% - SET COMPOSER_NO_INTERACTION=1 - - SET SYMFONY_DEPRECATIONS_HELPER=weak + - SET SYMFONY_DEPRECATIONS_HELPER=strict - SET PHP=1 - SET ANSICON=121x90 (121x90) - SET SYMFONY_PHPUNIT_SKIPPED_TESTS=phpunit.skipped @@ -19,8 +19,8 @@ install: - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) - cd c:\php - IF %PHP%==1 appveyor DownloadFile https://raw.githubusercontent.com/symfony/binary-utils/master/cacert.pem - - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-5.3.3-nts-Win32-VC9-x86.zip - - IF %PHP%==1 7z x php-5.3.3-nts-Win32-VC9-x86.zip -y >nul + - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-5.3.11-nts-Win32-VC9-x86.zip + - IF %PHP%==1 7z x php-5.3.11-nts-Win32-VC9-x86.zip -y >nul - IF %PHP%==1 appveyor DownloadFile https://raw.githubusercontent.com/symfony/binary-utils/master/ICU-51.2-dlls.zip - IF %PHP%==1 7z x ICU-51.2-dlls.zip -y >nul - IF %PHP%==1 del /Q *.zip diff --git a/composer.json b/composer.json index b200035f08c1..613e53f5aa35 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=5.3.3", + "php": ">=5.3.9", "doctrine/common": "~2.4", "paragonie/random_compat": "~1.0", "symfony/polyfill-apcu": "~1.1", @@ -25,6 +25,7 @@ "psr/log": "~1.0" }, "replace": { + "symfony/asset": "self.version", "symfony/browser-kit": "self.version", "symfony/class-loader": "self.version", "symfony/config": "self.version", @@ -32,9 +33,11 @@ "symfony/css-selector": "self.version", "symfony/dependency-injection": "self.version", "symfony/debug": "self.version", + "symfony/debug-bundle": "self.version", "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", @@ -46,15 +49,15 @@ "symfony/monolog-bridge": "self.version", "symfony/options-resolver": "self.version", "symfony/process": "self.version", - "symfony/propel1-bridge": "self.version", "symfony/property-access": "self.version", "symfony/proxy-manager-bridge": "self.version", "symfony/routing": "self.version", "symfony/security": "self.version", "symfony/security-acl": "self.version", - "symfony/security-bundle": "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", "symfony/swiftmailer-bridge": "self.version", @@ -63,6 +66,7 @@ "symfony/twig-bridge": "self.version", "symfony/twig-bundle": "self.version", "symfony/validator": "self.version", + "symfony/var-dumper": "self.version", "symfony/web-profiler-bundle": "self.version", "symfony/yaml": "self.version" }, @@ -70,13 +74,22 @@ "doctrine/data-fixtures": "1.0.*", "doctrine/dbal": "~2.4", "doctrine/orm": "~2.4,>=2.4.5", - "monolog/monolog": "~1.3", - "propel/propel1": "~1.6", + "doctrine/doctrine-bundle": "~1.2", + "monolog/monolog": "~1.11", "ircmaxell/password-compat": "~1.0", - "ocramius/proxy-manager": "~0.3.1|~1.0|~2.0" + "ocramius/proxy-manager": "~0.4|~1.0|~2.0", + "egulias/email-validator": "~1.2,>=1.2.1" }, "autoload": { - "psr-0": { "Symfony\\": "src/" }, + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "src/Symfony/Bridge/Doctrine/", + "Symfony\\Bridge\\Monolog\\": "src/Symfony/Bridge/Monolog/", + "Symfony\\Bridge\\ProxyManager\\": "src/Symfony/Bridge/ProxyManager/", + "Symfony\\Bridge\\Swiftmailer\\": "src/Symfony/Bridge/Swiftmailer/", + "Symfony\\Bridge\\Twig\\": "src/Symfony/Bridge/Twig/", + "Symfony\\Bundle\\": "src/Symfony/Bundle/", + "Symfony\\Component\\": "src/Symfony/Component/" + }, "classmap": [ "src/Symfony/Component/HttpFoundation/Resources/stubs", "src/Symfony/Component/Intl/Resources/stubs" @@ -86,10 +99,15 @@ "**/Tests/" ] }, + "autoload-dev": { + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "src/Symfony/Bridge/PhpUnit/" + } + }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/phpunit b/phpunit index 6d66f7b2b252..f70cdbfb49ed 100755 --- a/phpunit +++ b/phpunit @@ -53,7 +53,10 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__ chdir("phpunit-$PHPUNIT_VERSION"); passthru("$COMPOSER remove --no-update phpspec/prophecy"); passthru("$COMPOSER remove --no-update symfony/yaml"); - passthru("$COMPOSER require --dev --no-update symfony/phpunit-bridge \">=3.1@dev\""); + if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { + passthru("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); + } + passthru("$COMPOSER require --dev --no-update symfony/phpunit-bridge \">=3.2@dev\""); passthru("$COMPOSER install --prefer-dist --no-progress --ansi", $exit); if ($exit) { exit($exit); @@ -170,8 +173,11 @@ if (isset($argv[1]) && 'symfony' === $argv[1]) { unlink($file); } - // Fail on any individual component failures but ignore STATUS_STACK_BUFFER_OVERRUN (-1073740791/0xC0000409) and STATUS_ACCESS_VIOLATION (-1073741819/0xC0000005) on Windows when APCu is enabled - if ($procStatus && ('\\' !== DIRECTORY_SEPARATOR || !extension_loaded('apcu') || !ini_get('apc.enable_cli') || (-1073740791 !== $procStatus && -1073741819 !== $procStatus))) { + // Fail on any individual component failures but ignore some error codes on Windows when APCu is enabled: + // STATUS_STACK_BUFFER_OVERRUN (-1073740791/0xC0000409) + // STATUS_ACCESS_VIOLATION (-1073741819/0xC0000005) + // STATUS_HEAP_CORRUPTION (-1073740940/0xC0000374) + if ($procStatus && ('\\' !== DIRECTORY_SEPARATOR || !extension_loaded('apcu') || !ini_get('apc.enable_cli') || !in_array($procStatus, array(-1073740791, -1073741819, -1073740940)))) { $exit = $procStatus; echo "\033[41mKO\033[0m $component\n\n"; } else { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 29673772a479..34e951b4fc42 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,6 +17,7 @@ ./src/Symfony/Bridge/*/Tests/ ./src/Symfony/Component/*/Tests/ + ./src/Symfony/Component/*/*/Tests/ ./src/Symfony/Bundle/*/Tests/ @@ -34,6 +35,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/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 9c747b8abc7b..4d8c44701dd3 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,21 @@ CHANGELOG ========= +2.7.0 +----- + + * added DoctrineChoiceLoader + * deprecated EntityChoiceList + * deprecated passing a query builder closure to ORMQueryBuilderLoader + * deprecated $manager and $em arguments of ORMQueryBuilderLoader + * added optional arguments $propertyAccessor and $choiceListFactory to DoctrineOrmExtension constructor + * deprecated "loader" and "property" options of DoctrineType + +2.4.0 +----- + + * deprecated DoctrineOrmTestCase class + 2.2.0 ----- diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php index 442903c3dfea..af584579c8a6 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php @@ -308,14 +308,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').'%'; @@ -326,8 +342,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').'%'; @@ -339,8 +355,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').'%'; @@ -352,8 +368,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': @@ -367,11 +383,51 @@ 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; + } + + /** + * Returns a modified version of $managerConfigs. + * + * The manager called $autoMappedManager will map all bundles that are not mepped by other managers. + * + * @param array $managerConfigs + * @param array $bundles + * + * @return array The modified version of $managerConfigs. + */ + protected function fixManagersAutoMappings(array $managerConfigs, array $bundles) + { + if ($autoMappedManager = $this->validateAutoMapping($managerConfigs)) { + foreach (array_keys($bundles) as $bundle) { + foreach ($managerConfigs as $manager) { + if (isset($manager['mappings'][$bundle])) { + continue 2; + } + } + $managerConfigs[$autoMappedManager]['mappings'][$bundle] = array( + 'mapping' => true, + 'is_bundle' => true, + ); + } + $managerConfigs[$autoMappedManager]['auto_mapping'] = false; + } + + return $managerConfigs; } /** @@ -407,4 +463,31 @@ abstract protected function getMappingResourceConfigDirectory(); * @return string */ abstract protected function getMappingResourceExtension(); + + /** + * Search for a manager that is declared as 'auto_mapping' = true. + * + * @param array $managerConfigs + * + * @return null|string The name of the manager. If no one manager is found, returns null + * + * @throws \LogicException + */ + private function validateAutoMapping(array $managerConfigs) + { + $autoMappedManager = null; + foreach ($managerConfigs as $name => $manager) { + if (!$manager['auto_mapping']) { + continue; + } + + if (null !== $autoMappedManager) { + throw new \LogicException(sprintf('You cannot enable "auto_mapping" on more than one manager at the same time (found in "%s" and %s").', $autoMappedManager, $name)); + } + + $autoMappedManager = $name; + } + + return $autoMappedManager; + } } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php index 57f3d9dece13..94f72fd8c8c0 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,34 +69,38 @@ 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); + if (!empty($taggedSubscribers)) { + $subscribersPerCon = $this->groupByConnection($taggedSubscribers); + foreach ($subscribersPerCon as $con => $subscribers) { + $em = $this->getEventManager($con); - uasort($subscribers, $sortFunc); - foreach ($subscribers as $id => $instance) { - if ($container->getDefinition($id)->isAbstract()) { - throw new \InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event subscriber.', $id)); - } + uasort($subscribers, $sortFunc); + foreach ($subscribers as $id => $instance) { + if ($container->getDefinition($id)->isAbstract()) { + throw new \InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event subscriber.', $id)); + } - $em->addMethodCall('addEventSubscriber', array(new Reference($id))); + $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); + 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) { - if ($container->getDefinition($id)->isAbstract()) { - throw new \InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event listener.', $id)); - } + uasort($listeners, $sortFunc); + foreach ($listeners as $id => $instance) { + if ($container->getDefinition($id)->isAbstract()) { + throw new \InvalidArgumentException(sprintf('The abstract service "%s" cannot be tagged as a doctrine event listener.', $id)); + } - $em->addMethodCall('addEventListener', array( - array_unique($instance['event']), - isset($instance['lazy']) && $instance['lazy'] ? $id : new Reference($id), - )); + $em->addMethodCall('addEventListener', array( + array_unique($instance['event']), + isset($instance['lazy']) && $instance['lazy'] ? $id : new Reference($id), + )); + } } } } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php index 1ab331dc1296..fd32b8d4ceb6 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php @@ -74,26 +74,70 @@ abstract class RegisterMappingsPass implements CompilerPassInterface protected $enabledParameter; /** - * @param Definition|Reference $driver driver DI definition or reference - * @param string[] $namespaces list of namespaces handled by $driver - * @param string[] $managerParameters list of container parameters - * that could hold the manager name - * @param string $driverPattern pattern to get the metadata driver service names - * @param string|false $enabledParameter service container parameter that must be - * present to enable the mapping. Set to false - * to not do any check, optional. + * Naming pattern for the configuration service id, for example + * 'doctrine.orm.%s_configuration'. + * + * @var string + */ + private $configurationPattern; + + /** + * Method name to call on the configuration service. This depends on the + * Doctrine implementation. For example addEntityNamespace. + * + * @var string + */ + private $registerAliasMethodName; + + /** + * Map of alias to namespace. + * + * @var string[] + */ + private $aliasMap; + + /** + * Constructor. + * + * The $managerParameters is an ordered list of container parameters that could provide the + * name of the manager to register these namespaces and alias on. The first non-empty name + * is used, the others skipped. + * + * The $aliasMap parameter can be used to define bundle namespace shortcuts like the + * DoctrineBundle provides automatically for objects in the default Entity/Document folder. + * + * @param Definition|Reference $driver Driver DI definition or reference. + * @param string[] $namespaces List of namespaces handled by $driver. + * @param string[] $managerParameters List of container parameters that could + * hold the manager name. + * @param string $driverPattern Pattern for the metadata driver service name. + * @param string $enabledParameter Service container parameter that must be + * present to enable the mapping. Set to false + * to not do any check, optional. + * @param string $configurationPattern Pattern for the Configuration service name. + * @param string $registerAliasMethodName Name of Configuration class method to + * register alias. + * @param string[] $aliasMap Map of alias to namespace. + * + * @since Support for bundle alias was added in Symfony 2.6 */ - public function __construct($driver, array $namespaces, array $managerParameters, $driverPattern, $enabledParameter = false) + public function __construct($driver, array $namespaces, array $managerParameters, $driverPattern, $enabledParameter = false, $configurationPattern = '', $registerAliasMethodName = '', array $aliasMap = array()) { $this->driver = $driver; $this->namespaces = $namespaces; $this->managerParameters = $managerParameters; $this->driverPattern = $driverPattern; $this->enabledParameter = $enabledParameter; + if (count($aliasMap) && (!$configurationPattern || !$registerAliasMethodName)) { + throw new \InvalidArgumentException('configurationPattern and registerAliasMethodName are required to register namespace alias'); + } + $this->configurationPattern = $configurationPattern; + $this->registerAliasMethodName = $registerAliasMethodName; + $this->aliasMap = $aliasMap; } /** - * Register mappings with the metadata drivers. + * Register mappings and alias with the metadata drivers. * * @param ContainerBuilder $container */ @@ -104,39 +148,39 @@ public function process(ContainerBuilder $container) } $mappingDriverDef = $this->getDriver($container); - $chainDriverDefService = $this->getChainDriverServiceName($container); + // Definition for a Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain $chainDriverDef = $container->getDefinition($chainDriverDefService); foreach ($this->namespaces as $namespace) { $chainDriverDef->addMethodCall('addDriver', array($mappingDriverDef, $namespace)); } + + if (!count($this->aliasMap)) { + return; + } + + $configurationServiceName = $this->getConfigurationServiceName($container); + // Definition of the Doctrine\...\Configuration class specific to the Doctrine flavour. + $configurationServiceDefinition = $container->getDefinition($configurationServiceName); + foreach ($this->aliasMap as $alias => $namespace) { + $configurationServiceDefinition->addMethodCall($this->registerAliasMethodName, array($alias, $namespace)); + } } /** * Get the service name of the metadata chain driver that the mappings - * should be registered with. The default implementation loops over the - * managerParameters and applies the first non-empty parameter it finds to - * the driverPattern. + * should be registered with. * * @param ContainerBuilder $container * - * @return string a service definition name + * @return string The name of the chain driver service * * @throws ParameterNotFoundException if non of the managerParameters has a * non-empty value. */ protected function getChainDriverServiceName(ContainerBuilder $container) { - foreach ($this->managerParameters as $param) { - if ($container->hasParameter($param)) { - $name = $container->getParameter($param); - if ($name) { - return sprintf($this->driverPattern, $name); - } - } - } - - throw new ParameterNotFoundException('None of the managerParameters resulted in a valid name'); + return sprintf($this->driverPattern, $this->getManagerName($container)); } /** @@ -152,6 +196,47 @@ protected function getDriver(ContainerBuilder $container) return $this->driver; } + /** + * Get the service name from the pattern and the configured manager name. + * + * @param ContainerBuilder $container + * + * @return string a service definition name + * + * @throws ParameterNotFoundException if none of the managerParameters has a + * non-empty value. + */ + private function getConfigurationServiceName(ContainerBuilder $container) + { + return sprintf($this->configurationPattern, $this->getManagerName($container)); + } + + /** + * Determine the manager name. + * + * The default implementation loops over the managerParameters and returns + * the first non-empty parameter. + * + * @param ContainerBuilder $container + * + * @return string The name of the active manager. + * + * @throws ParameterNotFoundException If none of the managerParameters is found in the container. + */ + private function getManagerName(ContainerBuilder $container) + { + foreach ($this->managerParameters as $param) { + if ($container->hasParameter($param)) { + $name = $container->getParameter($param); + if ($name) { + return $name; + } + } + } + + throw new ParameterNotFoundException('Could not determine the Doctrine manager. Either Doctrine is not configured or a bundle is misconfigured.'); + } + /** * Determine whether this mapping should be activated or not. This allows * to take this decision with the container builder available. diff --git a/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php b/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php new file mode 100644 index 000000000000..1c0e8cdfee26 --- /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; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function save($key, ParsedExpression $expression) + { + $this->cache->save($key, $expression); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php new file mode 100644 index 000000000000..9343876ef35b --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Form\ChoiceList; + +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; + +/** + * Loads choices using a Doctrine object manager. + * + * @author Bernhard Schussek + */ +class DoctrineChoiceLoader implements ChoiceLoaderInterface +{ + /** + * @var ChoiceListFactoryInterface + */ + private $factory; + + /** + * @var ObjectManager + */ + private $manager; + + /** + * @var string + */ + private $class; + + /** + * @var IdReader + */ + private $idReader; + + /** + * @var null|EntityLoaderInterface + */ + private $objectLoader; + + /** + * @var ChoiceListInterface + */ + private $choiceList; + + /** + * Creates a new choice loader. + * + * Optionally, an implementation of {@link EntityLoaderInterface} can be + * passed which optimizes the object loading for one of the Doctrine + * mapper implementations. + * + * @param ChoiceListFactoryInterface $factory The factory for creating + * the loaded choice list + * @param ObjectManager $manager The object manager + * @param string $class The class name of the + * loaded objects + * @param IdReader $idReader The reader for the object + * IDs. + * @param null|EntityLoaderInterface $objectLoader The objects loader + */ + public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader = null, EntityLoaderInterface $objectLoader = null) + { + $classMetadata = $manager->getClassMetadata($class); + + $this->factory = $factory; + $this->manager = $manager; + $this->class = $classMetadata->getName(); + $this->idReader = $idReader ?: new IdReader($manager, $classMetadata); + $this->objectLoader = $objectLoader; + } + + /** + * {@inheritdoc} + */ + public function loadChoiceList($value = null) + { + if ($this->choiceList) { + return $this->choiceList; + } + + $objects = $this->objectLoader + ? $this->objectLoader->getEntities() + : $this->manager->getRepository($this->class)->findAll(); + + $this->choiceList = $this->factory->createListFromChoices($objects, $value); + + return $this->choiceList; + } + + /** + * {@inheritdoc} + */ + public function loadValuesForChoices(array $choices, $value = null) + { + // Performance optimization + if (empty($choices)) { + return array(); + } + + // Optimize performance for single-field identifiers. We already + // know that the IDs are used as values + $optimize = null === $value || is_array($value) && $value[0] === $this->idReader; + + // Attention: This optimization does not check choices for existence + if ($optimize && !$this->choiceList && $this->idReader->isSingleId()) { + $values = array(); + + // Maintain order and indices of the given objects + foreach ($choices as $i => $object) { + if ($object instanceof $this->class) { + // Make sure to convert to the right format + $values[$i] = (string) $this->idReader->getIdValue($object); + } + } + + return $values; + } + + return $this->loadChoiceList($value)->getValuesForChoices($choices); + } + + /** + * {@inheritdoc} + */ + public function loadChoicesForValues(array $values, $value = null) + { + // Performance optimization + // Also prevents the generation of "WHERE id IN ()" queries through the + // object loader. At least with MySQL and on the development machine + // this was tested on, no exception was thrown for such invalid + // statements, consequently no test fails when this code is removed. + // https://github.com/symfony/symfony/pull/8981#issuecomment-24230557 + if (empty($values)) { + return array(); + } + + // Optimize performance in case we have an object loader and + // a single-field identifier + $optimize = null === $value || is_array($value) && $value[0] === $this->idReader; + + if ($optimize && !$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { + $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); + $objectsById = array(); + $objects = array(); + + // Maintain order and indices from the given $values + // An alternative approach to the following loop is to add the + // "INDEX BY" clause to the Doctrine query in the loader, + // but I'm not sure whether that's doable in a generic fashion. + foreach ($unorderedObjects as $object) { + $objectsById[(string) $this->idReader->getIdValue($object)] = $object; + } + + foreach ($values as $i => $id) { + if (isset($objectsById[$id])) { + $objects[$i] = $objectsById[$id]; + } + } + + return $objects; + } + + return $this->loadChoiceList($value)->getChoicesForValues($values); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 976393e74b51..bd3fa8eb2792 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -11,17 +11,22 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; +@trigger_error('The '.__NAMESPACE__.'\EntityChoiceList class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED); + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\Exception\StringCastException; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; /** * A choice list presenting a list of Doctrine entities as choices. * * @author Bernhard Schussek + * + * @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0. + * Use {@link DoctrineChoiceLoader} instead. */ class EntityChoiceList extends ObjectChoiceList { @@ -307,9 +312,12 @@ public function getValuesForChoices(array $entities) * @return array * * @see ChoiceListInterface + * @deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForChoices(array $entities) { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.4 and will be removed in 3.0.', E_USER_DEPRECATED); + // Performance optimization if (empty($entities)) { return array(); @@ -347,9 +355,12 @@ public function getIndicesForChoices(array $entities) * @return array * * @see ChoiceListInterface + * @deprecated since version 2.4, to be removed in 3.0. */ public function getIndicesForValues(array $values) { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.4 and will be removed in 3.0.', E_USER_DEPRECATED); + // Performance optimization if (empty($values)) { return array(); diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php new file mode 100644 index 000000000000..6ae98b57f8d4 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.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\Bridge\Doctrine\Form\ChoiceList; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\Form\Exception\RuntimeException; + +/** + * A utility for reading object IDs. + * + * @since 1.0 + * + * @author Bernhard Schussek + * + * @internal This class is meant for internal use only. + */ +class IdReader +{ + /** + * @var ObjectManager + */ + private $om; + + /** + * @var ClassMetadata + */ + private $classMetadata; + + /** + * @var bool + */ + private $singleId; + + /** + * @var bool + */ + private $intId; + + /** + * @var string + */ + private $idField; + + /** + * @var IdReader|null + */ + private $associationIdReader; + + public function __construct(ObjectManager $om, ClassMetadata $classMetadata) + { + $ids = $classMetadata->getIdentifierFieldNames(); + $idType = $classMetadata->getTypeOfField(current($ids)); + + $this->om = $om; + $this->classMetadata = $classMetadata; + $this->singleId = 1 === count($ids); + $this->intId = $this->singleId && in_array($idType, array('integer', 'smallint', 'bigint')); + $this->idField = current($ids); + + // single field association are resolved, since the schema column could be an int + if ($this->singleId && $classMetadata->hasAssociation($this->idField)) { + $this->associationIdReader = new self($om, $om->getClassMetadata( + $classMetadata->getAssociationTargetClass($this->idField) + )); + + $this->singleId = $this->associationIdReader->isSingleId(); + $this->intId = $this->associationIdReader->isIntId(); + } + } + + /** + * Returns whether the class has a single-column ID. + * + * @return bool Returns `true` if the class has a single-column ID and + * `false` otherwise. + */ + public function isSingleId() + { + return $this->singleId; + } + + /** + * Returns whether the class has a single-column integer ID. + * + * @return bool Returns `true` if the class has a single-column integer ID + * and `false` otherwise. + */ + public function isIntId() + { + return $this->intId; + } + + /** + * Returns the ID value for an object. + * + * This method assumes that the object has a single-column ID. + * + * @param object $object The object. + * + * @return mixed The ID value. + */ + public function getIdValue($object) + { + if (!$object) { + return; + } + + if (!$this->om->contains($object)) { + throw new RuntimeException( + 'Entities passed to the choice field must be managed. Maybe '. + 'persist them in the entity manager?' + ); + } + + $this->om->initializeObject($object); + + $idValue = current($this->classMetadata->getIdentifierValues($object)); + + if ($this->associationIdReader) { + $idValue = $this->associationIdReader->getIdValue($idValue); + } + + return $idValue; + } + + /** + * Returns the name of the ID field. + * + * This method assumes that the object has a single-column ID. + * + * @return string The name of the ID field. + */ + public function getIdField() + { + return $this->idField; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index ce4d508b8e04..3c5bd170009b 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -17,7 +17,10 @@ use Doctrine\Common\Persistence\ObjectManager; /** - * Getting Entities through the ORM QueryBuilder. + * Loads entities using a {@link QueryBuilder} instance. + * + * @author Benjamin Eberlei + * @author Bernhard Schussek */ class ORMQueryBuilderLoader implements EntityLoaderInterface { @@ -34,9 +37,14 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface /** * Construct an ORM Query Builder Loader. * - * @param QueryBuilder|\Closure $queryBuilder - * @param ObjectManager $manager - * @param string $class + * @param QueryBuilder|\Closure $queryBuilder The query builder or a closure + * for creating the query builder. + * Passing a closure is + * deprecated and will not be + * supported anymore as of + * Symfony 3.0. + * @param ObjectManager $manager Deprecated. + * @param string $class Deprecated. * * @throws UnexpectedTypeException */ @@ -49,10 +57,15 @@ public function __construct($queryBuilder, $manager = null, $class = null) } if ($queryBuilder instanceof \Closure) { + @trigger_error('Passing a QueryBuilder closure to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + if (!$manager instanceof ObjectManager) { throw new UnexpectedTypeException($manager, 'Doctrine\Common\Persistence\ObjectManager'); } + @trigger_error('Passing an EntityManager to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + @trigger_error('Passing a class to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + $queryBuilder = $queryBuilder($manager->getRepository($class)); if (!$queryBuilder instanceof QueryBuilder) { diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php index 570cc8f189df..ed8e0a793444 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -14,21 +14,38 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class DoctrineOrmExtension extends AbstractExtension { protected $registry; - public function __construct(ManagerRegistry $registry) + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @var ChoiceListFactoryInterface + */ + private $choiceListFactory; + + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor)); } protected function loadTypes() { return array( - new EntityType($this->registry, PropertyAccess::createPropertyAccessor()), + new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory), ); } diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index e66862e72e1a..ebdcb02ca77a 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -26,12 +26,11 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface { protected $registry; - private $cache; + private $cache = array(); public function __construct(ManagerRegistry $registry) { $this->registry = $registry; - $this->cache = array(); } /** @@ -92,7 +91,7 @@ public function guessRequired($class, $property) return; } - /* @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 3b4abc26689f..99f301972696 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -12,17 +12,21 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ManagerRegistry; -use Symfony\Component\Form\Exception\RuntimeException; use Doctrine\Common\Persistence\ObjectManager; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList; +use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; -use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; +use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; -use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; abstract class DoctrineType extends AbstractType @@ -33,19 +37,86 @@ abstract class DoctrineType extends AbstractType protected $registry; /** - * @var array + * @var ChoiceListFactoryInterface */ - private $choiceListCache = array(); + private $choiceListFactory; /** - * @var PropertyAccessorInterface + * @var IdReader[] */ - private $propertyAccessor; + private $idReaders = array(); - public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null) + /** + * @var DoctrineChoiceLoader[] + */ + private $choiceLoaders = array(); + + /** + * Creates the label for a choice. + * + * For backwards compatibility, objects are cast to strings by default. + * + * @param object $choice The object. + * + * @return string The string representation of the object. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public static function createChoiceLabel($choice) + { + return (string) $choice; + } + + /** + * Creates the field name for a choice. + * + * This method is used to generate field names if the underlying object has + * a single-column integer ID. In that case, the value of the field is + * the ID of the object. That ID is also used as field name. + * + * @param object $choice The object. + * @param int|string $key The choice key. + * @param string $value The choice value. Corresponds to the object's + * ID here. + * + * @return string The field name. + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public static function createChoiceName($choice, $key, $value) + { + return str_replace('-', '_', (string) $value); + } + + /** + * Gets important parts from QueryBuilder that will allow to cache its results. + * For instance in ORM two query builders with an equal SQL string and + * equal parameters are considered to be equal. + * + * @param object $queryBuilder + * + * @return array|false Array with important QueryBuilder parts or false if + * they can't be determined + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public function getQueryBuilderPartsForCachingHash($queryBuilder) + { + return false; + } + + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null) { $this->registry = $registry; - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + $this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator( + new PropertyAccessDecorator( + new DefaultChoiceListFactory(), + $propertyAccessor + ) + ); } public function buildForm(FormBuilderInterface $builder, array $options) @@ -58,93 +129,109 @@ public function buildForm(FormBuilderInterface $builder, array $options) } } - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function configureOptions(OptionsResolver $resolver) { - $choiceListCache = &$this->choiceListCache; $registry = $this->registry; - $propertyAccessor = $this->propertyAccessor; + $choiceListFactory = $this->choiceListFactory; + $idReaders = &$this->idReaders; + $choiceLoaders = &$this->choiceLoaders; $type = $this; - $loader = function (Options $options) use ($type) { - $queryBuilder = (null !== $options['query_builder']) - ? $options['query_builder'] - : $options['em']->getRepository($options['class'])->createQueryBuilder('e'); + $choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders, $type) { + + // Unless the choices are given explicitly, load them on demand + if (null === $options['choices']) { + $hash = null; + $qbParts = null; + + // If there is no QueryBuilder we can safely cache DoctrineChoiceLoader, + // also if concrete Type can return important QueryBuilder parts to generate + // hash key we go for it as well + if (!$options['query_builder'] || false !== ($qbParts = $type->getQueryBuilderPartsForCachingHash($options['query_builder']))) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + $qbParts, + $options['loader'], + )); + + if (isset($choiceLoaders[$hash])) { + return $choiceLoaders[$hash]; + } + } - return $type->getLoader($options['em'], $queryBuilder, $options['class']); - }; + if ($options['loader']) { + $entityLoader = $options['loader']; + } elseif (null !== $options['query_builder']) { + $entityLoader = $type->getLoader($options['em'], $options['query_builder'], $options['class']); + } else { + $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); + $entityLoader = $type->getLoader($options['em'], $queryBuilder, $options['class']); + } + + $doctrineChoiceLoader = new DoctrineChoiceLoader( + $choiceListFactory, + $options['em'], + $options['class'], + $options['id_reader'], + $entityLoader + ); - $choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) { - // Support for closures - $propertyHash = is_object($options['property']) - ? spl_object_hash($options['property']) - : $options['property']; - - $choiceHashes = $options['choices']; - - // Support for recursive arrays - if (is_array($choiceHashes)) { - // A second parameter ($key) is passed, so we cannot use - // spl_object_hash() directly (which strictly requires - // one parameter) - array_walk_recursive($choiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); - } elseif ($choiceHashes instanceof \Traversable) { - $hashes = array(); - foreach ($choiceHashes as $value) { - $hashes[] = spl_object_hash($value); + if ($hash !== null) { + $choiceLoaders[$hash] = $doctrineChoiceLoader; } - $choiceHashes = $hashes; + return $doctrineChoiceLoader; } + }; - $preferredChoiceHashes = $options['preferred_choices']; - - if (is_array($preferredChoiceHashes)) { - array_walk_recursive($preferredChoiceHashes, function (&$value) { - $value = spl_object_hash($value); - }); + $choiceLabel = function (Options $options) { + // BC with the "property" option + if ($options['property']) { + return $options['property']; } - // Support for custom loaders (with query builders) - $loaderHash = is_object($options['loader']) - ? spl_object_hash($options['loader']) - : $options['loader']; + // BC: use __toString() by default + return array(__CLASS__, 'createChoiceLabel'); + }; - // Support for closures - $groupByHash = is_object($options['group_by']) - ? spl_object_hash($options['group_by']) - : $options['group_by']; + $choiceName = function (Options $options) { + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; - $hash = md5(json_encode(array( - spl_object_hash($options['em']), - $options['class'], - $propertyHash, - $loaderHash, - $choiceHashes, - $preferredChoiceHashes, - $groupByHash, - ))); - - if (!isset($choiceListCache[$hash])) { - $choiceListCache[$hash] = new EntityChoiceList( - $options['em'], - $options['class'], - $options['property'], - $options['loader'], - $options['choices'], - $options['preferred_choices'], - $options['group_by'], - $propertyAccessor - ); + // If the object has a single-column, numeric ID, use that ID as + // field name. We can only use numeric IDs as names, as we cannot + // guarantee that a non-numeric ID contains a valid form name + if ($idReader->isIntId()) { + return array(__CLASS__, 'createChoiceName'); } - return $choiceListCache[$hash]; + // Otherwise, an incrementing integer is used as name automatically + }; + + // The choices are always indexed by ID (see "choices" normalizer + // and DoctrineChoiceLoader), unless the ID is composite. Then they + // are indexed by an incrementing integer. + // Use the ID/incrementing integer as choice value. + $choiceValue = function (Options $options) { + /** @var IdReader $idReader */ + $idReader = $options['id_reader']; + + // If the entity has a single-column ID, use that ID as value + if ($idReader->isSingleId()) { + return array($idReader, 'getIdValue'); + } + + // Otherwise, an incrementing integer is used as value automatically }; $emNormalizer = function (Options $options, $em) use ($registry) { /* @var ManagerRegistry $registry */ if (null !== $em) { + if ($em instanceof ObjectManager) { + return $em; + } + return $registry->getManager($em); } @@ -161,25 +248,81 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) return $em; }; + // deprecation note + $propertyNormalizer = function (Options $options, $propertyName) { + if ($propertyName) { + @trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED); + } + + return $propertyName; + }; + + // Invoke the query builder closure so that we can cache choice lists + // for equal query builders + $queryBuilderNormalizer = function (Options $options, $queryBuilder) { + if (is_callable($queryBuilder)) { + $queryBuilder = call_user_func($queryBuilder, $options['em']->getRepository($options['class'])); + } + + return $queryBuilder; + }; + + // deprecation note + $loaderNormalizer = function (Options $options, $loader) { + if ($loader) { + @trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED); + } + + return $loader; + }; + + // Set the "id_reader" option via the normalizer. This option is not + // supposed to be set by the user. + $idReaderNormalizer = function (Options $options) use (&$idReaders) { + $hash = CachingFactoryDecorator::generateHash(array( + $options['em'], + $options['class'], + )); + + // The ID reader is a utility that is needed to read the object IDs + // when generating the field values. The callback generating the + // field values has no access to the object manager or the class + // of the field, so we store that information in the reader. + // The reader is cached so that two choice lists for the same class + // (and hence with the same reader) can successfully be cached. + if (!isset($idReaders[$hash])) { + $classMetadata = $options['em']->getClassMetadata($options['class']); + $idReaders[$hash] = new IdReader($options['em'], $classMetadata); + } + + return $idReaders[$hash]; + }; + $resolver->setDefaults(array( 'em' => null, - 'property' => null, + 'property' => null, // deprecated, use "choice_label" 'query_builder' => null, - 'loader' => $loader, + 'loader' => null, // deprecated, use "choice_loader" 'choices' => null, - 'choice_list' => $choiceList, - 'group_by' => null, + 'choices_as_values' => true, + 'choice_loader' => $choiceLoader, + 'choice_label' => $choiceLabel, + 'choice_name' => $choiceName, + 'choice_value' => $choiceValue, + 'id_reader' => null, // internal + 'choice_translation_domain' => false, )); $resolver->setRequired(array('class')); - $resolver->setNormalizers(array( - 'em' => $emNormalizer, - )); + $resolver->setNormalizer('em', $emNormalizer); + $resolver->setNormalizer('property', $propertyNormalizer); + $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); + $resolver->setNormalizer('loader', $loaderNormalizer); + $resolver->setNormalizer('id_reader', $idReaderNormalizer); - $resolver->setAllowedTypes(array( - 'loader' => array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'), - )); + $resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager')); + $resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 79f2767e3078..fef097d3ea60 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -12,30 +12,84 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ORM\Query\Parameter; +use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; class EntityType extends DoctrineType { + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + // Invoke the query builder closure so that we can cache choice lists + // for equal query builders + $queryBuilderNormalizer = function (Options $options, $queryBuilder) { + if (is_callable($queryBuilder)) { + $queryBuilder = call_user_func($queryBuilder, $options['em']->getRepository($options['class'])); + + if (!$queryBuilder instanceof QueryBuilder) { + throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); + } + } + + return $queryBuilder; + }; + + $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); + $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder')); + } + /** * Return the default loader object. * * @param ObjectManager $manager - * @param mixed $queryBuilder + * @param QueryBuilder $queryBuilder * @param string $class * * @return ORMQueryBuilderLoader */ public function getLoader(ObjectManager $manager, $queryBuilder, $class) { - return new ORMQueryBuilderLoader( - $queryBuilder, - $manager, - $class - ); + return new ORMQueryBuilderLoader($queryBuilder, $manager, $class); } public function getName() { return 'entity'; } + + /** + * We consider two query builders with an equal SQL string and + * equal parameters to be equal. + * + * @param QueryBuilder $queryBuilder + * + * @return array + * + * @internal This method is public to be usable as callback. It should not + * be used in user code. + */ + public function getQueryBuilderPartsForCachingHash($queryBuilder) + { + return array( + $queryBuilder->getQuery()->getSQL(), + array_map(array($this, 'parameterToArray'), $queryBuilder->getParameters()->toArray()), + ); + } + + /** + * Converts a query parameter to an array. + * + * @param Parameter $parameter The query parameter + * + * @return array The array representation of the parameter + */ + private function parameterToArray(Parameter $parameter) + { + return array($parameter->getName(), $parameter->getType(), $parameter->getValue()); + } } diff --git a/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php b/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php index fd7dcff62c3d..92c7b6bf40af 100644 --- a/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php +++ b/src/Symfony/Bridge/Doctrine/HttpFoundation/DbalSessionHandler.php @@ -180,7 +180,7 @@ public function write($sessionId, $data) $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); $updateStmt->execute(); - // When MERGE is not supported, like in Postgres, we have to use this approach that can result in + // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in // duplicate key errors when the same session is written simultaneously. We can just catch such an // error and re-execute the update. This is similar to a serializable transaction with retry logic // on serialization failures but without the overhead and without possible false positives due to @@ -224,11 +224,11 @@ private function getMergeSql() { $platform = $this->con->getDatabasePlatform()->getName(); - switch ($platform) { - case 'mysql': + switch (true) { + case 'mysql' === $platform: return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)"; - case 'oracle': + case 'oracle' === $platform: // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". @@ -239,8 +239,11 @@ private function getMergeSql() return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ". "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;"; - case 'sqlite': + case 'sqlite' === $platform: return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"; + case 'postgresql' === $platform && version_compare($this->con->getServerVersion(), '9.5', '>='): + return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ". + "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->timeCol)"; } } } diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index a07e9d180aae..b2216b7ae31d 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -46,7 +46,7 @@ class DoctrineTokenProvider implements TokenProviderInterface private $conn; /** - * new DoctrineTokenProvider for the RemembeMe authentication service. + * new DoctrineTokenProvider for the RememberMe authentication service. * * @param Connection $conn */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php index 2643819f3d83..ba73678541e7 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php @@ -139,6 +139,17 @@ public function testProcessEventSubscribersWithPriorities() $this->assertEquals(array('b', 'a'), $serviceOrder); } + 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 000000000000..760c0fada0fa --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php @@ -0,0 +1,284 @@ + + * + * 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; + })); + } + + /** + * @expectedException \LogicException + */ + public function testFixManagersAutoMappingsWithTwoAutomappings() + { + $emConfigs = array( + 'em1' => array( + 'auto_mapping' => true, + ), + 'em2' => array( + 'auto_mapping' => true, + ), + ); + + $bundles = array( + 'FristBundle' => 'My\FristBundle', + 'SecondBundle' => 'My\SecondBundle', + ); + + $reflection = new \ReflectionClass(get_class($this->extension)); + $method = $reflection->getMethod('fixManagersAutoMappings'); + $method->setAccessible(true); + + $method->invoke($this->extension, $emConfigs, $bundles); + } + + public function getAutomappingData() + { + return array( + array( + array( // no auto mapping on em1 + 'auto_mapping' => false, + ), + array( // no auto mapping on em2 + 'auto_mapping' => false, + ), + array(), + array(), + ), + array( + array( // no auto mapping on em1 + 'auto_mapping' => false, + ), + array( // auto mapping enabled on em2 + 'auto_mapping' => true, + ), + array(), + array( + 'mappings' => array( + 'FristBundle' => array( + 'mapping' => true, + 'is_bundle' => true, + ), + 'SecondBundle' => array( + 'mapping' => true, + 'is_bundle' => true, + ), + ), + ), + ), + array( + array( // no auto mapping on em1, but it defines SecondBundle as own + 'auto_mapping' => false, + 'mappings' => array( + 'SecondBundle' => array( + 'mapping' => true, + 'is_bundle' => true, + ), + ), + ), + array( // auto mapping enabled on em2 + 'auto_mapping' => true, + ), + array( + 'mappings' => array( + 'SecondBundle' => array( + 'mapping' => true, + 'is_bundle' => true, + ), + ), + ), + array( + 'mappings' => array( + 'FristBundle' => array( + 'mapping' => true, + 'is_bundle' => true, + ), + ), + ), + ), + ); + } + + /** + * @dataProvider getAutomappingData + */ + public function testFixManagersAutoMappings(array $originalEm1, array $originalEm2, array $expectedEm1, array $expectedEm2) + { + $emConfigs = array( + 'em1' => $originalEm1, + 'em2' => $originalEm2, + ); + + $bundles = array( + 'FristBundle' => 'My\FristBundle', + 'SecondBundle' => 'My\SecondBundle', + ); + + $reflection = new \ReflectionClass(get_class($this->extension)); + $method = $reflection->getMethod('fixManagersAutoMappings'); + $method->setAccessible(true); + + $newEmConfigs = $method->invoke($this->extension, $emConfigs, $bundles); + + $this->assertEquals($newEmConfigs['em1'], array_merge(array( + 'auto_mapping' => false, + ), $expectedEm1)); + $this->assertEquals($newEmConfigs['em2'], array_merge(array( + 'auto_mapping' => false, + ), $expectedEm2)); + } + + 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 e506352d161d..283c65ee4d13 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DoctrineOrmTestCase.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DoctrineOrmTestCase.php @@ -11,18 +11,21 @@ namespace Symfony\Bridge\Doctrine\Tests; +@trigger_error('The '.__NAMESPACE__.'\DoctrineOrmTestCase class is deprecated since version 2.4 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper class instead.', E_USER_DEPRECATED); + +use Doctrine\ORM\EntityManager; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; /** * Class DoctrineOrmTestCase. * - * @deprecated Deprecated as of Symfony 2.3, to be removed in Symfony 3.0. + * @deprecated since version 2.4, to be removed in 3.0. * Use {@link DoctrineTestHelper} instead. */ abstract class DoctrineOrmTestCase extends \PHPUnit_Framework_TestCase { /** - * @return \Doctrine\ORM\EntityManager + * @return EntityManager */ public static function createTestEntityManager() { 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 000000000000..a473b3ace2fe --- /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/Fixtures/SingleStringCastableIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringCastableIdEntity.php new file mode 100644 index 000000000000..e457f69dd091 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringCastableIdEntity.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\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\GeneratedValue; +use Doctrine\ORM\Mapping\Id; + +/** @Entity */ +class SingleStringCastableIdEntity +{ + /** + * @Id + * @Column(type="string") + * @GeneratedValue(strategy="NONE") + */ + protected $id; + + /** @Column(type="string", nullable=true) */ + public $name; + + public function __construct($id, $name) + { + $this->id = new StringCastableObjectIdentity($id); + $this->name = $name; + } + + public function __toString() + { + return (string) $this->name; + } +} + +class StringCastableObjectIdentity +{ + protected $id; + + public function __construct($id) + { + $this->id = $id; + } + + public function __toString() + { + return (string) $this->id; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/AbstractEntityChoiceListSingleAssociationToIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/AbstractEntityChoiceListSingleAssociationToIntIdTest.php index 80710828e6c4..7324f721ec34 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/AbstractEntityChoiceListSingleAssociationToIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/AbstractEntityChoiceListSingleAssociationToIntIdTest.php @@ -20,6 +20,7 @@ * * @author Premi Giorgio * @author Bernhard Schussek + * @group legacy */ abstract class AbstractEntityChoiceListSingleAssociationToIntIdTest extends AbstractEntityChoiceListTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php new file mode 100644 index 000000000000..4848d88818f8 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php @@ -0,0 +1,459 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; + +use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Common\Persistence\ObjectRepository; +use Doctrine\ORM\Mapping\ClassMetadata; +use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; +use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; +use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; +use Symfony\Component\Form\ChoiceList\ArrayChoiceList; +use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; + +/** + * @author Bernhard Schussek + */ +class DoctrineChoiceLoaderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ChoiceListFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var ObjectManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $om; + + /** + * @var ObjectRepository|\PHPUnit_Framework_MockObject_MockObject + */ + private $repository; + + /** + * @var string + */ + private $class; + + /** + * @var IdReader|\PHPUnit_Framework_MockObject_MockObject + */ + private $idReader; + + /** + * @var EntityLoaderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectLoader; + + /** + * @var \stdClass + */ + private $obj1; + + /** + * @var \stdClass + */ + private $obj2; + + /** + * @var \stdClass + */ + private $obj3; + + protected function setUp() + { + $this->factory = $this->getMock('Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface'); + $this->om = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); + $this->repository = $this->getMock('Doctrine\Common\Persistence\ObjectRepository'); + $this->class = 'stdClass'; + $this->idReader = $this->getMockBuilder('Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader') + ->disableOriginalConstructor() + ->getMock(); + $this->objectLoader = $this->getMock('Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'); + $this->obj1 = (object) array('name' => 'A'); + $this->obj2 = (object) array('name' => 'B'); + $this->obj3 = (object) array('name' => 'C'); + + $this->om->expects($this->any()) + ->method('getRepository') + ->with($this->class) + ->willReturn($this->repository); + + $this->om->expects($this->any()) + ->method('getClassMetadata') + ->with($this->class) + ->willReturn(new ClassMetadata($this->class)); + } + + public function testLoadChoiceList() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $choices = array($this->obj1, $this->obj2, $this->obj3); + $choiceList = new ArrayChoiceList(array()); + $value = function () {}; + + $this->repository->expects($this->once()) + ->method('findAll') + ->willReturn($choices); + + $this->factory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $value) + ->willReturn($choiceList); + + $this->assertSame($choiceList, $loader->loadChoiceList($value)); + + // no further loads on subsequent calls + + $this->assertSame($choiceList, $loader->loadChoiceList($value)); + } + + public function testLoadChoiceListUsesObjectLoaderIfAvailable() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader, + $this->objectLoader + ); + + $choices = array($this->obj1, $this->obj2, $this->obj3); + $choiceList = new ArrayChoiceList(array()); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->objectLoader->expects($this->once()) + ->method('getEntities') + ->willReturn($choices); + + $this->factory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices) + ->willReturn($choiceList); + + $this->assertSame($choiceList, $loader->loadChoiceList()); + + // no further loads on subsequent calls + + $this->assertSame($choiceList, $loader->loadChoiceList()); + } + + public function testLoadValuesForChoices() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $choices = array($this->obj1, $this->obj2, $this->obj3); + $choiceList = new ArrayChoiceList($choices); + + $this->repository->expects($this->once()) + ->method('findAll') + ->willReturn($choices); + + $this->factory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices) + ->willReturn($choiceList); + + $this->assertSame(array('1', '2'), $loader->loadValuesForChoices(array($this->obj2, $this->obj3))); + + // no further loads on subsequent calls + + $this->assertSame(array('1', '2'), $loader->loadValuesForChoices(array($this->obj2, $this->obj3))); + } + + public function testLoadValuesForChoicesDoesNotLoadIfEmptyChoices() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->factory->expects($this->never()) + ->method('createListFromChoices'); + + $this->assertSame(array(), $loader->loadValuesForChoices(array())); + } + + public function testLoadValuesForChoicesDoesNotLoadIfSingleIntId() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $this->idReader->expects($this->any()) + ->method('isSingleId') + ->willReturn(true); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->factory->expects($this->never()) + ->method('createListFromChoices'); + + $this->idReader->expects($this->any()) + ->method('getIdValue') + ->with($this->obj2) + ->willReturn('2'); + + $this->assertSame(array('2'), $loader->loadValuesForChoices(array($this->obj2))); + } + + public function testLoadValuesForChoicesLoadsIfSingleIntIdAndValueGiven() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $choices = array($this->obj1, $this->obj2, $this->obj3); + $value = function (\stdClass $object) { return $object->name; }; + $choiceList = new ArrayChoiceList($choices, $value); + + $this->idReader->expects($this->any()) + ->method('isSingleId') + ->willReturn(true); + + $this->repository->expects($this->once()) + ->method('findAll') + ->willReturn($choices); + + $this->factory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $value) + ->willReturn($choiceList); + + $this->assertSame(array('B'), $loader->loadValuesForChoices( + array($this->obj2), + $value + )); + } + + public function testLoadValuesForChoicesDoesNotLoadIfValueIsIdReader() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $value = array($this->idReader, 'getIdValue'); + + $this->idReader->expects($this->any()) + ->method('isSingleId') + ->willReturn(true); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->factory->expects($this->never()) + ->method('createListFromChoices'); + + $this->idReader->expects($this->any()) + ->method('getIdValue') + ->with($this->obj2) + ->willReturn('2'); + + $this->assertSame(array('2'), $loader->loadValuesForChoices( + array($this->obj2), + $value + )); + } + + public function testLoadChoicesForValues() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $choices = array($this->obj1, $this->obj2, $this->obj3); + $choiceList = new ArrayChoiceList($choices); + + $this->repository->expects($this->once()) + ->method('findAll') + ->willReturn($choices); + + $this->factory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices) + ->willReturn($choiceList); + + $this->assertSame(array($this->obj2, $this->obj3), $loader->loadChoicesForValues(array('1', '2'))); + + // no further loads on subsequent calls + + $this->assertSame(array($this->obj2, $this->obj3), $loader->loadChoicesForValues(array('1', '2'))); + } + + public function testLoadChoicesForValuesDoesNotLoadIfEmptyValues() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->factory->expects($this->never()) + ->method('createListFromChoices'); + + $this->assertSame(array(), $loader->loadChoicesForValues(array())); + } + + public function testLoadChoicesForValuesLoadsOnlyChoicesIfSingleIntId() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader, + $this->objectLoader + ); + + $choices = array($this->obj2, $this->obj3); + + $this->idReader->expects($this->any()) + ->method('isSingleId') + ->willReturn(true); + + $this->idReader->expects($this->any()) + ->method('getIdField') + ->willReturn('idField'); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->factory->expects($this->never()) + ->method('createListFromChoices'); + + $this->objectLoader->expects($this->once()) + ->method('getEntitiesByIds') + ->with('idField', array(4 => '3', 7 => '2')) + ->willReturn($choices); + + $this->idReader->expects($this->any()) + ->method('getIdValue') + ->willReturnMap(array( + array($this->obj2, '2'), + array($this->obj3, '3'), + )); + + $this->assertSame( + array(4 => $this->obj3, 7 => $this->obj2), + $loader->loadChoicesForValues(array(4 => '3', 7 => '2') + )); + } + + public function testLoadChoicesForValuesLoadsAllIfSingleIntIdAndValueGiven() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader + ); + + $choices = array($this->obj1, $this->obj2, $this->obj3); + $value = function (\stdClass $object) { return $object->name; }; + $choiceList = new ArrayChoiceList($choices, $value); + + $this->idReader->expects($this->any()) + ->method('isSingleId') + ->willReturn(true); + + $this->repository->expects($this->once()) + ->method('findAll') + ->willReturn($choices); + + $this->factory->expects($this->once()) + ->method('createListFromChoices') + ->with($choices, $value) + ->willReturn($choiceList); + + $this->assertSame(array($this->obj2), $loader->loadChoicesForValues( + array('B'), + $value + )); + } + + public function testLoadChoicesForValuesLoadsOnlyChoicesIfValueIsIdReader() + { + $loader = new DoctrineChoiceLoader( + $this->factory, + $this->om, + $this->class, + $this->idReader, + $this->objectLoader + ); + + $choices = array($this->obj2, $this->obj3); + $value = array($this->idReader, 'getIdValue'); + + $this->idReader->expects($this->any()) + ->method('isSingleId') + ->willReturn(true); + + $this->idReader->expects($this->any()) + ->method('getIdField') + ->willReturn('idField'); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->factory->expects($this->never()) + ->method('createListFromChoices'); + + $this->objectLoader->expects($this->once()) + ->method('getEntitiesByIds') + ->with('idField', array('2')) + ->willReturn($choices); + + $this->idReader->expects($this->any()) + ->method('getIdValue') + ->willReturnMap(array( + array($this->obj2, '2'), + array($this->obj3, '3'), + )); + + $this->assertSame(array($this->obj2), $loader->loadChoicesForValues(array('2'), $value)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php index ebb7ed0e987a..f4530091fbb0 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/GenericEntityChoiceListTest.php @@ -19,6 +19,9 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Doctrine\ORM\Tools\SchemaTool; +/** + * @group legacy + */ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase { const SINGLE_INT_ID_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; @@ -265,6 +268,21 @@ public function testInitShorthandEntityName() ); $this->assertEquals(array(1, 2), $choiceList->getValuesForChoices(array($item1, $item2))); + } + + public function testInitShorthandEntityName2() + { + $item1 = new SingleIntIdEntity(1, 'Foo'); + $item2 = new SingleIntIdEntity(2, 'Bar'); + + $this->em->persist($item1); + $this->em->persist($item2); + + $choiceList = new EntityChoiceList( + $this->em, + 'SymfonyTestsDoctrine:SingleIntIdEntity' + ); + $this->assertEquals(array(1, 2), $choiceList->getIndicesForChoices(array($item1, $item2))); } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php index 90cbf1d7c8b3..a2ee7cdc8a64 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListCompositeIdTest.php @@ -13,6 +13,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleAssociationToIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleAssociationToIntIdTest.php index e16be91f4691..60e3797bac86 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleAssociationToIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleAssociationToIntIdTest.php @@ -14,6 +14,7 @@ /** * @author Premi Giorgio * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListSingleAssociationToIntIdTest extends AbstractEntityChoiceListSingleAssociationToIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php index 52d04c38798a..f655784004fb 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleIntIdTest.php @@ -13,6 +13,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php index 690d4b3d2300..629b399ac36a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/LoadedEntityChoiceListSingleStringIdTest.php @@ -13,6 +13,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class LoadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php index 81b716ef38e2..d755af5430bc 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php @@ -20,6 +20,7 @@ class ORMQueryBuilderLoaderTest extends \PHPUnit_Framework_TestCase { /** * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + * @group legacy */ public function testItOnlyWorksWithQueryBuilderOrClosure() { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php index 5740a2ff9434..114eee661efe 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdTest.php @@ -13,6 +13,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php index 9c72ccccd91a..422295feb1e0 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest extends UnloadedEntityChoiceListCompositeIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdTest.php index 00a4f545aaf4..20267bd02077 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdTest.php @@ -14,6 +14,7 @@ /** * @author Premi Giorgio * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleAssociationToIntIdTest extends AbstractEntityChoiceListSingleAssociationToIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdWithQueryBuilderTest.php index 4cad38055290..45ab799585cb 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleAssociationToIntIdWithQueryBuilderTest.php @@ -17,6 +17,7 @@ /** * @author Premi Giorgio * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleAssociationToIntIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleAssociationToIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php index dd53bf422615..0668c09bdb63 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdTest.php @@ -13,6 +13,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest { @@ -20,12 +21,4 @@ public function testGetIndicesForValuesIgnoresNonExistingValues() { $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); } - - /** - * @group legacy - */ - public function testLegacyGetIndicesForValuesIgnoresNonExistingValues() - { - $this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.'); - } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php index fa5bb80ae7be..c093782ff0ec 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleIntIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php index 5b25b49a710b..39363bae26a2 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdTest.php @@ -13,6 +13,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php index 9fba5b9295a0..23329e80df3c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest.php @@ -16,6 +16,7 @@ /** * @author Bernhard Schussek + * @group legacy */ class UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleStringIdTest { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php index 1269812bbf15..57df4195bcec 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php @@ -90,7 +90,7 @@ public function testCollapsedEntityField() $this->setMaxRunningTime(1); for ($i = 0; $i < 40; ++$i) { - $form = $this->factory->create('entity', null, array( + $form = $this->factory->create('Symfony\Bridge\Doctrine\Form\Type\EntityType', null, array( 'class' => self::ENTITY_CLASS, )); @@ -108,7 +108,7 @@ public function testCollapsedEntityFieldWithChoices() $this->setMaxRunningTime(1); for ($i = 0; $i < 40; ++$i) { - $form = $this->factory->create('entity', null, array( + $form = $this->factory->create('Symfony\Bridge\Doctrine\Form\Type\EntityType', null, array( 'class' => self::ENTITY_CLASS, 'choices' => $choices, )); @@ -127,7 +127,7 @@ public function testCollapsedEntityFieldWithPreferredChoices() $this->setMaxRunningTime(1); for ($i = 0; $i < 40; ++$i) { - $form = $this->factory->create('entity', null, array( + $form = $this->factory->create('Symfony\Bridge\Doctrine\Form\Type\EntityType', null, array( 'class' => self::ENTITY_CLASS, 'preferred_choices' => $choices, )); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 32b1985cc4be..5341e81f7b60 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -11,33 +11,47 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Tools\SchemaTool; +use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; +use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringCastableIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; -use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; -use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; -use Doctrine\ORM\Tools\SchemaTool; -use Doctrine\Common\Collections\ArrayCollection; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Forms; +use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleAssociationToIntIdEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity; class EntityTypeTest extends TypeTestCase { const ITEM_GROUP_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity'; const SINGLE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; + const SINGLE_IDENT_NO_TO_STRING_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity'; const SINGLE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity'; + const SINGLE_ASSOC_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleAssociationToIntIdEntity'; + const SINGLE_STRING_CASTABLE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringCastableIdEntity'; const COMPOSITE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity'; const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity'; /** - * @var \Doctrine\ORM\EntityManager + * @var EntityManager */ private $em; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry */ private $emRegistry; @@ -52,7 +66,10 @@ protected function setUp() $classes = array( $this->em->getClassMetadata(self::ITEM_GROUP_CLASS), $this->em->getClassMetadata(self::SINGLE_IDENT_CLASS), + $this->em->getClassMetadata(self::SINGLE_IDENT_NO_TO_STRING_CLASS), $this->em->getClassMetadata(self::SINGLE_STRING_IDENT_CLASS), + $this->em->getClassMetadata(self::SINGLE_ASSOC_IDENT_CLASS), + $this->em->getClassMetadata(self::SINGLE_STRING_CASTABLE_IDENT_CLASS), $this->em->getClassMetadata(self::COMPOSITE_IDENT_CLASS), $this->em->getClassMetadata(self::COMPOSITE_STRING_IDENT_CLASS), ); @@ -102,6 +119,16 @@ public function testClassOptionIsRequired() $this->factory->createNamed('name', 'entity'); } + /** + * @expectedException \Symfony\Component\Form\Exception\RuntimeException + */ + public function testInvalidClassOption() + { + $this->factory->createNamed('name', 'entity', null, array( + 'class' => 'foo', + )); + } + public function testSetDataToUninitializedEntityWithNonRequired() { $entity1 = new SingleIntIdEntity(1, 'Foo'); @@ -113,7 +140,7 @@ public function testSetDataToUninitializedEntityWithNonRequired() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); @@ -147,7 +174,7 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, - 'property' => 'name', + 'choice_label' => 'name', 'query_builder' => $qb, )); @@ -155,7 +182,7 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() } /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException */ public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure() { @@ -234,7 +261,7 @@ public function testSubmitSingleExpandedNull() $field->submit(null); $this->assertNull($field->getData()); - $this->assertSame(array(), $field->getViewData()); + $this->assertSame('', $field->getViewData(), 'View data is always a string'); } public function testSubmitSingleNonExpandedNull() @@ -276,7 +303,32 @@ public function testSubmitSingleNonExpandedSingleIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', + )); + + $field->submit('2'); + + $this->assertTrue($field->isSynchronized()); + $this->assertSame($entity2, $field->getData()); + $this->assertSame('2', $field->getViewData()); + } + + public function testSubmitSingleNonExpandedSingleAssocIdentifier() + { + $innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo'); + $innerEntity2 = new SingleIntIdNoToStringEntity(2, 'InBar'); + + $entity1 = new SingleAssociationToIntIdEntity($innerEntity1, 'Foo'); + $entity2 = new SingleAssociationToIntIdEntity($innerEntity2, 'Bar'); + + $this->persist(array($innerEntity1, $innerEntity2, $entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => false, + 'expanded' => false, + 'em' => 'default', + 'class' => self::SINGLE_ASSOC_IDENT_CLASS, + 'choice_label' => 'name', )); $field->submit('2'); @@ -298,7 +350,7 @@ public function testSubmitSingleNonExpandedCompositeIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // the collection key is used here @@ -322,7 +374,36 @@ public function testSubmitMultipleNonExpandedSingleIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', + )); + + $field->submit(array('1', '3')); + + $expected = new ArrayCollection(array($entity1, $entity3)); + + $this->assertTrue($field->isSynchronized()); + $this->assertEquals($expected, $field->getData()); + $this->assertSame(array('1', '3'), $field->getViewData()); + } + + public function testSubmitMultipleNonExpandedSingleAssocIdentifier() + { + $innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo'); + $innerEntity2 = new SingleIntIdNoToStringEntity(2, 'InBar'); + $innerEntity3 = new SingleIntIdNoToStringEntity(3, 'InBaz'); + + $entity1 = new SingleAssociationToIntIdEntity($innerEntity1, 'Foo'); + $entity2 = new SingleAssociationToIntIdEntity($innerEntity2, 'Bar'); + $entity3 = new SingleAssociationToIntIdEntity($innerEntity3, 'Baz'); + + $this->persist(array($innerEntity1, $innerEntity2, $innerEntity3, $entity1, $entity2, $entity3)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => true, + 'expanded' => false, + 'em' => 'default', + 'class' => self::SINGLE_ASSOC_IDENT_CLASS, + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -347,7 +428,7 @@ public function testSubmitMultipleNonExpandedSingleIdentifierForExistingData() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -378,7 +459,7 @@ public function testSubmitMultipleNonExpandedCompositeIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // because of the composite key collection keys are used @@ -404,7 +485,7 @@ public function testSubmitMultipleNonExpandedCompositeIdentifierExistingData() 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $existing = new ArrayCollection(array(0 => $entity2)); @@ -434,7 +515,7 @@ public function testSubmitSingleExpanded() 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -460,7 +541,7 @@ public function testSubmitMultipleExpanded() 'expanded' => true, 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit(array('1', '3')); @@ -477,6 +558,164 @@ public function testSubmitMultipleExpanded() $this->assertSame('3', $field['3']->getViewData()); } + public function testSubmitMultipleExpandedWithNegativeIntegerId() + { + $entity1 = new SingleIntIdEntity(-1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => true, + 'expanded' => true, + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + + $field->submit(array('-1')); + + $expected = new ArrayCollection(array($entity1)); + + $this->assertTrue($field->isSynchronized()); + $this->assertEquals($expected, $field->getData()); + $this->assertTrue($field['_1']->getData()); + $this->assertFalse($field['2']->getData()); + } + + public function testSubmitSingleNonExpandedStringCastableIdentifier() + { + $entity1 = new SingleStringCastableIdEntity(1, 'Foo'); + $entity2 = new SingleStringCastableIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => false, + 'expanded' => false, + 'em' => 'default', + 'class' => self::SINGLE_STRING_CASTABLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + + $field->submit('2'); + + $this->assertTrue($field->isSynchronized()); + $this->assertSame($entity2, $field->getData()); + $this->assertSame('2', $field->getViewData()); + } + + public function testSubmitSingleStringCastableIdentifierExpanded() + { + $entity1 = new SingleStringCastableIdEntity(1, 'Foo'); + $entity2 = new SingleStringCastableIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => false, + 'expanded' => true, + 'em' => 'default', + 'class' => self::SINGLE_STRING_CASTABLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + + $field->submit('2'); + + $this->assertTrue($field->isSynchronized()); + $this->assertSame($entity2, $field->getData()); + $this->assertFalse($field['0']->getData()); + $this->assertTrue($field['1']->getData()); + $this->assertNull($field['0']->getViewData()); + $this->assertSame('2', $field['1']->getViewData()); + } + + public function testSubmitMultipleNonExpandedStringCastableIdentifierForExistingData() + { + $entity1 = new SingleStringCastableIdEntity(1, 'Foo'); + $entity2 = new SingleStringCastableIdEntity(2, 'Bar'); + $entity3 = new SingleStringCastableIdEntity(3, 'Baz'); + + $this->persist(array($entity1, $entity2, $entity3)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => true, + 'expanded' => false, + 'em' => 'default', + 'class' => self::SINGLE_STRING_CASTABLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + + $existing = new ArrayCollection(array(0 => $entity2)); + + $field->setData($existing); + $field->submit(array('1', '3')); + + // entry with index 0 ($entity2) was replaced + $expected = new ArrayCollection(array(0 => $entity1, 1 => $entity3)); + + $this->assertTrue($field->isSynchronized()); + $this->assertEquals($expected, $field->getData()); + // same object still, useful if it is a PersistentCollection + $this->assertSame($existing, $field->getData()); + $this->assertSame(array('1', '3'), $field->getViewData()); + } + + public function testSubmitMultipleNonExpandedStringCastableIdentifier() + { + $entity1 = new SingleStringCastableIdEntity(1, 'Foo'); + $entity2 = new SingleStringCastableIdEntity(2, 'Bar'); + $entity3 = new SingleStringCastableIdEntity(3, 'Baz'); + + $this->persist(array($entity1, $entity2, $entity3)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => true, + 'expanded' => false, + 'em' => 'default', + 'class' => self::SINGLE_STRING_CASTABLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + + $field->submit(array('1', '3')); + + $expected = new ArrayCollection(array($entity1, $entity3)); + + $this->assertTrue($field->isSynchronized()); + $this->assertEquals($expected, $field->getData()); + $this->assertSame(array('1', '3'), $field->getViewData()); + } + + public function testSubmitMultipleStringCastableIdentifierExpanded() + { + $entity1 = new SingleStringCastableIdEntity(1, 'Foo'); + $entity2 = new SingleStringCastableIdEntity(2, 'Bar'); + $entity3 = new SingleStringCastableIdEntity(3, 'Bar'); + + $this->persist(array($entity1, $entity2, $entity3)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'multiple' => true, + 'expanded' => true, + 'em' => 'default', + 'class' => self::SINGLE_STRING_CASTABLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + + $field->submit(array('1', '3')); + + $expected = new ArrayCollection(array($entity1, $entity3)); + + $this->assertTrue($field->isSynchronized()); + $this->assertEquals($expected, $field->getData()); + $this->assertTrue($field['0']->getData()); + $this->assertFalse($field['1']->getData()); + $this->assertTrue($field['2']->getData()); + $this->assertSame('1', $field['0']->getViewData()); + $this->assertNull($field['1']->getViewData()); + $this->assertSame('3', $field['2']->getViewData()); + } + public function testOverrideChoices() { $entity1 = new SingleIntIdEntity(1, 'Foo'); @@ -490,7 +729,7 @@ public function testOverrideChoices() 'class' => self::SINGLE_IDENT_CLASS, // not all persisted entities should be displayed 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -501,6 +740,83 @@ public function testOverrideChoices() $this->assertSame('2', $field->getViewData()); } + public function testOverrideChoicesValues() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'choice_label' => 'name', + 'choice_value' => 'name', + )); + + $field->submit('Bar'); + + $this->assertEquals(array('Foo' => new ChoiceView($entity1, 'Foo', 'Foo'), 'Bar' => new ChoiceView($entity2, 'Bar', 'Bar')), $field->createView()->vars['choices']); + $this->assertTrue($field->isSynchronized(), 'Field should be synchronized.'); + $this->assertSame($entity2, $field->getData(), 'Entity should be loaded by custom value.'); + $this->assertSame('Bar', $field->getViewData()); + } + + public function testOverrideChoicesValuesWithCallable() + { + $entity1 = new GroupableEntity(1, 'Foo', 'BazGroup'); + $entity2 = new GroupableEntity(2, 'Bar', 'BooGroup'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::ITEM_GROUP_CLASS, + 'choice_label' => 'name', + 'choice_value' => function (GroupableEntity $entity = null) { + if (null === $entity) { + return ''; + } + + return $entity->groupName.'/'.$entity->name; + }, + )); + + $field->submit('BooGroup/Bar'); + + $this->assertEquals(array( + 'BazGroup/Foo' => new ChoiceView($entity1, 'BazGroup/Foo', 'Foo'), + 'BooGroup/Bar' => new ChoiceView($entity2, 'BooGroup/Bar', 'Bar'), + ), $field->createView()->vars['choices']); + $this->assertTrue($field->isSynchronized(), 'Field should be synchronized.'); + $this->assertSame($entity2, $field->getData(), 'Entity should be loaded by custom value.'); + $this->assertSame('BooGroup/Bar', $field->getViewData()); + } + + public function testChoicesForValuesOptimization() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + + $this->em->clear(); + + $field->submit(1); + + $unitOfWorkIdentityMap = $this->em->getUnitOfWork()->getIdentityMap(); + $managedEntitiesNames = array_map('strval', $unitOfWorkIdentityMap['Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity']); + + $this->assertContains((string) $entity1, $managedEntitiesNames); + $this->assertNotContains((string) $entity2, $managedEntitiesNames); + } + public function testGroupByChoices() { $item1 = new GroupableEntity(1, 'Foo', 'Group1'); @@ -514,7 +830,7 @@ public function testGroupByChoices() 'em' => 'default', 'class' => self::ITEM_GROUP_CLASS, 'choices' => array($item1, $item2, $item3, $item4), - 'property' => 'name', + 'choice_label' => 'name', 'group_by' => 'groupName', )); @@ -522,9 +838,14 @@ public function testGroupByChoices() $this->assertSame('2', $field->getViewData()); $this->assertEquals(array( - 'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')), - 'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')), - '4' => new ChoiceView($item4, '4', 'Boo!'), + 'Group1' => new ChoiceGroupView('Group1', array( + 1 => new ChoiceView($item1, '1', 'Foo'), + 2 => new ChoiceView($item2, '2', 'Bar'), + )), + 'Group2' => new ChoiceGroupView('Group2', array( + 3 => new ChoiceView($item3, '3', 'Baz'), + )), + 4 => new ChoiceView($item4, '4', 'Boo!'), ), $field->createView()->vars['choices']); } @@ -540,7 +861,7 @@ public function testPreferredChoices() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'preferred_choices' => array($entity3, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['preferred_choices']); @@ -560,7 +881,7 @@ public function testOverrideChoicesWithPreferredChoices() 'class' => self::SINGLE_IDENT_CLASS, 'choices' => array($entity2, $entity3), 'preferred_choices' => array($entity3), - 'property' => 'name', + 'choice_label' => 'name', )); $this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz')), $field->createView()->vars['preferred_choices']); @@ -579,7 +900,30 @@ public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier() 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', + )); + + $field->submit('3'); + + $this->assertFalse($field->isSynchronized()); + $this->assertNull($field->getData()); + } + + public function testDisallowChoicesThatAreNotIncludedChoicesSingleAssocIdentifier() + { + $innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo'); + $innerEntity2 = new SingleIntIdNoToStringEntity(2, 'InBar'); + + $entity1 = new SingleAssociationToIntIdEntity($innerEntity1, 'Foo'); + $entity2 = new SingleAssociationToIntIdEntity($innerEntity2, 'Bar'); + + $this->persist(array($innerEntity1, $innerEntity2, $entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_ASSOC_IDENT_CLASS, + 'choices' => array($entity1, $entity2), + 'choice_label' => 'name', )); $field->submit('3'); @@ -600,7 +944,7 @@ public function testDisallowChoicesThatAreNotIncludedChoicesCompositeIdentifier( 'em' => 'default', 'class' => self::COMPOSITE_IDENT_CLASS, 'choices' => array($entity1, $entity2), - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -624,7 +968,35 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleIdentifie 'class' => self::SINGLE_IDENT_CLASS, 'query_builder' => $repository->createQueryBuilder('e') ->where('e.id IN (1, 2)'), - 'property' => 'name', + 'choice_label' => 'name', + )); + + $field->submit('3'); + + $this->assertFalse($field->isSynchronized()); + $this->assertNull($field->getData()); + } + + public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleAssocIdentifier() + { + $innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo'); + $innerEntity2 = new SingleIntIdNoToStringEntity(2, 'InBar'); + $innerEntity3 = new SingleIntIdNoToStringEntity(3, 'InBaz'); + + $entity1 = new SingleAssociationToIntIdEntity($innerEntity1, 'Foo'); + $entity2 = new SingleAssociationToIntIdEntity($innerEntity2, 'Bar'); + $entity3 = new SingleAssociationToIntIdEntity($innerEntity3, 'Baz'); + + $this->persist(array($innerEntity1, $innerEntity2, $innerEntity3, $entity1, $entity2, $entity3)); + + $repository = $this->em->getRepository(self::SINGLE_ASSOC_IDENT_CLASS); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_ASSOC_IDENT_CLASS, + 'query_builder' => $repository->createQueryBuilder('e') + ->where('e.entity IN (1, 2)'), + 'choice_label' => 'name', )); $field->submit('3'); @@ -646,9 +1018,9 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderAsClosureSingle 'class' => self::SINGLE_IDENT_CLASS, 'query_builder' => function ($repository) { return $repository->createQueryBuilder('e') - ->where('e.id IN (1, 2)'); + ->where('e.id IN (1, 2)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('3'); @@ -670,9 +1042,9 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderAsClosureCompos 'class' => self::COMPOSITE_IDENT_CLASS, 'query_builder' => function ($repository) { return $repository->createQueryBuilder('e') - ->where('e.id1 IN (10, 50)'); + ->where('e.id1 IN (10, 50)'); }, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('2'); @@ -692,7 +1064,7 @@ public function testSubmitSingleStringIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::SINGLE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); $field->submit('foo'); @@ -713,7 +1085,7 @@ public function testSubmitCompositeStringIdentifier() 'expanded' => false, 'em' => 'default', 'class' => self::COMPOSITE_STRING_IDENT_CLASS, - 'property' => 'name', + 'choice_label' => 'name', )); // the collection key is used here @@ -735,19 +1107,204 @@ public function testGetManagerForClassIfNoEm() ->will($this->returnValue($this->em)); $this->factory->createNamed('name', 'entity', null, array( + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'choice_label' => 'name', + )); + } + + public function testExplicitEm() + { + $this->emRegistry->expects($this->never()) + ->method('getManager'); + + $this->emRegistry->expects($this->never()) + ->method('getManagerForClass'); + + $this->factory->createNamed('name', 'entity', null, array( + 'em' => $this->em, + 'class' => self::SINGLE_IDENT_CLASS, + 'choice_label' => 'name', + )); + } + + public function testLoaderCaching() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + $entity3 = new SingleIntIdEntity(3, 'Baz'); + + $this->persist(array($entity1, $entity2, $entity3)); + + $repo = $this->em->getRepository(self::SINGLE_IDENT_CLASS); + + $entityType = new EntityType( + $this->emRegistry, + PropertyAccess::createPropertyAccessor() + ); + + $entityTypeGuesser = new DoctrineOrmTypeGuesser($this->emRegistry); + + $factory = Forms::createFormFactoryBuilder() + ->addType($entityType) + ->addTypeGuesser($entityTypeGuesser) + ->getFormFactory(); + + $formBuilder = $factory->createNamedBuilder('form', 'form'); + + $formBuilder->add('property1', 'entity', array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'), + )); + + $formBuilder->add('property2', 'entity', array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'); + }, + )); + + $formBuilder->add('property3', 'entity', array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'); + }, + )); + + $form = $formBuilder->getForm(); + + $form->submit(array( + 'property1' => 1, + 'property2' => 1, + 'property3' => 2, + )); + + $choiceList1 = $form->get('property1')->getConfig()->getOption('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getOption('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getOption('choice_list'); + + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); + } + + public function testLoaderCachingWithParameters() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + $entity3 = new SingleIntIdEntity(3, 'Baz'); + + $this->persist(array($entity1, $entity2, $entity3)); + + $repo = $this->em->getRepository(self::SINGLE_IDENT_CLASS); + + $entityType = new EntityType( + $this->emRegistry, + PropertyAccess::createPropertyAccessor() + ); + + $entityTypeGuesser = new DoctrineOrmTypeGuesser($this->emRegistry); + + $factory = Forms::createFormFactoryBuilder() + ->addType($entityType) + ->addTypeGuesser($entityTypeGuesser) + ->getFormFactory(); + + $formBuilder = $factory->createNamedBuilder('form', 'form'); + + $formBuilder->add('property1', 'entity', array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => $repo->createQueryBuilder('e')->where('e.id = :id')->setParameter('id', 1), + )); + + $formBuilder->add('property2', 'entity', array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id = :id')->setParameter('id', 1); + }, + )); + + $formBuilder->add('property3', 'entity', array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('e')->where('e.id = :id')->setParameter('id', 1); + }, + )); + + $form = $formBuilder->getForm(); + + $form->submit(array( + 'property1' => 1, + 'property2' => 1, + 'property3' => 2, + )); + + $choiceList1 = $form->get('property1')->getConfig()->getOption('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getOption('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getOption('choice_list'); + + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); + } + + public function testCacheChoiceLists() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + + $this->persist(array($entity1)); + + $field1 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'choice_label' => 'name', + )); + + $field2 = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'required' => false, + 'choice_label' => 'name', + )); + + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $field1->getConfig()->getOption('choice_list')); + $this->assertSame($field1->getConfig()->getOption('choice_list'), $field2->getConfig()->getOption('choice_list')); + } + + /** + * @group legacy + */ + public function testPropertyOption() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + + $this->persist(array($entity1, $entity2)); + + $field = $this->factory->createNamed('name', 'entity', null, array( + 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'required' => false, 'property' => 'name', )); + + $this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']); } protected function createRegistryMock($name, $em) { $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); $registry->expects($this->any()) - ->method('getManager') - ->with($this->equalTo($name)) - ->will($this->returnValue($em)); + ->method('getManager') + ->with($this->equalTo($name)) + ->will($this->returnValue($em)); return $registry; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php b/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php index 92fec99b5b47..37a72b07b6b0 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php @@ -106,7 +106,7 @@ public function testLogNonUtf8Array() public function testLogLongString() { - $logger = $this->getMock('Symfony\\Component\\HttpKernel\\Log\\LoggerInterface'); + $logger = $this->getMock('Psr\\Log\\LoggerInterface'); $dbalLogger = $this ->getMockBuilder('Symfony\\Bridge\\Doctrine\\Logger\\DbalLogger') @@ -137,7 +137,7 @@ public function testLogLongString() */ public function testLogUTF8LongString() { - $logger = $this->getMock('Symfony\\Component\\HttpKernel\\Log\\LoggerInterface'); + $logger = $this->getMock('Psr\\Log\\LoggerInterface'); $dbalLogger = $this ->getMockBuilder('Symfony\\Bridge\\Doctrine\\Logger\\DbalLogger') diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/LegacyUniqueEntityValidatorLegacyApiTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/LegacyUniqueEntityValidatorLegacyApiTest.php new file mode 100644 index 000000000000..cde865cc1d87 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/LegacyUniqueEntityValidatorLegacyApiTest.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\Bridge\Doctrine\Tests\Validator\Constraints; + +use Symfony\Component\Validator\Validation; + +/** + * @since 2.5.4 + * + * @author Bernhard Schussek + * @group legacy + */ +class LegacyUniqueEntityValidatorLegacyApiTest extends UniqueEntityValidatorTest +{ + protected function getApiVersion() + { + return Validation::API_VERSION_2_5_BC; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 7c061d7ac286..33e484320a56 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -16,12 +16,13 @@ use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\Persistence\ObjectRepository; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Component\Validator\Tests\Constraints\AbstractConstraintValidatorTest; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; +use Symfony\Component\Validator\Tests\Constraints\AbstractConstraintValidatorTest; +use Symfony\Component\Validator\Validation; use Doctrine\ORM\Tools\SchemaTool; /** @@ -46,6 +47,11 @@ class UniqueEntityValidatorTest extends AbstractConstraintValidatorTest */ protected $repository; + protected function getApiVersion() + { + return Validation::API_VERSION_2_5; + } + protected function setUp() { $this->em = DoctrineTestHelper::createTestEntityManager(); diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 5ae077dc9f20..f4c8671abae9 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; use Doctrine\Common\Persistence\ManagerRegistry; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -43,6 +44,10 @@ public function __construct(ManagerRegistry $registry) */ public function validate($entity, Constraint $constraint) { + if (!$constraint instanceof UniqueEntity) { + throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\UniqueEntity'); + } + if (!is_array($constraint->fields) && !is_string($constraint->fields)) { throw new UnexpectedTypeException($constraint->fields, 'array'); } @@ -123,6 +128,16 @@ public function validate($entity, Constraint $constraint) $errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0]; $invalidValue = isset($criteria[$errorPath]) ? $criteria[$errorPath] : $criteria[$fields[0]]; - $this->context->addViolationAt($errorPath, $constraint->message, array(), $invalidValue); + if ($this->context instanceof ExecutionContextInterface) { + $this->context->buildViolation($constraint->message) + ->atPath($errorPath) + ->setInvalidValue($invalidValue) + ->addViolation(); + } else { + $this->buildViolation($constraint->message) + ->atPath($errorPath) + ->setInvalidValue($invalidValue) + ->addViolation(); + } } } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 03e1376503c0..4c106a2568f5 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -16,17 +16,18 @@ } ], "require": { - "php": ">=5.3.3", + "php": ">=5.3.9", "doctrine/common": "~2.4" }, "require-dev": { "symfony/stopwatch": "~2.2", - "symfony/dependency-injection": "~2.0,>=2.0.5", - "symfony/form": "~2.3,>=2.3.8", + "symfony/dependency-injection": "~2.2", + "symfony/form": "~2.7.12|~2.8.5", "symfony/http-kernel": "~2.2", "symfony/property-access": "~2.3", "symfony/security": "~2.2", - "symfony/validator": "~2.3.0,>=2.3.20", + "symfony/expression-language": "~2.2", + "symfony/validator": "~2.5,>=2.5.5", "symfony/translation": "~2.0,>=2.0.5", "doctrine/data-fixtures": "1.0.*", "doctrine/dbal": "~2.4", @@ -40,16 +41,15 @@ "doctrine/orm": "" }, "autoload": { - "psr-0": { "Symfony\\Bridge\\Doctrine\\": "" }, + "psr-4": { "Symfony\\Bridge\\Doctrine\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bridge/Doctrine", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 88ecedd5f6b5..73239fa86b64 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 000000000000..1af93bc97aeb --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.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\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 __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = true) + { + parent::__construct($format, $dateFormat, $allowInlineLineBreaks, $ignoreEmptyContextAndExtra); + } + + /** + * {@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 43f54b536ae4..2636bc3b8980 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 000000000000..592584ffa4af --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php @@ -0,0 +1,187 @@ + + * + * 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 bool $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) + { + $output = $event->getOutput(); + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $this->setOutput($output); + } + + /** + * 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 => array('onCommand', 255), + ConsoleEvents::TERMINATE => array('onTerminate', -255), + ); + } + + /** + * {@inheritdoc} + */ + protected function write(array $record) + { + $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 bool 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/FingersCrossed/NotFoundActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php new file mode 100644 index 000000000000..413b476f2938 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.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\Bridge\Monolog\Handler\FingersCrossed; + +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * Activation strategy that ignores 404s for certain URLs. + * + * @author Jordi Boggiano + * @author Fabien Potencier + */ +class NotFoundActivationStrategy extends ErrorLevelActivationStrategy +{ + private $blacklist; + private $requestStack; + + public function __construct(RequestStack $requestStack, array $excludedUrls, $actionLevel) + { + parent::__construct($actionLevel); + + $this->requestStack = $requestStack; + $this->blacklist = '{('.implode('|', $excludedUrls).')}i'; + } + + public function isHandlerActivated(array $record) + { + $isActivated = parent::isHandlerActivated($record); + + if ( + $isActivated + && isset($record['context']['exception']) + && $record['context']['exception'] instanceof HttpException + && $record['context']['exception']->getStatusCode() == 404 + && ($request = $this->requestStack->getMasterRequest()) + ) { + return !preg_match($this->blacklist, $request->getPathInfo()); + } + + return $isActivated; + } +} diff --git a/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php b/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php index 0131d19b0f81..339843c1d4ff 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/Handler/SwiftMailerHandler.php b/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php index 85671e47a963..0412e94f223a 100644 --- a/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Monolog\Handler; use Monolog\Handler\SwiftMailerHandler as BaseSwiftMailerHandler; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\HttpKernel\Event\PostResponseEvent; /** @@ -43,6 +44,16 @@ public function onKernelTerminate(PostResponseEvent $event) $this->instantFlush = true; } + /** + * After the CLI application has been terminated we will always flush messages. + * + * @param ConsoleTerminateEvent $event + */ + public function onCliTerminate(ConsoleTerminateEvent $event) + { + $this->instantFlush = true; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bridge/Monolog/Logger.php b/src/Symfony/Bridge/Monolog/Logger.php index cf8b537bdf42..fbc2f5ff3329 100644 --- a/src/Symfony/Bridge/Monolog/Logger.php +++ b/src/Symfony/Bridge/Monolog/Logger.php @@ -23,34 +23,42 @@ class Logger extends BaseLogger implements LoggerInterface, DebugLoggerInterface { /** - * @deprecated since 2.2, to be removed in 3.0. Use emergency() which is PSR-3 compatible. + * @deprecated since version 2.2, to be removed in 3.0. Use emergency() which is PSR-3 compatible. */ public function emerg($message, array $context = array()) { + @trigger_error('The '.__METHOD__.' method inherited from the Symfony\Component\HttpKernel\Log\LoggerInterface interface is deprecated since version 2.2 and will be removed in 3.0. Use the emergency() method instead, which is PSR-3 compatible.', E_USER_DEPRECATED); + return parent::addRecord(BaseLogger::EMERGENCY, $message, $context); } /** - * @deprecated since 2.2, to be removed in 3.0. Use critical() which is PSR-3 compatible. + * @deprecated since version 2.2, to be removed in 3.0. Use critical() which is PSR-3 compatible. */ public function crit($message, array $context = array()) { + @trigger_error('The '.__METHOD__.' method inherited from the Symfony\Component\HttpKernel\Log\LoggerInterface interface is deprecated since version 2.2 and will be removed in 3.0. Use the method critical() method instead, which is PSR-3 compatible.', E_USER_DEPRECATED); + return parent::addRecord(BaseLogger::CRITICAL, $message, $context); } /** - * @deprecated since 2.2, to be removed in 3.0. Use error() which is PSR-3 compatible. + * @deprecated since version 2.2, to be removed in 3.0. Use error() which is PSR-3 compatible. */ public function err($message, array $context = array()) { + @trigger_error('The '.__METHOD__.' method inherited from the Symfony\Component\HttpKernel\Log\LoggerInterface interface is deprecated since version 2.2 and will be removed in 3.0. Use the error() method instead, which is PSR-3 compatible.', E_USER_DEPRECATED); + return parent::addRecord(BaseLogger::ERROR, $message, $context); } /** - * @deprecated since 2.2, to be removed in 3.0. Use warning() which is PSR-3 compatible. + * @deprecated since version 2.2, to be removed in 3.0. Use warning() which is PSR-3 compatible. */ public function warn($message, array $context = array()) { + @trigger_error('The '.__METHOD__.' method inherited from the Symfony\Component\HttpKernel\Log\LoggerInterface interface is deprecated since version 2.2 and will be removed in 3.0. Use the warning() method instead, which is PSR-3 compatible.', E_USER_DEPRECATED); + return parent::addRecord(BaseLogger::WARNING, $message, $context); } diff --git a/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php b/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php index c5032dbcd9ab..5222258e4693 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(array $extraFields = null) public function onKernelRequest(GetResponseEvent $event) { - if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) { + if ($event->isMasterRequest()) { $this->serverData = $event->getRequest()->server->all(); $this->serverData['REMOTE_ADDR'] = $event->getRequest()->getClientIp(); } 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 000000000000..6cb315967e4f --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php @@ -0,0 +1,178 @@ + + * + * 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\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Console\Command\Command; + +/** + * 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\OutputInterface'); + $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") + ; + + $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.'); + } + + public function testLogsFromListeners() + { + $output = new BufferedOutput(); + $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + + $handler = new ConsoleHandler(null, false); + + $logger = new Logger('app'); + $logger->pushHandler($handler); + + $dispatcher = new EventDispatcher(); + $dispatcher->addListener(ConsoleEvents::COMMAND, function () use ($logger) { + $logger->addInfo('Before command message.'); + }); + $dispatcher->addListener(ConsoleEvents::TERMINATE, function () use ($logger) { + $logger->addInfo('Before terminate message.'); + }); + + $dispatcher->addSubscriber($handler); + + $dispatcher->addListener(ConsoleEvents::COMMAND, function () use ($logger) { + $logger->addInfo('After command message.'); + }); + $dispatcher->addListener(ConsoleEvents::TERMINATE, function () use ($logger) { + $logger->addInfo('After terminate message.'); + }); + + $event = new ConsoleCommandEvent(new Command('foo'), $this->getMock('Symfony\Component\Console\Input\InputInterface'), $output); + $dispatcher->dispatch(ConsoleEvents::COMMAND, $event); + $this->assertContains('Before command message.', $out = $output->fetch()); + $this->assertContains('After command message.', $out); + + $event = new ConsoleTerminateEvent(new Command('foo'), $this->getMock('Symfony\Component\Console\Input\InputInterface'), $output, 0); + $dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); + $this->assertContains('Before terminate message.', $out = $output->fetch()); + $this->assertContains('After terminate message.', $out); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php new file mode 100644 index 000000000000..48bddc99a5ee --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.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\Bridge\Monolog\Tests\Handler\FingersCrossed; + +use Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Monolog\Logger; + +class NotFoundActivationStrategyTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider isActivatedProvider + */ + public function testIsActivated($url, $record, $expected) + { + $requestStack = new RequestStack(); + $requestStack->push(Request::create($url)); + + $strategy = new NotFoundActivationStrategy($requestStack, array('^/foo', 'bar'), Logger::WARNING); + + $this->assertEquals($expected, $strategy->isHandlerActivated($record)); + } + + public function isActivatedProvider() + { + return array( + array('/test', array('level' => Logger::DEBUG), false), + array('/foo', array('level' => Logger::DEBUG, 'context' => $this->getContextException(404)), false), + array('/baz/bar', array('level' => Logger::ERROR, 'context' => $this->getContextException(404)), false), + array('/foo', array('level' => Logger::ERROR, 'context' => $this->getContextException(404)), false), + array('/foo', array('level' => Logger::ERROR, 'context' => $this->getContextException(500)), true), + + array('/test', array('level' => Logger::ERROR), true), + array('/baz', array('level' => Logger::ERROR, 'context' => $this->getContextException(404)), true), + array('/baz', array('level' => Logger::ERROR, 'context' => $this->getContextException(500)), true), + ); + } + + protected function getContextException($code) + { + return array('exception' => new HttpException($code)); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php index 56a40b1a50c5..143f63b5371b 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php @@ -14,7 +14,6 @@ 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 { @@ -92,8 +91,8 @@ private function createRequestEvent($additionalServerParameters = array()) ->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 c38fe461fdcf..5bf272afe7c6 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -16,21 +16,29 @@ } ], "require": { - "php": ">=5.3.3", - "symfony/http-kernel": "~2.2", - "monolog/monolog": "~1.3" + "php": ">=5.3.9", + "monolog/monolog": "~1.11", + "symfony/http-kernel": "~2.4" }, + "require-dev": { + "symfony/console": "~2.4", + "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\\": "" }, + "psr-4": { "Symfony\\Bridge\\Monolog\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bridge/Monolog", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bridge/Propel1/.gitignore b/src/Symfony/Bridge/PhpUnit/.gitignore similarity index 100% rename from src/Symfony/Bridge/Propel1/.gitignore rename to src/Symfony/Bridge/PhpUnit/.gitignore diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php new file mode 100644 index 000000000000..21fbc30ee68d --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +/** + * Catch deprecation notices and print a summary report at the end of the test suite. + * + * @author Nicolas Grekas + */ +class DeprecationErrorHandler +{ + private static $isRegistered = false; + + public static function register($mode = false) + { + if (self::$isRegistered) { + return; + } + + $getMode = function () use ($mode) { + static $memoizedMode = false; + + if (false !== $memoizedMode) { + return $memoizedMode; + } + if (false === $mode) { + $mode = getenv('SYMFONY_DEPRECATIONS_HELPER') ?: ''; + } + + return $memoizedMode = $mode; + }; + + $deprecations = array( + 'unsilencedCount' => 0, + 'remainingCount' => 0, + 'legacyCount' => 0, + 'otherCount' => 0, + 'unsilenced' => array(), + 'remaining' => array(), + 'legacy' => array(), + 'other' => array(), + ); + $deprecationHandler = function ($type, $msg, $file, $line, $context) use (&$deprecations, $getMode) { + if (E_USER_DEPRECATED !== $type) { + return \PHPUnit_Util_ErrorHandler::handleError($type, $msg, $file, $line, $context); + } + + $mode = $getMode(); + $trace = debug_backtrace(true); + $group = 'other'; + + $i = count($trace); + while (1 < $i && (!isset($trace[--$i]['class']) || ('ReflectionMethod' === $trace[$i]['class'] || 0 === strpos($trace[$i]['class'], 'PHPUnit_')))) { + // No-op + } + + if (isset($trace[$i]['object']) || isset($trace[$i]['class'])) { + $class = isset($trace[$i]['object']) ? get_class($trace[$i]['object']) : $trace[$i]['class']; + $method = $trace[$i]['function']; + + if (0 !== error_reporting()) { + $group = 'unsilenced'; + } elseif (0 === strpos($method, 'testLegacy') + || 0 === strpos($method, 'provideLegacy') + || 0 === strpos($method, 'getLegacy') + || strpos($class, '\Legacy') + || in_array('legacy', \PHPUnit_Util_Test::getGroups($class, $method), true) + ) { + $group = 'legacy'; + } else { + $group = 'remaining'; + } + + if ('legacy' !== $group && 'weak' !== $mode) { + $ref = &$deprecations[$group][$msg]['count']; + ++$ref; + $ref = &$deprecations[$group][$msg][$class.'::'.$method]; + ++$ref; + } + } elseif ('weak' !== $mode) { + $ref = &$deprecations[$group][$msg]['count']; + ++$ref; + } + ++$deprecations[$group.'Count']; + }; + $oldErrorHandler = set_error_handler($deprecationHandler); + + if (null !== $oldErrorHandler) { + restore_error_handler(); + if (array('PHPUnit_Util_ErrorHandler', 'handleError') === $oldErrorHandler) { + restore_error_handler(); + self::register($mode); + } + } else { + self::$isRegistered = true; + if (self::hasColorSupport()) { + $colorize = function ($str, $red) { + $color = $red ? '41;37' : '43;30'; + + return "\x1B[{$color}m{$str}\x1B[0m"; + }; + } else { + $colorize = function ($str) {return $str;}; + } + register_shutdown_function(function () use ($getMode, &$deprecations, $deprecationHandler, $colorize) { + $mode = $getMode(); + $currErrorHandler = set_error_handler('var_dump'); + restore_error_handler(); + + if ('weak' === $mode) { + $colorize = function ($str) {return $str;}; + } + if ($currErrorHandler !== $deprecationHandler) { + echo "\n", $colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n"; + } + + $cmp = function ($a, $b) { + return $b['count'] - $a['count']; + }; + + foreach (array('unsilenced', 'remaining', 'legacy', 'other') as $group) { + if ($deprecations[$group.'Count']) { + echo "\n", $colorize(sprintf('%s deprecation notices (%d)', ucfirst($group), $deprecations[$group.'Count']), 'legacy' !== $group), "\n"; + + uasort($deprecations[$group], $cmp); + + foreach ($deprecations[$group] as $msg => $notices) { + echo "\n", rtrim($msg, '.'), ': ', $notices['count'], "x\n"; + + arsort($notices); + + foreach ($notices as $method => $count) { + if ('count' !== $method) { + echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n"; + } + } + } + } + } + if (!empty($notices)) { + echo "\n"; + } + + if ('weak' !== $mode && ($deprecations['unsilenced'] || $deprecations['remaining'] || $deprecations['other'])) { + exit(1); + } + }); + } + } + + private static function hasColorSupport() + { + if ('\\' === DIRECTORY_SEPARATOR) { + return + 0 >= version_compare('10.0.10586', PHP_WINDOWS_VERSION_MAJOR.'.'.PHP_WINDOWS_VERSION_MINOR.'.'.PHP_WINDOWS_VERSION_BUILD) + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return defined('STDOUT') && function_exists('posix_isatty') && @posix_isatty(STDOUT); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/LICENSE b/src/Symfony/Bridge/PhpUnit/LICENSE new file mode 100644 index 000000000000..39fa189d2b5f --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Bridge/PhpUnit/README.md b/src/Symfony/Bridge/PhpUnit/README.md new file mode 100644 index 000000000000..8c4e6e59ccb4 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/README.md @@ -0,0 +1,13 @@ +PHPUnit Bridge +============== + +Provides utilities for PHPUnit, especially user deprecation notices management. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/phpunit_bridge.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/default.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/default.phpt new file mode 100644 index 000000000000..fac5c53ae748 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/default.phpt @@ -0,0 +1,69 @@ +--TEST-- +Test DeprecationErrorHandler in default mode +--FILE-- +testLegacyFoo(); +$foo->testNonLegacyBar(); + +?> +--EXPECTF-- +Unsilenced deprecation notices (3) + +unsilenced foo deprecation: 2x + 2x in FooTestCase::testLegacyFoo + +unsilenced bar deprecation: 1x + 1x in FooTestCase::testNonLegacyBar + +Remaining deprecation notices (1) + +silenced bar deprecation: 1x + 1x in FooTestCase::testNonLegacyBar + +Legacy deprecation notices (1) + +Other deprecation notices (1) + +root deprecation: 1x + diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak.phpt new file mode 100644 index 000000000000..9e78d96e70ef --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak.phpt @@ -0,0 +1,40 @@ +--TEST-- +Test DeprecationErrorHandler in weak mode +--FILE-- +testLegacyFoo(); + +?> +--EXPECTF-- +Unsilenced deprecation notices (1) + +Legacy deprecation notices (1) + +Other deprecation notices (1) + diff --git a/src/Symfony/Bridge/PhpUnit/TextUI/Command.php b/src/Symfony/Bridge/PhpUnit/TextUI/Command.php new file mode 100644 index 000000000000..5243044c6b92 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/TextUI/Command.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\Bridge\PhpUnit\TextUI; + +/** + * {@inheritdoc} + */ +class Command extends \PHPUnit_TextUI_Command +{ + /** + * {@inheritdoc} + */ + protected function handleBootstrap($filename) + { + parent::handleBootstrap($filename); + + // By default, we want PHPUnit's autoloader before Symfony's one + if (!getenv('SYMFONY_PHPUNIT_OVERLOAD')) { + $filename = realpath(stream_resolve_include_path($filename)); + $symfonyLoader = realpath(dirname(PHPUNIT_COMPOSER_INSTALL).'/../../../vendor/autoload.php'); + + if ($filename === $symfonyLoader) { + $symfonyLoader = require $symfonyLoader; + $symfonyLoader->unregister(); + $symfonyLoader->register(false); + } + } + } +} diff --git a/src/Symfony/Bridge/PhpUnit/bootstrap.php b/src/Symfony/Bridge/PhpUnit/bootstrap.php new file mode 100644 index 000000000000..bb98a9296162 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/bootstrap.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Doctrine\Common\Annotations\AnnotationRegistry; +use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; + +// Detect if we're loaded by an actual run of phpunit +if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists('PHPUnit_TextUI_Command', false)) { + return; +} + +// Enforce a consistent locale +setlocale(LC_ALL, 'C'); + +if (class_exists('Doctrine\Common\Annotations\AnnotationRegistry')) { + AnnotationRegistry::registerLoader('class_exists'); +} + +DeprecationErrorHandler::register(getenv('SYMFONY_DEPRECATIONS_HELPER')); diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json new file mode 100644 index 000000000000..39eb9ef2180f --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/phpunit-bridge", + "type": "symfony-bridge", + "description": "Symfony PHPUnit Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3 EVEN ON LATEST SYMFONY VERSIONS TO ALLOW USING", + "php": "THIS BRIDGE WHEN TESTING LOWEST SYMFONY VERSIONS.", + "php": ">=5.3.3" + }, + "suggest": { + "symfony/debug": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + }, + "autoload": { + "files": [ "bootstrap.php" ], + "psr-4": { "Symfony\\Bridge\\PhpUnit\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + } +} diff --git a/src/Symfony/Bridge/PhpUnit/phpunit.xml.dist b/src/Symfony/Bridge/PhpUnit/phpunit.xml.dist new file mode 100644 index 000000000000..9b64b02947c0 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + + ./Tests/ + ./Tests/DeprecationErrorHandler/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Bridge/Propel1/CHANGELOG.md b/src/Symfony/Bridge/Propel1/CHANGELOG.md deleted file mode 100644 index 242d576c33b7..000000000000 --- a/src/Symfony/Bridge/Propel1/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -CHANGELOG -========= - -2.2.0 ------ - - * added a collection type for the I18n behavior - * added an optional PropertyAccessorInterface parameter to ModelType and - ModelChoiceList - * [BC BREAK] ModelType now has a constructor - -2.1.0 ------ - - * added the bridge diff --git a/src/Symfony/Bridge/Propel1/DataCollector/PropelDataCollector.php b/src/Symfony/Bridge/Propel1/DataCollector/PropelDataCollector.php deleted file mode 100644 index bc91564a5717..000000000000 --- a/src/Symfony/Bridge/Propel1/DataCollector/PropelDataCollector.php +++ /dev/null @@ -1,147 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\DataCollector; - -use Symfony\Bridge\Propel1\Logger\PropelLogger; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\DataCollector\DataCollector; - -/** - * The PropelDataCollector collector class collects information. - * - * @author William Durand - */ -class PropelDataCollector extends DataCollector -{ - /** - * Propel logger. - * - * @var PropelLogger - */ - private $logger; - - /** - * Propel configuration. - * - * @var \PropelConfiguration - */ - protected $propelConfiguration; - - /** - * Constructor. - * - * @param PropelLogger $logger A Propel logger. - * @param \PropelConfiguration $propelConfiguration The Propel configuration object. - */ - public function __construct(PropelLogger $logger, \PropelConfiguration $propelConfiguration) - { - $this->logger = $logger; - $this->propelConfiguration = $propelConfiguration; - } - - /** - * {@inheritdoc} - */ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - $this->data = array( - 'queries' => $this->buildQueries(), - 'querycount' => $this->countQueries(), - ); - } - - /** - * Returns the collector name. - * - * @return string - */ - public function getName() - { - return 'propel'; - } - - /** - * Returns the collected stats for all executed SQL queries. - * - * @return array - */ - public function getQueries() - { - return $this->data['queries']; - } - - /** - * Returns the query count. - * - * @return int - */ - public function getQueryCount() - { - return $this->data['querycount']; - } - - /** - * Returns the total time spent on running all queries. - * - * @return float - */ - public function getTime() - { - $time = 0; - foreach ($this->data['queries'] as $query) { - $time += (float) $query['time']; - } - - return $time; - } - - /** - * Computes the stats of all executed SQL queries. - * - * @return array - */ - private function buildQueries() - { - $queries = array(); - - $outerGlue = $this->propelConfiguration->getParameter('debugpdo.logging.outerglue', ' | '); - $innerGlue = $this->propelConfiguration->getParameter('debugpdo.logging.innerglue', ': '); - - foreach ($this->logger->getQueries() as $q) { - $parts = explode($outerGlue, $q, 4); - - $times = explode($innerGlue, $parts[0]); - $con = explode($innerGlue, $parts[2]); - $memories = explode($innerGlue, $parts[1]); - - $sql = trim($parts[3]); - $con = trim($con[1]); - $time = trim($times[1]); - $memory = trim($memories[1]); - - $queries[] = array('connection' => $con, 'sql' => $sql, 'time' => $time, 'memory' => $memory); - } - - return $queries; - } - - /** - * Returns the total count of SQL queries. - * - * @return int - */ - private function countQueries() - { - return count($this->logger->getQueries()); - } -} diff --git a/src/Symfony/Bridge/Propel1/DependencyInjection/Security/UserProvider/PropelFactory.php b/src/Symfony/Bridge/Propel1/DependencyInjection/Security/UserProvider/PropelFactory.php deleted file mode 100644 index eb8e2e4a7295..000000000000 --- a/src/Symfony/Bridge/Propel1/DependencyInjection/Security/UserProvider/PropelFactory.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\DependencyInjection\Security\UserProvider; - -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; -use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\DefinitionDecorator; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -/** - * PropelFactory creates services for Propel user provider. - * - * @author William Durand - */ -class PropelFactory implements UserProviderFactoryInterface -{ - private $key; - private $providerId; - - /** - * Constructor. - * - * @param string $key - * @param string $providerId - */ - public function __construct($key, $providerId) - { - $this->key = $key; - $this->providerId = $providerId; - } - - public function create(ContainerBuilder $container, $id, $config) - { - $container - ->setDefinition($id, new DefinitionDecorator($this->providerId)) - ->addArgument($config['class']) - ->addArgument($config['property']) - ; - } - - public function getKey() - { - return $this->key; - } - - public function addConfiguration(NodeDefinition $node) - { - $node - ->children() - ->scalarNode('class') - ->isRequired() - ->cannotBeEmpty() - ->end() - ->scalarNode('property') - ->defaultNull() - ->end() - ->end() - ; - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php b/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php deleted file mode 100644 index cc4d76cf6482..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php +++ /dev/null @@ -1,490 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form\ChoiceList; - -use Symfony\Bridge\Propel1\Form\Type\ModelType; -use Symfony\Component\Form\Exception\StringCastException; -use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; -use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; -use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; -use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; -use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * A choice list for object choices based on Propel model. - * - * @author William Durand - * @author Toni Uebernickel - */ -class ModelChoiceList extends ObjectChoiceList -{ - /** - * The fields of which the identifier of the underlying class consists. - * - * This property should only be accessed through identifier. - * - * @var array - */ - protected $identifier = array(); - - /** - * The query to retrieve the choices of this list. - * - * @var \ModelCriteria - */ - protected $query; - - /** - * The query to retrieve the preferred choices for this list. - * - * @var \ModelCriteria - */ - protected $preferredQuery; - - /** - * Whether the model objects have already been loaded. - * - * @var bool - */ - protected $loaded = false; - - /** - * Whether to use the identifier for index generation. - * - * @var bool - */ - private $identifierAsIndex = false; - - /** - * Constructor. - * - * @see ModelType How to use the preferred choices. - * - * @param string $class The FQCN of the model class to be loaded. - * @param string $labelPath A property path pointing to the property used for the choice labels. - * @param array $choices An optional array to use, rather than fetching the models. - * @param \ModelCriteria $queryObject The query to use retrieving model data from database. - * @param string $groupPath A property path pointing to the property used to group the choices. - * @param array|\ModelCriteria $preferred The preferred items of this choice. - * Either an array if $choices is given, - * or a \ModelCriteria to be merged with the $queryObject. - * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. - * - * @throws MissingOptionsException when no model class is given - * @throws InvalidOptionsException when the model class cannot be found - */ - public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array(), PropertyAccessorInterface $propertyAccessor = null) - { - $this->class = $class; - - $queryClass = $this->class.'Query'; - if (!class_exists($queryClass)) { - if (empty($this->class)) { - throw new MissingOptionsException('The "class" parameter is empty, you should provide the model class'); - } - throw new InvalidOptionsException(sprintf('The query class "%s" is not found, you should provide the FQCN of the model class', $queryClass)); - } - - $query = new $queryClass(); - - $this->query = $queryObject ?: $query; - $this->identifier = $this->query->getTableMap()->getPrimaryKeys(); - $this->loaded = is_array($choices) || $choices instanceof \Traversable; - - if ($preferred instanceof \ModelCriteria) { - $this->preferredQuery = $preferred->mergeWith($this->query); - } - - if (!$this->loaded) { - // Make sure the constraints of the parent constructor are - // fulfilled - $choices = array(); - $preferred = array(); - } - - if (1 === count($this->identifier) && $this->isScalar(current($this->identifier))) { - $this->identifierAsIndex = true; - } - - parent::__construct($choices, $labelPath, $preferred, $groupPath, null, $propertyAccessor); - } - - /** - * Returns the class name of the model. - * - * @return string - */ - public function getClass() - { - return $this->class; - } - - /** - * {@inheritdoc} - */ - public function getChoices() - { - $this->load(); - - return parent::getChoices(); - } - - /** - * {@inheritdoc} - */ - public function getValues() - { - $this->load(); - - return parent::getValues(); - } - - /** - * {@inheritdoc} - */ - public function getPreferredViews() - { - $this->load(); - - return parent::getPreferredViews(); - } - - /** - * {@inheritdoc} - */ - public function getRemainingViews() - { - $this->load(); - - return parent::getRemainingViews(); - } - - /** - * {@inheritdoc} - */ - public function getChoicesForValues(array $values) - { - if (empty($values)) { - return array(); - } - - /* - * This performance optimization reflects a common scenario: - * * A simple select of a model entry. - * * The choice option "expanded" is set to false. - * * The current request is the submission of the selected value. - * - * @see ChoicesToValuesTransformer::reverseTransform() - * @see ChoiceToValueTransformer::reverseTransform() - */ - if (!$this->loaded) { - if (1 === count($this->identifier)) { - $filterBy = 'filterBy'.current($this->identifier)->getPhpName(); - - // The initial query is cloned, so all additional filters are applied correctly. - $query = clone $this->query; - $result = (array) $query - ->$filterBy($values) - ->find(); - - // Preserve the keys as provided by the values. - $models = array(); - foreach ($values as $index => $value) { - foreach ($result as $eachModel) { - if ($value === $this->createValue($eachModel)) { - // Make sure to convert to the right format - $models[$index] = $this->fixChoice($eachModel); - - // If all values have been assigned, skip further loops. - unset($values[$index]); - if (0 === count($values)) { - break 2; - } - } - } - } - - return $models; - } - } - - $this->load(); - - return parent::getChoicesForValues($values); - } - - /** - * {@inheritdoc} - */ - public function getValuesForChoices(array $models) - { - if (empty($models)) { - return array(); - } - - if (!$this->loaded) { - /* - * This performance optimization assumes the validation of the respective values will be done by other means. - * - * It correlates with the performance optimization in {@link ModelChoiceList::getChoicesForValues()} - * as it won't load the actual entries from the database. - * - * @see ChoicesToValuesTransformer::transform() - * @see ChoiceToValueTransformer::transform() - */ - if (1 === count($this->identifier)) { - $values = array(); - foreach ($models as $index => $model) { - if ($model instanceof $this->class) { - // Make sure to convert to the right format - $values[$index] = $this->fixValue(current($this->getIdentifierValues($model))); - } - } - - return $values; - } - } - - $this->load(); - - $values = array(); - $availableValues = $this->getValues(); - - /* - * Overwriting default implementation. - * - * The two objects may represent the same entry in the database, - * but if they originated from different queries, there are not the same object within the code. - * - * This happens when using m:n relations with either sides model as data_class of the form. - * The choicelist will retrieve the list of available related models with a different query, resulting in different objects. - */ - $choices = $this->fixChoices($models); - foreach ($choices as $i => $givenChoice) { - if (null === $givenChoice) { - continue; - } - - foreach ($this->getChoices() as $j => $choice) { - if ($this->isEqual($choice, $givenChoice)) { - $values[$i] = $availableValues[$j]; - - // If all choices have been assigned, skip further loops. - unset($choices[$i]); - if (0 === count($choices)) { - break 2; - } - } - } - } - - return $values; - } - - /** - * {@inheritdoc} - */ - public function getIndicesForChoices(array $models) - { - if (empty($models)) { - return array(); - } - - $this->load(); - - $indices = array(); - - /* - * Overwriting default implementation. - * - * The two objects may represent the same entry in the database, - * but if they originated from different queries, there are not the same object within the code. - * - * This happens when using m:n relations with either sides model as data_class of the form. - * The choicelist will retrieve the list of available related models with a different query, resulting in different objects. - */ - $choices = $this->fixChoices($models); - foreach ($choices as $i => $givenChoice) { - if (null === $givenChoice) { - continue; - } - - foreach ($this->getChoices() as $j => $choice) { - if ($this->isEqual($choice, $givenChoice)) { - $indices[$i] = $j; - - // If all choices have been assigned, skip further loops. - unset($choices[$i]); - if (0 === count($choices)) { - break 2; - } - } - } - } - - return $indices; - } - - /** - * {@inheritdoc} - */ - public function getIndicesForValues(array $values) - { - if (empty($values)) { - return array(); - } - - $this->load(); - - return parent::getIndicesForValues($values); - } - - /** - * Creates a new unique index for this model. - * - * If the model has a single-field identifier, this identifier is used. - * - * Otherwise a new integer is generated. - * - * @param mixed $model The choice to create an index for - * - * @return int|string A unique index containing only ASCII letters, - * digits and underscores. - */ - protected function createIndex($model) - { - if ($this->identifierAsIndex) { - return current($this->getIdentifierValues($model)); - } - - return parent::createIndex($model); - } - - /** - * Creates a new unique value for this model. - * - * If the model has a single-field identifier, this identifier is used. - * - * Otherwise a new integer is generated. - * - * @param mixed $model The choice to create a value for - * - * @return int|string A unique value without character limitations. - */ - protected function createValue($model) - { - if ($this->identifierAsIndex) { - return (string) current($this->getIdentifierValues($model)); - } - - return parent::createValue($model); - } - - /** - * Loads the complete choice list entries, once. - * - * If data has been loaded the choice list is initialized with the retrieved data. - */ - private function load() - { - if ($this->loaded) { - return; - } - - $models = (array) $this->query->find(); - - $preferred = array(); - if ($this->preferredQuery instanceof \ModelCriteria) { - $preferred = (array) $this->preferredQuery->find(); - } - - try { - // The second parameter $labels is ignored by ObjectChoiceList - parent::initialize($models, array(), $preferred); - - $this->loaded = true; - } catch (StringCastException $e) { - throw new StringCastException(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), null, $e); - } - } - - /** - * Returns the values of the identifier fields of a model. - * - * Propel must know about this model, that is, the model must already - * be persisted or added to the idmodel map before. Otherwise an - * exception is thrown. - * - * @param object $model The model for which to get the identifier - * - * @return array - */ - private function getIdentifierValues($model) - { - if (!$model instanceof $this->class) { - return array(); - } - - if ($model instanceof \Persistent) { - return array($model->getPrimaryKey()); - } - - // readonly="true" models do not implement \Persistent. - if ($model instanceof \BaseObject && method_exists($model, 'getPrimaryKey')) { - return array($model->getPrimaryKey()); - } - - if (!method_exists($model, 'getPrimaryKeys')) { - return array(); - } - - return $model->getPrimaryKeys(); - } - - /** - * Whether this column contains scalar values (to be used as indices). - * - * @param \ColumnMap $column - * - * @return bool - */ - private function isScalar(\ColumnMap $column) - { - return in_array($column->getPdoType(), array( - \PDO::PARAM_BOOL, - \PDO::PARAM_INT, - \PDO::PARAM_STR, - )); - } - - /** - * Check the given choices for equality. - * - * @param mixed $choice - * @param mixed $givenChoice - * - * @return bool - */ - private function isEqual($choice, $givenChoice) - { - if ($choice === $givenChoice) { - return true; - } - - if ($this->getIdentifierValues($choice) === $this->getIdentifierValues($givenChoice)) { - return true; - } - - return false; - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/DataTransformer/CollectionToArrayTransformer.php b/src/Symfony/Bridge/Propel1/Form/DataTransformer/CollectionToArrayTransformer.php deleted file mode 100644 index 09ff457aee9b..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/DataTransformer/CollectionToArrayTransformer.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form\DataTransformer; - -use PropelObjectCollection; -use Symfony\Component\Form\DataTransformerInterface; -use Symfony\Component\Form\Exception\TransformationFailedException; - -/** - * CollectionToArrayTransformer class. - * - * @author William Durand - * @author Pierre-Yves Lebecq - */ -class CollectionToArrayTransformer implements DataTransformerInterface -{ - public function transform($collection) - { - if (null === $collection) { - return array(); - } - - if (!$collection instanceof PropelObjectCollection) { - throw new TransformationFailedException('Expected a \PropelObjectCollection.'); - } - - return $collection->getData(); - } - - public function reverseTransform($array) - { - $collection = new PropelObjectCollection(); - - if ('' === $array || null === $array) { - return $collection; - } - - if (!is_array($array)) { - throw new TransformationFailedException('Expected an array.'); - } - - $collection->setData($array); - - return $collection; - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/EventListener/TranslationCollectionFormListener.php b/src/Symfony/Bridge/Propel1/Form/EventListener/TranslationCollectionFormListener.php deleted file mode 100644 index 1652dfa73ff3..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/EventListener/TranslationCollectionFormListener.php +++ /dev/null @@ -1,103 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form\EventListener; - -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\FormEvent; - -/** - * listener class for propel1_translatable_collection. - * - * @author Patrick Kaufmann - */ -class TranslationCollectionFormListener implements EventSubscriberInterface -{ - private $i18nClass; - private $languages; - - public function __construct($languages, $i18nClass) - { - $this->i18nClass = $i18nClass; - $this->languages = $languages; - } - - public static function getSubscribedEvents() - { - return array( - FormEvents::PRE_SET_DATA => array('preSetData', 1), - ); - } - - public function preSetData(FormEvent $event) - { - $form = $event->getForm(); - $data = $event->getData(); - - if (null === $data) { - return; - } - - if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { - throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); - } - - //get the class name of the i18nClass - $temp = explode('\\', $this->i18nClass); - $dataClass = end($temp); - - $rootData = $form->getRoot()->getData(); - $foundData = false; - - $addFunction = 'add'.$dataClass; - - //add a database row for every needed language - foreach ($this->languages as $lang) { - $found = false; - - foreach ($data as $i18n) { - if (!method_exists($i18n, 'getLocale')) { - throw new UnexpectedTypeException($i18n, 'Propel i18n object'); - } - - if ($i18n->getLocale() == $lang) { - $found = true; - break; - } - } - - if (!$found) { - $currentForm = $form; - while (!$foundData) { - if (method_exists($rootData, $addFunction)) { - $foundData = true; - break; - } elseif ($currentForm->hasParent()) { - $currentForm = $currentForm->getParent(); - $rootData = $currentForm->getData(); - } else { - break; - } - } - if (!$foundData) { - throw new UnexpectedTypeException($rootData, 'Propel i18n object'); - } - - $newTranslation = new $this->i18nClass(); - $newTranslation->setLocale($lang); - - $rootData->$addFunction($newTranslation); - } - } - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/EventListener/TranslationFormListener.php b/src/Symfony/Bridge/Propel1/Form/EventListener/TranslationFormListener.php deleted file mode 100644 index 0bc8734249ed..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/EventListener/TranslationFormListener.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form\EventListener; - -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; - -/** - * Event Listener class for propel1_translation. - * - * @author Patrick Kaufmann - */ -class TranslationFormListener implements EventSubscriberInterface -{ - private $columns; - private $dataClass; - - public function __construct($columns, $dataClass) - { - $this->columns = $columns; - $this->dataClass = $dataClass; - } - - public static function getSubscribedEvents() - { - return array( - FormEvents::PRE_SET_DATA => array('preSetData', 1), - ); - } - - public function preSetData(FormEvent $event) - { - $form = $event->getForm(); - $data = $event->getData(); - - if (!$data instanceof $this->dataClass) { - return; - } - - //loop over all columns and add the input - foreach ($this->columns as $column => $options) { - if (is_string($options)) { - $column = $options; - $options = array(); - } - if (null === $options) { - $options = array(); - } - - $type = 'text'; - if (array_key_exists('type', $options)) { - $type = $options['type']; - } - $label = $column; - if (array_key_exists('label', $options)) { - $label = $options['label']; - } - - $customOptions = array(); - if (array_key_exists('options', $options)) { - $customOptions = $options['options']; - } - $options = array( - 'label' => $label.' '.strtoupper($data->getLocale()), - ); - - $options = array_merge($options, $customOptions); - - $form->add($column, $type, $options); - } - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/PropelExtension.php b/src/Symfony/Bridge/Propel1/Form/PropelExtension.php deleted file mode 100644 index d333f7990378..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/PropelExtension.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form; - -use Symfony\Component\Form\AbstractExtension; -use Symfony\Component\PropertyAccess\PropertyAccess; - -/** - * Represents the Propel form extension, which loads the Propel functionality. - * - * @author Joseph Rouff - */ -class PropelExtension extends AbstractExtension -{ - protected function loadTypes() - { - return array( - new Type\ModelType(PropertyAccess::createPropertyAccessor()), - new Type\TranslationCollectionType(), - new Type\TranslationType(), - ); - } - - protected function loadTypeGuesser() - { - return new PropelTypeGuesser(); - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/PropelTypeGuesser.php b/src/Symfony/Bridge/Propel1/Form/PropelTypeGuesser.php deleted file mode 100644 index 3db34b53e3f7..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/PropelTypeGuesser.php +++ /dev/null @@ -1,180 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form; - -use Symfony\Component\Form\FormTypeGuesserInterface; -use Symfony\Component\Form\Guess\Guess; -use Symfony\Component\Form\Guess\TypeGuess; -use Symfony\Component\Form\Guess\ValueGuess; - -/** - * Propel Type guesser. - * - * @author Fabien Potencier - */ -class PropelTypeGuesser implements FormTypeGuesserInterface -{ - private $cache = array(); - - /** - * {@inheritdoc} - */ - public function guessType($class, $property) - { - if (!$table = $this->getTable($class)) { - return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE); - } - - foreach ($table->getRelations() as $relation) { - if ($relation->getType() === \RelationMap::MANY_TO_ONE) { - if (strtolower($property) === strtolower($relation->getName())) { - return new TypeGuess('model', array( - 'class' => $relation->getForeignTable()->getClassName(), - 'multiple' => false, - ), Guess::HIGH_CONFIDENCE); - } - } elseif ($relation->getType() === \RelationMap::ONE_TO_MANY) { - if (strtolower($property) === strtolower($relation->getPluralName())) { - return new TypeGuess('model', array( - 'class' => $relation->getForeignTable()->getClassName(), - 'multiple' => true, - ), Guess::HIGH_CONFIDENCE); - } - } elseif ($relation->getType() === \RelationMap::MANY_TO_MANY) { - if (strtolower($property) == strtolower($relation->getPluralName())) { - return new TypeGuess('model', array( - 'class' => $relation->getLocalTable()->getClassName(), - 'multiple' => true, - ), Guess::HIGH_CONFIDENCE); - } - } - } - - if (!$column = $this->getColumn($class, $property)) { - return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE); - } - - switch ($column->getType()) { - case \PropelColumnTypes::BOOLEAN: - case \PropelColumnTypes::BOOLEAN_EMU: - return new TypeGuess('checkbox', array(), Guess::HIGH_CONFIDENCE); - case \PropelColumnTypes::TIMESTAMP: - case \PropelColumnTypes::BU_TIMESTAMP: - return new TypeGuess('datetime', array(), Guess::HIGH_CONFIDENCE); - case \PropelColumnTypes::DATE: - case \PropelColumnTypes::BU_DATE: - return new TypeGuess('date', array(), Guess::HIGH_CONFIDENCE); - case \PropelColumnTypes::TIME: - return new TypeGuess('time', array(), Guess::HIGH_CONFIDENCE); - case \PropelColumnTypes::FLOAT: - case \PropelColumnTypes::REAL: - case \PropelColumnTypes::DOUBLE: - case \PropelColumnTypes::DECIMAL: - return new TypeGuess('number', array(), Guess::MEDIUM_CONFIDENCE); - case \PropelColumnTypes::TINYINT: - case \PropelColumnTypes::SMALLINT: - case \PropelColumnTypes::INTEGER: - case \PropelColumnTypes::BIGINT: - case \PropelColumnTypes::NUMERIC: - return new TypeGuess('integer', array(), Guess::MEDIUM_CONFIDENCE); - case \PropelColumnTypes::ENUM: - case \PropelColumnTypes::CHAR: - if ($column->getValueSet()) { - //check if this is mysql enum - $choices = $column->getValueSet(); - $labels = array_map('ucfirst', $choices); - - return new TypeGuess('choice', array('choices' => array_combine($choices, $labels)), Guess::MEDIUM_CONFIDENCE); - } - case \PropelColumnTypes::VARCHAR: - return new TypeGuess('text', array(), Guess::MEDIUM_CONFIDENCE); - case \PropelColumnTypes::LONGVARCHAR: - case \PropelColumnTypes::BLOB: - case \PropelColumnTypes::CLOB: - case \PropelColumnTypes::CLOB_EMU: - return new TypeGuess('textarea', array(), Guess::MEDIUM_CONFIDENCE); - default: - return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE); - } - } - - /** - * {@inheritdoc} - */ - public function guessRequired($class, $property) - { - if ($column = $this->getColumn($class, $property)) { - return new ValueGuess($column->isNotNull(), Guess::HIGH_CONFIDENCE); - } - } - - /** - * {@inheritdoc} - */ - public function guessMaxLength($class, $property) - { - if ($column = $this->getColumn($class, $property)) { - if ($column->isText()) { - return new ValueGuess($column->getSize(), Guess::HIGH_CONFIDENCE); - } - switch ($column->getType()) { - case \PropelColumnTypes::FLOAT: - case \PropelColumnTypes::REAL: - case \PropelColumnTypes::DOUBLE: - case \PropelColumnTypes::DECIMAL: - return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); - } - } - } - - /** - * {@inheritdoc} - */ - public function guessPattern($class, $property) - { - if ($column = $this->getColumn($class, $property)) { - switch ($column->getType()) { - case \PropelColumnTypes::FLOAT: - case \PropelColumnTypes::REAL: - case \PropelColumnTypes::DOUBLE: - case \PropelColumnTypes::DECIMAL: - return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); - } - } - } - - protected function getTable($class) - { - if (isset($this->cache[$class])) { - return $this->cache[$class]; - } - - if (class_exists($queryClass = $class.'Query')) { - $query = new $queryClass(); - - return $this->cache[$class] = $query->getTableMap(); - } - } - - protected function getColumn($class, $property) - { - if (isset($this->cache[$class.'::'.$property])) { - return $this->cache[$class.'::'.$property]; - } - - $table = $this->getTable($class); - - if ($table && $table->hasColumn($property)) { - return $this->cache[$class.'::'.$property] = $table->getColumn($property); - } - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php b/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php deleted file mode 100644 index aa500928a1b7..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form\Type; - -use Symfony\Bridge\Propel1\Form\ChoiceList\ModelChoiceList; -use Symfony\Bridge\Propel1\Form\DataTransformer\CollectionToArrayTransformer; -use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * ModelType class. - * - * @author William Durand - * @author Toni Uebernickel - * - * Example using the preferred_choices option. - * - * - * public function buildForm(FormBuilderInterface $builder, array $options) - * { - * $builder - * ->add('product', 'model', array( - * 'class' => 'Model\Product', - * 'query' => ProductQuery::create() - * ->filterIsActive(true) - * ->useI18nQuery($options['locale']) - * ->orderByName() - * ->endUse() - * , - * 'preferred_choices' => ProductQuery::create() - * ->filterByIsTopProduct(true) - * , - * )) - * ; - * } - * - */ -class ModelType extends AbstractType -{ - /** - * @var PropertyAccessorInterface - */ - private $propertyAccessor; - - /** - * Constructor. - * - * @param PropertyAccessorInterface|null $propertyAccessor - */ - public function __construct(PropertyAccessorInterface $propertyAccessor = null) - { - $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); - } - - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) - { - if ($options['multiple']) { - $builder->addViewTransformer(new CollectionToArrayTransformer(), true); - } - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $propertyAccessor = $this->propertyAccessor; - - $choiceList = function (Options $options) use ($propertyAccessor) { - return new ModelChoiceList( - $options['class'], - $options['property'], - $options['choices'], - $options['query'], - $options['group_by'], - $options['preferred_choices'], - $propertyAccessor - ); - }; - - $resolver->setDefaults(array( - 'template' => 'choice', - 'multiple' => false, - 'expanded' => false, - 'class' => null, - 'property' => null, - 'query' => null, - 'choices' => null, - 'choice_list' => $choiceList, - 'group_by' => null, - 'by_reference' => false, - )); - } - - /** - * {@inheritdoc} - */ - public function getParent() - { - return 'choice'; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'model'; - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/Type/TranslationCollectionType.php b/src/Symfony/Bridge/Propel1/Form/Type/TranslationCollectionType.php deleted file mode 100644 index 8aaa120b55ad..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/Type/TranslationCollectionType.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form\Type; - -use Symfony\Component\Form\AbstractType; -use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; -use Symfony\Bridge\Propel1\Form\EventListener\TranslationCollectionFormListener; - -/** - * form type for i18n-columns in propel. - * - * @author Patrick Kaufmann - */ -class TranslationCollectionType extends AbstractType -{ - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) - { - if (!isset($options['options']['data_class']) || null === $options['options']['data_class']) { - throw new MissingOptionsException('data_class must be set'); - } - if (!isset($options['options']['columns']) || null === $options['options']['columns']) { - throw new MissingOptionsException('columns must be set'); - } - - $listener = new TranslationCollectionFormListener($options['languages'], $options['options']['data_class']); - $builder->addEventSubscriber($listener); - } - - /** - * {@inheritdoc} - */ - public function getParent() - { - return 'collection'; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'propel1_translation_collection'; - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setRequired(array( - 'languages', - )); - - $resolver->setDefaults(array( - 'type' => 'propel1_translation', - 'allow_add' => false, - 'allow_delete' => false, - 'options' => array( - 'data_class' => null, - 'columns' => null, - ), - )); - } -} diff --git a/src/Symfony/Bridge/Propel1/Form/Type/TranslationType.php b/src/Symfony/Bridge/Propel1/Form/Type/TranslationType.php deleted file mode 100644 index 7fed076fe090..000000000000 --- a/src/Symfony/Bridge/Propel1/Form/Type/TranslationType.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Form\Type; - -use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; -use Symfony\Bridge\Propel1\Form\EventListener\TranslationFormListener; - -/** - * Translation type class. - * - * @author Patrick Kaufmann - */ -class TranslationType extends AbstractType -{ - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->addEventSubscriber( - new TranslationFormListener($options['columns'], $options['data_class']) - ); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'propel1_translation'; - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setRequired(array( - 'data_class', - 'columns', - )); - } -} diff --git a/src/Symfony/Bridge/Propel1/Logger/PropelLogger.php b/src/Symfony/Bridge/Propel1/Logger/PropelLogger.php deleted file mode 100644 index b651935afd0e..000000000000 --- a/src/Symfony/Bridge/Propel1/Logger/PropelLogger.php +++ /dev/null @@ -1,170 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Logger; - -use Symfony\Component\Stopwatch\Stopwatch; -use Psr\Log\LoggerInterface; - -/** - * PropelLogger. - * - * @author Fabien Potencier - * @author William Durand - */ -class PropelLogger -{ - /** - * @var LoggerInterface - */ - protected $logger; - - /** - * @var array - */ - protected $queries; - - /** - * @var Stopwatch - */ - protected $stopwatch; - - private $isPrepared; - - /** - * Constructor. - * - * @param LoggerInterface $logger A LoggerInterface instance - * @param Stopwatch $stopwatch A Stopwatch instance - */ - 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. - */ - public function alert($message) - { - if (null !== $this->logger) { - $this->logger->alert($message); - } - } - - /** - * A convenience function for logging a critical event. - * - * @param mixed $message the message to log. - */ - public function crit($message) - { - if (null !== $this->logger) { - $this->logger->critical($message); - } - } - - /** - * A convenience function for logging an error event. - * - * @param mixed $message the message to log. - */ - public function err($message) - { - if (null !== $this->logger) { - $this->logger->error($message); - } - } - - /** - * A convenience function for logging a warning event. - * - * @param mixed $message the message to log. - */ - public function warning($message) - { - if (null !== $this->logger) { - $this->logger->warning($message); - } - } - - /** - * A convenience function for logging an critical event. - * - * @param mixed $message the message to log. - */ - public function notice($message) - { - if (null !== $this->logger) { - $this->logger->notice($message); - } - } - - /** - * A convenience function for logging an critical event. - * - * @param mixed $message the message to log. - */ - public function info($message) - { - if (null !== $this->logger) { - $this->logger->info($message); - } - } - - /** - * A convenience function for logging a debug event. - * - * @param mixed $message the message to log. - */ - public function debug($message) - { - $add = true; - - if (null !== $this->stopwatch) { - $trace = debug_backtrace(); - $method = $trace[2]['args'][2]; - - $watch = 'Propel Query '.(count($this->queries) + 1); - if ('PropelPDO::prepare' === $method) { - $this->isPrepared = true; - $this->stopwatch->start($watch, 'propel'); - - $add = false; - } elseif ($this->isPrepared) { - $this->isPrepared = false; - $this->stopwatch->stop($watch); - } - } - - if ($add) { - $this->queries[] = $message; - if (null !== $this->logger) { - $this->logger->debug($message); - } - } - } - - /** - * Returns queries. - * - * @return array Queries - */ - public function getQueries() - { - return $this->queries; - } -} diff --git a/src/Symfony/Bridge/Propel1/Security/User/PropelUserProvider.php b/src/Symfony/Bridge/Propel1/Security/User/PropelUserProvider.php deleted file mode 100644 index 44a55593d585..000000000000 --- a/src/Symfony/Bridge/Propel1/Security/User/PropelUserProvider.php +++ /dev/null @@ -1,103 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Security\User; - -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - -/** - * Provides easy to use provisioning for Propel model users. - * - * @author William DURAND - */ -class PropelUserProvider implements UserProviderInterface -{ - /** - * A Model class name. - * - * @var string - */ - protected $class; - - /** - * A Query class name. - * - * @var string - */ - protected $queryClass; - - /** - * A property to use to retrieve the user. - * - * @var string - */ - protected $property; - - /** - * Default constructor. - * - * @param string $class The User model class. - * @param string|null $property The property to use to retrieve a user. - */ - public function __construct($class, $property = null) - { - $this->class = $class; - $this->queryClass = $class.'Query'; - $this->property = $property; - } - - /** - * {@inheritdoc} - */ - public function loadUserByUsername($username) - { - $queryClass = $this->queryClass; - $query = $queryClass::create(); - - if (null !== $this->property) { - $filter = 'filterBy'.ucfirst($this->property); - $query->$filter($username); - } else { - $query->filterByUsername($username); - } - - if (null === $user = $query->findOne()) { - throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); - } - - return $user; - } - - /** - * {@inheritdoc} - */ - public function refreshUser(UserInterface $user) - { - if (!$user instanceof $this->class) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); - } - - $queryClass = $this->queryClass; - - return $queryClass::create()->findPk($user->getPrimaryKey()); - } - - /** - * {@inheritdoc} - */ - public function supportsClass($class) - { - return $class === $this->class; - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/DataCollector/PropelDataCollectorTest.php b/src/Symfony/Bridge/Propel1/Tests/DataCollector/PropelDataCollectorTest.php deleted file mode 100644 index f92e43e598e0..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/DataCollector/PropelDataCollectorTest.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\DataCollector; - -use Symfony\Bridge\Propel1\DataCollector\PropelDataCollector; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Bridge\Propel1\Tests\Propel1TestCase; - -class PropelDataCollectorTest extends Propel1TestCase -{ - public function testCollectWithoutData() - { - $c = $this->createCollector(array()); - $c->collect(new Request(), new Response()); - - $this->assertEquals(array(), $c->getQueries()); - $this->assertEquals(0, $c->getQueryCount()); - } - - public function testCollectWithData() - { - $queries = array( - "time: 0.000 sec | mem: 1.4 MB | connection: default | SET NAMES 'utf8'", - ); - - $c = $this->createCollector($queries); - $c->collect(new Request(), new Response()); - - $this->assertEquals(array( - array( - 'sql' => "SET NAMES 'utf8'", - 'time' => '0.000 sec', - 'connection' => 'default', - 'memory' => '1.4 MB', - ), - ), $c->getQueries()); - $this->assertEquals(1, $c->getQueryCount()); - } - - public function testCollectWithMultipleData() - { - $queries = array( - "time: 0.000 sec | mem: 1.4 MB | connection: default | SET NAMES 'utf8'", - 'time: 0.012 sec | mem: 2.4 MB | connection: default | SELECT tags.NAME, image.FILENAME FROM tags LEFT JOIN image ON tags.IMAGEID = image.ID WHERE image.ID = 12', - "time: 0.012 sec | mem: 2.4 MB | connection: default | INSERT INTO `table` (`some_array`) VALUES ('| 1 | 2 | 3 |')", - ); - - $c = $this->createCollector($queries); - $c->collect(new Request(), new Response()); - - $this->assertEquals(array( - array( - 'sql' => "SET NAMES 'utf8'", - 'time' => '0.000 sec', - 'connection' => 'default', - 'memory' => '1.4 MB', - ), - array( - 'sql' => 'SELECT tags.NAME, image.FILENAME FROM tags LEFT JOIN image ON tags.IMAGEID = image.ID WHERE image.ID = 12', - 'time' => '0.012 sec', - 'connection' => 'default', - 'memory' => '2.4 MB', - ), - array( - 'sql' => "INSERT INTO `table` (`some_array`) VALUES ('| 1 | 2 | 3 |')", - 'time' => '0.012 sec', - 'connection' => 'default', - 'memory' => '2.4 MB', - ), - ), $c->getQueries()); - $this->assertEquals(3, $c->getQueryCount()); - $this->assertEquals(0.024, $c->getTime()); - } - - private function createCollector($queries) - { - $config = $this->getMock('\PropelConfiguration'); - - $config - ->expects($this->any()) - ->method('getParameter') - ->will($this->returnArgument(1)) - ; - - $logger = $this->getMock('\Symfony\Bridge\Propel1\Logger\PropelLogger'); - $logger - ->expects($this->any()) - ->method('getQueries') - ->will($this->returnValue($queries)) - ; - - return new PropelDataCollector($logger, $config); - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Fixtures/Column.php b/src/Symfony/Bridge/Propel1/Tests/Fixtures/Column.php deleted file mode 100644 index 431f8e137356..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Fixtures/Column.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Fixtures; - -class Column -{ - private $name; - private $type; - - public function __construct($name, $type) - { - $this->name = $name; - $this->type = $type; - } - - public function getType() - { - return $this->type; - } - - public function isText() - { - if (!$this->type) { - return false; - } - - switch ($this->type) { - case \PropelColumnTypes::CHAR: - case \PropelColumnTypes::VARCHAR: - case \PropelColumnTypes::LONGVARCHAR: - case \PropelColumnTypes::BLOB: - case \PropelColumnTypes::CLOB: - case \PropelColumnTypes::CLOB_EMU: - return true; - } - - return false; - } - - public function getSize() - { - return $this->isText() ? 255 : 0; - } - - public function isNotNull() - { - return 'id' === $this->name; - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Fixtures/Item.php b/src/Symfony/Bridge/Propel1/Tests/Fixtures/Item.php deleted file mode 100644 index faca7df59e3e..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Fixtures/Item.php +++ /dev/null @@ -1,103 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Fixtures; - -class Item implements \Persistent -{ - private $id; - private $value; - private $groupName; - private $price; - - public function __construct($id = null, $value = null, $groupName = null, $price = null) - { - $this->id = $id; - $this->value = $value; - $this->groupName = $groupName; - $this->price = $price; - } - - public function getId() - { - return $this->id; - } - - public function setId($id) - { - $this->id = $id; - } - - public function getValue() - { - return $this->value; - } - - public function getGroupName() - { - return $this->groupName; - } - - public function getPrice() - { - return $this->price; - } - - public function getPrimaryKey() - { - return $this->getId(); - } - - public function setPrimaryKey($primaryKey) - { - $this->setId($primaryKey); - } - - public function isModified() - { - return false; - } - - public function isColumnModified($col) - { - return false; - } - - public function isNew() - { - return false; - } - - public function setNew($b) - { - } - - public function resetModified() - { - } - - public function isDeleted() - { - return false; - } - - public function setDeleted($b) - { - } - - public function delete(\PropelPDO $con = null) - { - } - - public function save(\PropelPDO $con = null) - { - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Fixtures/ItemQuery.php b/src/Symfony/Bridge/Propel1/Tests/Fixtures/ItemQuery.php deleted file mode 100644 index cae25694461c..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Fixtures/ItemQuery.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Fixtures; - -class ItemQuery -{ - private $map = array( - 'id' => \PropelColumnTypes::INTEGER, - 'value' => \PropelColumnTypes::VARCHAR, - 'price' => \PropelColumnTypes::FLOAT, - 'is_active' => \PropelColumnTypes::BOOLEAN, - 'enabled' => \PropelColumnTypes::BOOLEAN_EMU, - 'updated_at' => \PropelColumnTypes::TIMESTAMP, - ); - - public static $result = array(); - - public function find() - { - return self::$result; - } - - public function filterById($id) - { - return $this; - } - - public function getTableMap() - { - // Allows to define methods in this class - // to avoid a lot of mock classes - return $this; - } - - public function getPrimaryKeys() - { - $cm = new \ColumnMap('id', new \TableMap()); - $cm->setType('INTEGER'); - $cm->setPhpName('Id'); - - return array('id' => $cm); - } - - /** - * Method from the TableMap API. - */ - public function hasColumn($column) - { - return in_array($column, array_keys($this->map)); - } - - /** - * Method from the TableMap API. - */ - public function getColumn($column) - { - if ($this->hasColumn($column)) { - return new Column($column, $this->map[$column]); - } - } - - /** - * Method from the TableMap API. - */ - public function getRelations() - { - // table maps - $authorTable = new \TableMap(); - $authorTable->setClassName('\Foo\Author'); - - $resellerTable = new \TableMap(); - $resellerTable->setClassName('\Foo\Reseller'); - - // relations - $mainAuthorRelation = new \RelationMap('MainAuthor'); - $mainAuthorRelation->setType(\RelationMap::MANY_TO_ONE); - $mainAuthorRelation->setForeignTable($authorTable); - - $authorRelation = new \RelationMap('Author'); - $authorRelation->setType(\RelationMap::ONE_TO_MANY); - $authorRelation->setForeignTable($authorTable); - - $resellerRelation = new \RelationMap('Reseller'); - $resellerRelation->setType(\RelationMap::MANY_TO_MANY); - $resellerRelation->setLocalTable($resellerTable); - - return array( - $mainAuthorRelation, - $authorRelation, - $resellerRelation, - ); - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Fixtures/ReadOnlyItemQuery.php b/src/Symfony/Bridge/Propel1/Tests/Fixtures/ReadOnlyItemQuery.php deleted file mode 100644 index 0e77c26fcfc8..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Fixtures/ReadOnlyItemQuery.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Fixtures; - -class ReadOnlyItemQuery -{ - public function getTableMap() - { - // Allows to define methods in this class - // to avoid a lot of mock classes - return $this; - } - - public function getPrimaryKeys() - { - $cm = new \ColumnMap('id', new \TableMap()); - $cm->setType('INTEGER'); - - return array('id' => $cm); - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Fixtures/TranslatableItem.php b/src/Symfony/Bridge/Propel1/Tests/Fixtures/TranslatableItem.php deleted file mode 100644 index 8c1c2e2dc723..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Fixtures/TranslatableItem.php +++ /dev/null @@ -1,125 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Fixtures; - -class TranslatableItem implements \Persistent -{ - private $id; - private $currentTranslations; - private $groupName; - private $price; - - public function __construct($id = null, $translations = array()) - { - $this->id = $id; - $this->currentTranslations = $translations; - } - - public function getId() - { - return $this->id; - } - - public function setId($id) - { - $this->id = $id; - } - - public function getGroupName() - { - return $this->groupName; - } - - public function getPrice() - { - return $this->price; - } - - public function getPrimaryKey() - { - return $this->getId(); - } - - public function setPrimaryKey($primaryKey) - { - $this->setId($primaryKey); - } - - public function isModified() - { - return false; - } - - public function isColumnModified($col) - { - return false; - } - - public function isNew() - { - return false; - } - - public function setNew($b) - { - } - - public function resetModified() - { - } - - public function isDeleted() - { - return false; - } - - public function setDeleted($b) - { - } - - public function delete(\PropelPDO $con = null) - { - } - - public function save(\PropelPDO $con = null) - { - } - - public function getTranslation($locale = 'de', \PropelPDO $con = null) - { - if (!isset($this->currentTranslations[$locale])) { - $translation = new TranslatableItemI18n(); - $translation->setLocale($locale); - $this->currentTranslations[$locale] = $translation; - } - - return $this->currentTranslations[$locale]; - } - - public function addTranslatableItemI18n(TranslatableItemI18n $i) - { - if (!in_array($i, $this->currentTranslations)) { - $this->currentTranslations[$i->getLocale()] = $i; - $i->setItem($this); - } - } - - public function removeTranslatableItemI18n(TranslatableItemI18n $i) - { - unset($this->currentTranslations[$i->getLocale()]); - } - - public function getTranslatableItemI18ns() - { - return $this->currentTranslations; - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Fixtures/TranslatableItemI18n.php b/src/Symfony/Bridge/Propel1/Tests/Fixtures/TranslatableItemI18n.php deleted file mode 100644 index 76c546c0b3d5..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Fixtures/TranslatableItemI18n.php +++ /dev/null @@ -1,128 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Fixtures; - -class TranslatableItemI18n implements \Persistent -{ - private $id; - private $locale; - private $value; - private $value2; - private $item; - - public function __construct($id = null, $locale = null, $value = null) - { - $this->id = $id; - $this->locale = $locale; - $this->value = $value; - } - - public function getId() - { - return $this->id; - } - - public function setId($id) - { - $this->id = $id; - } - - public function getPrimaryKey() - { - return $this->getId(); - } - - public function setPrimaryKey($primaryKey) - { - $this->setId($primaryKey); - } - - public function isModified() - { - return false; - } - - public function isColumnModified($col) - { - return false; - } - - public function isNew() - { - return false; - } - - public function setNew($b) - { - } - - public function resetModified() - { - } - - public function isDeleted() - { - return false; - } - - public function setDeleted($b) - { - } - - public function delete(\PropelPDO $con = null) - { - } - - public function save(\PropelPDO $con = null) - { - } - - public function setLocale($locale) - { - $this->locale = $locale; - } - - public function getLocale() - { - return $this->locale; - } - - public function getItem() - { - return $this->item; - } - - public function setItem($item) - { - $this->item = $item; - } - - public function setValue($value) - { - $this->value = $value; - } - - public function getValue() - { - return $this->value; - } - - public function setValue2($value2) - { - $this->value2 = $value2; - } - - public function getValue2() - { - return $this->value2; - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/CompatModelChoiceListTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/CompatModelChoiceListTest.php deleted file mode 100644 index 2148330ea28c..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/CompatModelChoiceListTest.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Form\ChoiceList; - -use Symfony\Bridge\Propel1\Form\ChoiceList\ModelChoiceList; -use Symfony\Bridge\Propel1\Tests\Fixtures\Item; -use Symfony\Bridge\Propel1\Tests\Fixtures\ItemQuery; -use Symfony\Component\Form\Tests\Extension\Core\ChoiceList\AbstractChoiceListTest; - -class CompatModelChoiceListTest extends AbstractChoiceListTest -{ - const ITEM_CLASS = '\Symfony\Bridge\Propel1\Tests\Fixtures\Item'; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Symfony\Bridge\Propel1\Tests\Fixtures\ItemQuery - */ - protected $query; - - protected $item1; - protected $item2; - protected $item3; - protected $item4; - - public function testGetChoicesForValues() - { - $this->query - ->expects($this->once()) - ->method('filterById') - ->with(array(1, 2)) - ->will($this->returnSelf()) - ; - - ItemQuery::$result = array( - $this->item2, - $this->item1, - ); - - parent::testGetChoicesForValues(); - } - - protected function setUp() - { - $this->query = $this->getMock('Symfony\Bridge\Propel1\Tests\Fixtures\ItemQuery', array( - 'filterById', - ), array(), '', true, true, true, false, true); - - $this->query - ->expects($this->any()) - ->method('filterById') - ->with($this->anything()) - ->will($this->returnSelf()) - ; - - $this->createItems(); - - ItemQuery::$result = array( - $this->item1, - $this->item2, - $this->item3, - $this->item4, - ); - - parent::setUp(); - } - - protected function createItems() - { - $this->item1 = new Item(1, 'Foo'); - $this->item2 = new Item(2, 'Bar'); - $this->item3 = new Item(3, 'Baz'); - $this->item4 = new Item(4, 'Cuz'); - } - - protected function createChoiceList() - { - return new ModelChoiceList(self::ITEM_CLASS, 'value', null, $this->query); - } - - protected function getChoices() - { - return array( - 1 => $this->item1, - 2 => $this->item2, - 3 => $this->item3, - 4 => $this->item4, - ); - } - - protected function getLabels() - { - return array( - 1 => 'Foo', - 2 => 'Bar', - 3 => 'Baz', - 4 => 'Cuz', - ); - } - - protected function getValues() - { - return array( - 1 => '1', - 2 => '2', - 3 => '3', - 4 => '4', - ); - } - - protected function getIndices() - { - return array( - 1, - 2, - 3, - 4, - ); - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/ModelChoiceListTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/ModelChoiceListTest.php deleted file mode 100644 index 4c306bec3186..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/ModelChoiceListTest.php +++ /dev/null @@ -1,249 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Form\ChoiceList; - -use Symfony\Bridge\Propel1\Form\ChoiceList\ModelChoiceList; -use Symfony\Bridge\Propel1\Tests\Propel1TestCase; -use Symfony\Bridge\Propel1\Tests\Fixtures\Item; -use Symfony\Bridge\Propel1\Tests\Fixtures\ItemQuery; -use Symfony\Bridge\Propel1\Tests\Fixtures\ReadOnlyItem; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; - -class ModelChoiceListTest extends Propel1TestCase -{ - const ITEM_CLASS = '\Symfony\Bridge\Propel1\Tests\Fixtures\Item'; - - protected function setUp() - { - ItemQuery::$result = array(); - } - - public function testEmptyChoicesReturnsEmpty() - { - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array() - ); - - $this->assertSame(array(), $choiceList->getChoices()); - } - - public function testReadOnlyIsValidChoice() - { - $item = new ReadOnlyItem(); - $choiceList = new ModelChoiceList( - '\Symfony\Bridge\Propel1\Tests\Fixtures\ReadOnlyItem', - 'name', - array( - $item, - ) - ); - - $this->assertSame(array(42 => $item), $choiceList->getChoices()); - } - - public function testFlattenedChoices() - { - $item1 = new Item(1, 'Foo'); - $item2 = new Item(2, 'Bar'); - - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array( - $item1, - $item2, - ) - ); - - $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); - } - - public function testFlattenedPreferredChoices() - { - $item1 = new Item(1, 'Foo'); - $item2 = new Item(2, 'Bar'); - - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array( - $item1, - $item2, - ), - null, - null, - array( - $item1, - ) - ); - - $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); - $this->assertEquals(array(1 => new ChoiceView($item1, '1', 'Foo')), $choiceList->getPreferredViews()); - } - - public function testNestedChoices() - { - $item1 = new Item(1, 'Foo'); - $item2 = new Item(2, 'Bar'); - - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array( - 'group1' => array($item1), - 'group2' => array($item2), - ) - ); - - $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); - $this->assertEquals(array( - 'group1' => array(1 => new ChoiceView($item1, '1', 'Foo')), - 'group2' => array(2 => new ChoiceView($item2, '2', 'Bar')), - ), $choiceList->getRemainingViews()); - } - - public function testGroupBySupportsString() - { - $item1 = new Item(1, 'Foo', 'Group1'); - $item2 = new Item(2, 'Bar', 'Group1'); - $item3 = new Item(3, 'Baz', 'Group2'); - $item4 = new Item(4, 'Boo!', null); - - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array( - $item1, - $item2, - $item3, - $item4, - ), - null, - 'groupName' - ); - - $this->assertEquals(array(1 => $item1, 2 => $item2, 3 => $item3, 4 => $item4), $choiceList->getChoices()); - $this->assertEquals(array( - 'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')), - 'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')), - 4 => new ChoiceView($item4, '4', 'Boo!'), - ), $choiceList->getRemainingViews()); - } - - public function testGroupByInvalidPropertyPathReturnsFlatChoices() - { - $item1 = new Item(1, 'Foo', 'Group1'); - $item2 = new Item(2, 'Bar', 'Group1'); - - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array( - $item1, - $item2, - ), - null, - 'child.that.does.not.exist' - ); - - $this->assertEquals(array( - 1 => $item1, - 2 => $item2, - ), $choiceList->getChoices()); - } - - public function testGetValuesForChoices() - { - $item1 = new Item(1, 'Foo'); - $item2 = new Item(2, 'Bar'); - - ItemQuery::$result = array( - $item1, - $item2, - ); - - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - null, - null, - null, - null - ); - - $this->assertEquals(array(1, 2), $choiceList->getValuesForChoices(array($item1, $item2))); - $this->assertEquals(array(1, 2), $choiceList->getIndicesForChoices(array($item1, $item2))); - } - - public function testDifferentEqualObjectsAreChosen() - { - $item = new Item(1, 'Foo'); - - ItemQuery::$result = array( - $item, - ); - - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array($item) - ); - - $chosenItem = new Item(1, 'Foo'); - - $this->assertEquals(array(1), $choiceList->getIndicesForChoices(array($chosenItem))); - $this->assertEquals(array('1'), $choiceList->getValuesForChoices(array($chosenItem))); - } - - public function testGetIndicesForNullChoices() - { - $item = new Item(1, 'Foo'); - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array($item) - ); - - $this->assertEquals(array(), $choiceList->getIndicesForChoices(array(null))); - } - - public function testDontAllowInvalidChoiceValues() - { - $item = new Item(1, 'Foo'); - $choiceList = new ModelChoiceList( - self::ITEM_CLASS, - 'value', - array($item) - ); - - $this->assertEquals(array(), $choiceList->getValuesForChoices(array(new Item(2, 'Bar')))); - $this->assertEquals(array(), $choiceList->getChoicesForValues(array(2))); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - */ - public function testEmptyClass() - { - new ModelChoiceList(''); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - */ - public function testInvalidClass() - { - new ModelChoiceList('Foo\Bar\DoesNotExistClass'); - } -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php deleted file mode 100644 index 9426d5c09b6e..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php +++ /dev/null @@ -1,106 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Form\DataTransformer; - -use Symfony\Bridge\Propel1\Form\DataTransformer\CollectionToArrayTransformer; -use Symfony\Bridge\Propel1\Tests\Propel1TestCase; - -class CollectionToArrayTransformerTest extends Propel1TestCase -{ - private $transformer; - - protected function setUp() - { - $this->transformer = new CollectionToArrayTransformer(); - } - - public function testTransform() - { - $result = $this->transformer->transform(new \PropelObjectCollection()); - - $this->assertInternalType('array', $result); - $this->assertCount(0, $result); - } - - public function testTransformWithNull() - { - $result = $this->transformer->transform(null); - - $this->assertInternalType('array', $result); - $this->assertCount(0, $result); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException - */ - public function testTransformThrowsExceptionIfNotPropelObjectCollection() - { - $this->transformer->transform(new DummyObject()); - } - - public function testTransformWithData() - { - $coll = new \PropelObjectCollection(); - $coll->setData(array('foo', 'bar')); - - $result = $this->transformer->transform($coll); - - $this->assertInternalType('array', $result); - $this->assertCount(2, $result); - $this->assertEquals('foo', $result[0]); - $this->assertEquals('bar', $result[1]); - } - - public function testReverseTransformWithNull() - { - $result = $this->transformer->reverseTransform(null); - - $this->assertInstanceOf('\PropelObjectCollection', $result); - $this->assertCount(0, $result->getData()); - } - - public function testReverseTransformWithEmptyString() - { - $result = $this->transformer->reverseTransform(''); - - $this->assertInstanceOf('\PropelObjectCollection', $result); - $this->assertCount(0, $result->getData()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException - */ - public function testReverseTransformThrowsExceptionIfNotArray() - { - $this->transformer->reverseTransform(new DummyObject()); - } - - public function testReverseTransformWithData() - { - $inputData = array('foo', 'bar'); - - $result = $this->transformer->reverseTransform($inputData); - $data = $result->getData(); - - $this->assertInstanceOf('\PropelObjectCollection', $result); - - $this->assertInternalType('array', $data); - $this->assertCount(2, $data); - $this->assertEquals('foo', $data[0]); - $this->assertEquals('bar', $data[1]); - $this->assertsame($inputData, $data); - } -} - -class DummyObject -{ -} diff --git a/src/Symfony/Bridge/Propel1/Tests/Form/PropelTypeGuesserTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/PropelTypeGuesserTest.php deleted file mode 100644 index 1d9e6bdb39c5..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Form/PropelTypeGuesserTest.php +++ /dev/null @@ -1,133 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Form; - -use Symfony\Bridge\Propel1\Form\PropelTypeGuesser; -use Symfony\Bridge\Propel1\Tests\Propel1TestCase; -use Symfony\Component\Form\Guess\Guess; - -class PropelTypeGuesserTest extends Propel1TestCase -{ - const CLASS_NAME = 'Symfony\Bridge\Propel1\Tests\Fixtures\Item'; - const UNKNOWN_CLASS_NAME = 'Symfony\Bridge\Propel1\Tests\Fixtures\UnknownItem'; - - private $guesser; - - protected function setUp() - { - $this->guesser = new PropelTypeGuesser(); - } - - protected function tearDown() - { - $this->guesser = null; - } - - public function testGuessMaxLengthWithText() - { - $value = $this->guesser->guessMaxLength(self::CLASS_NAME, 'value'); - - $this->assertNotNull($value); - $this->assertEquals(255, $value->getValue()); - } - - public function testGuessMaxLengthWithFloat() - { - $value = $this->guesser->guessMaxLength(self::CLASS_NAME, 'price'); - - $this->assertNotNull($value); - $this->assertNull($value->getValue()); - } - - public function testGuessMinLengthWithText() - { - $value = $this->guesser->guessPattern(self::CLASS_NAME, 'value'); - - $this->assertNull($value); - } - - public function testGuessMinLengthWithFloat() - { - $value = $this->guesser->guessPattern(self::CLASS_NAME, 'price'); - - $this->assertNotNull($value); - $this->assertNull($value->getValue()); - } - - public function testGuessRequired() - { - $value = $this->guesser->guessRequired(self::CLASS_NAME, 'id'); - - $this->assertNotNull($value); - $this->assertTrue($value->getValue()); - } - - public function testGuessRequiredWithNullableColumn() - { - $value = $this->guesser->guessRequired(self::CLASS_NAME, 'value'); - - $this->assertNotNull($value); - $this->assertFalse($value->getValue()); - } - - public function testGuessTypeWithoutTable() - { - $value = $this->guesser->guessType(self::UNKNOWN_CLASS_NAME, 'property'); - - $this->assertNotNull($value); - $this->assertEquals('text', $value->getType()); - $this->assertEquals(Guess::LOW_CONFIDENCE, $value->getConfidence()); - } - - public function testGuessTypeWithoutColumn() - { - $value = $this->guesser->guessType(self::CLASS_NAME, 'property'); - - $this->assertNotNull($value); - $this->assertEquals('text', $value->getType()); - $this->assertEquals(Guess::LOW_CONFIDENCE, $value->getConfidence()); - } - - /** - * @dataProvider dataProviderForGuessType - */ - public function testGuessType($property, $type, $confidence, $multiple = null) - { - $value = $this->guesser->guessType(self::CLASS_NAME, $property); - - $this->assertNotNull($value); - $this->assertEquals($type, $value->getType()); - $this->assertEquals($confidence, $value->getConfidence()); - - if ($type === 'model') { - $options = $value->getOptions(); - - $this->assertSame($multiple, $options['multiple']); - } - } - - public static function dataProviderForGuessType() - { - return array( - array('is_active', 'checkbox', Guess::HIGH_CONFIDENCE), - array('enabled', 'checkbox', Guess::HIGH_CONFIDENCE), - array('id', 'integer', Guess::MEDIUM_CONFIDENCE), - array('value', 'text', Guess::MEDIUM_CONFIDENCE), - array('price', 'number', Guess::MEDIUM_CONFIDENCE), - array('updated_at', '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/Form/Type/TranslationCollectionTypeTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/Type/TranslationCollectionTypeTest.php deleted file mode 100644 index cc2006c3ab92..000000000000 --- a/src/Symfony/Bridge/Propel1/Tests/Form/Type/TranslationCollectionTypeTest.php +++ /dev/null @@ -1,151 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Propel1\Tests\Form\Type; - -use Symfony\Bridge\Propel1\Tests\Fixtures\Item; -use Symfony\Bridge\Propel1\Form\PropelExtension; -use Symfony\Bridge\Propel1\Tests\Fixtures\TranslatableItemI18n; -use Symfony\Bridge\Propel1\Tests\Fixtures\TranslatableItem; -use Symfony\Component\Form\Test\TypeTestCase; - -class TranslationCollectionTypeTest extends TypeTestCase -{ - const TRANSLATION_CLASS = 'Symfony\Bridge\Propel1\Tests\Fixtures\TranslatableItem'; - const TRANSLATABLE_I18N_CLASS = 'Symfony\Bridge\Propel1\Tests\Fixtures\TranslatableItemI18n'; - const NON_TRANSLATION_CLASS = 'Symfony\Bridge\Propel1\Tests\Fixtures\Item'; - - protected function getExtensions() - { - return array(new PropelExtension()); - } - - public function testTranslationsAdded() - { - $item = new TranslatableItem(); - $item->addTranslatableItemI18n(new TranslatableItemI18n(1, 'fr', 'val1')); - $item->addTranslatableItemI18n(new TranslatableItemI18n(2, 'en', 'val2')); - - $builder = $this->factory->createBuilder('form', null, array( - 'data_class' => self::TRANSLATION_CLASS, - )); - - $builder->add('translatableItemI18ns', 'propel1_translation_collection', array( - 'languages' => array('en', 'fr'), - 'options' => array( - 'data_class' => self::TRANSLATABLE_I18N_CLASS, - 'columns' => array('value', 'value2' => array('label' => 'Label', 'type' => 'textarea')), - ), - )); - $form = $builder->getForm(); - $form->setData($item); - $translations = $form->get('translatableItemI18ns'); - - $this->assertCount(2, $translations); - $this->assertInstanceOf('Symfony\Component\Form\Form', $translations['en']); - $this->assertInstanceOf('Symfony\Component\Form\Form', $translations['fr']); - - $this->assertInstanceOf(self::TRANSLATABLE_I18N_CLASS, $translations['en']->getData()); - $this->assertInstanceOf(self::TRANSLATABLE_I18N_CLASS, $translations['fr']->getData()); - - $this->assertEquals($item->getTranslation('en'), $translations['en']->getData()); - $this->assertEquals($item->getTranslation('fr'), $translations['fr']->getData()); - - $columnOptions = $translations['fr']->getConfig()->getOption('columns'); - $this->assertEquals('value', $columnOptions[0]); - $this->assertEquals('textarea', $columnOptions['value2']['type']); - $this->assertEquals('Label', $columnOptions['value2']['label']); - } - - public function testNotPresentTranslationsAdded() - { - $item = new TranslatableItem(); - - $this->assertCount(0, $item->getTranslatableItemI18ns()); - - $builder = $this->factory->createBuilder('form', null, array( - 'data_class' => self::TRANSLATION_CLASS, - )); - $builder->add('translatableItemI18ns', 'propel1_translation_collection', array( - 'languages' => array('en', 'fr'), - 'options' => array( - 'data_class' => self::TRANSLATABLE_I18N_CLASS, - 'columns' => array('value', 'value2' => array('label' => 'Label', 'type' => 'textarea')), - ), - )); - - $form = $builder->getForm(); - $form->setData($item); - - $this->assertCount(2, $item->getTranslatableItemI18ns()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testNoArrayGiven() - { - $item = new Item(null, 'val'); - - $builder = $this->factory->createBuilder('form', null, array( - 'data_class' => self::NON_TRANSLATION_CLASS, - )); - $builder->add('value', 'propel1_translation_collection', array( - 'languages' => array('en', 'fr'), - 'options' => array( - 'data_class' => self::TRANSLATABLE_I18N_CLASS, - 'columns' => array('value', 'value2' => array('label' => 'Label', 'type' => 'textarea')), - ), - )); - - $form = $builder->getForm(); - $form->setData($item); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - */ - public function testNoDataClassAdded() - { - $this->factory->createNamed('itemI18ns', 'propel1_translation_collection', null, array( - 'languages' => array('en', 'fr'), - 'options' => array( - 'columns' => array('value', 'value2'), - ), - )); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - */ - public function testNoLanguagesAdded() - { - $this->factory->createNamed('itemI18ns', 'propel1_translation_collection', null, array( - 'options' => array( - 'data_class' => self::TRANSLATABLE_I18N_CLASS, - 'columns' => array('value', 'value2'), - ), - )); - } - - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - */ - public function testNoColumnsAdded() - { - $this->factory->createNamed('itemI18ns', 'propel1_translation_collection', null, array( - 'languages' => array('en', 'fr'), - 'options' => array( - 'data_class' => self::TRANSLATABLE_I18N_CLASS, - ), - )); - } -} 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 cbe96c1d5a4d..69e1b9b3e701 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 @@ -105,6 +105,8 @@ public function __set($name, $value) /** * @param string $name + * + * @return bool */ public function __isset($name) { diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 71059cc35bd2..667d7b50c983 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -16,26 +16,23 @@ } ], "require": { - "php": ">=5.3.3", + "php": ">=5.3.9", "symfony/dependency-injection": "~2.3", - "ocramius/proxy-manager": "~0.3.1|~1.0|~2.0" + "ocramius/proxy-manager": "~0.4|~1.0|~2.0" }, "require-dev": { "symfony/config": "~2.3" }, "autoload": { - "psr-0": { - "Symfony\\Bridge\\ProxyManager\\": "" - }, + "psr-4": { "Symfony\\Bridge\\ProxyManager\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bridge/ProxyManager", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php b/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php index 2e08c273539e..9e1d75ee9401 100644 --- a/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php +++ b/src/Symfony/Bridge/Swiftmailer/DataCollector/MessageDataCollector.php @@ -11,6 +11,8 @@ namespace Symfony\Bridge\Swiftmailer\DataCollector; +@trigger_error(__CLASS__.' class is deprecated since version 2.4 and will be removed in 3.0. Use the Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector class from SwiftmailerBundle instead. Require symfony/swiftmailer-bundle package to download SwiftmailerBundle with Composer.', E_USER_DEPRECATED); + use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -21,6 +23,9 @@ * * @author Fabien Potencier * @author Clément JOBEILI + * + * @deprecated since version 2.4, to be removed in 3.0. + * Use the MessageDataCollector from SwiftmailerBundle instead. */ class MessageDataCollector extends DataCollector { diff --git a/src/Symfony/Bridge/Swiftmailer/composer.json b/src/Symfony/Bridge/Swiftmailer/composer.json index ad203340e3f4..81b1d570eadc 100644 --- a/src/Symfony/Bridge/Swiftmailer/composer.json +++ b/src/Symfony/Bridge/Swiftmailer/composer.json @@ -16,23 +16,22 @@ } ], "require": { - "php": ">=5.3.3", + "php": ">=5.3.9", "swiftmailer/swiftmailer": ">=4.2.0,<6.0-dev" }, "suggest": { "symfony/http-kernel": "" }, "autoload": { - "psr-0": { "Symfony\\Bridge\\Swiftmailer\\": "" }, + "psr-4": { "Symfony\\Bridge\\Swiftmailer\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bridge/Swiftmailer", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bridge/Twig/AppVariable.php b/src/Symfony/Bridge/Twig/AppVariable.php new file mode 100644 index 000000000000..88d9835ce239 --- /dev/null +++ b/src/Symfony/Bridge/Twig/AppVariable.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\SecurityContext; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Exposes some Symfony parameters and services as an "app" global variable. + * + * @author Fabien Potencier + */ +class AppVariable +{ + private $container; + private $tokenStorage; + private $requestStack; + private $environment; + private $debug; + + /** + * @deprecated since version 2.7, to be removed in 3.0. + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + } + + public function setTokenStorage(TokenStorageInterface $tokenStorage) + { + $this->tokenStorage = $tokenStorage; + } + + public function setRequestStack(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + + public function setEnvironment($environment) + { + $this->environment = $environment; + } + + public function setDebug($debug) + { + $this->debug = (bool) $debug; + } + + /** + * Returns the security context service. + * + * @deprecated since version 2.6, to be removed in 3.0. + * + * @return SecurityContext|null The security context + */ + public function getSecurity() + { + @trigger_error('The "app.security" variable is deprecated since version 2.6 and will be removed in 3.0.', E_USER_DEPRECATED); + + if (null === $this->container) { + throw new \RuntimeException('The "app.security" variable is not available.'); + } + + if ($this->container->has('security.context')) { + return $this->container->get('security.context'); + } + } + + /** + * Returns the current user. + * + * @return mixed + * + * @see TokenInterface::getUser() + */ + public function getUser() + { + if (null === $this->tokenStorage) { + if (null === $this->container) { + throw new \RuntimeException('The "app.user" variable is not available.'); + } elseif (!$this->container->has('security.context')) { + return; + } + + $this->tokenStorage = $this->container->get('security.context'); + } + + if (!$token = $this->tokenStorage->getToken()) { + return; + } + + $user = $token->getUser(); + if (is_object($user)) { + return $user; + } + } + + /** + * Returns the current request. + * + * @return Request|null The HTTP request object + */ + public function getRequest() + { + if (null === $this->requestStack) { + if (null === $this->container) { + throw new \RuntimeException('The "app.request" variable is not available.'); + } + + $this->requestStack = $this->container->get('request_stack'); + } + + return $this->requestStack->getCurrentRequest(); + } + + /** + * Returns the current session. + * + * @return Session|null The session + */ + public function getSession() + { + if (null === $this->requestStack && null === $this->container) { + throw new \RuntimeException('The "app.session" variable is not available.'); + } + + if ($request = $this->getRequest()) { + return $request->getSession(); + } + } + + /** + * Returns the current app environment. + * + * @return string The current environment string (e.g 'dev') + */ + public function getEnvironment() + { + if (null === $this->environment) { + throw new \RuntimeException('The "app.environment" variable is not available.'); + } + + return $this->environment; + } + + /** + * Returns the current app debug mode. + * + * @return bool The current debug mode + */ + public function getDebug() + { + if (null === $this->debug) { + throw new \RuntimeException('The "app.debug" variable is not available.'); + } + + return $this->debug; + } +} diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index ad22216e40b8..b4df596fa2c0 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,24 @@ CHANGELOG ========= +2.7.0 +----- + + * added LogoutUrlExtension (provides `logout_url` and `logout_path`) + * added an HttpFoundation extension (provides the `absolute_url` and the `relative_path` functions) + * added AssetExtension (provides the `asset` and `asset_version` functions) + * Added possibility to extract translation messages from a file or files besides extracting from a directory + +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/DebugCommand.php b/src/Symfony/Bridge/Twig/Command/DebugCommand.php new file mode 100644 index 000000000000..4bf52c5dd3e3 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Command/DebugCommand.php @@ -0,0 +1,221 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Lists twig functions, filters, globals and tests present in the current project. + * + * @author Jordi Boggiano + */ +class DebugCommand extends Command +{ + private $twig; + + /** + * {@inheritdoc} + */ + public function __construct($name = 'debug:twig') + { + 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 + ->setDefinition(array( + new InputArgument('filter', InputArgument::OPTIONAL, 'Show details for all entries matching this filter'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (text or json)', 'text'), + )) + ->setDescription('Shows a list of twig functions, filters, globals and tests') + ->setHelp(<<%command.name% command outputs a list of twig functions, +filters, globals and tests. Output can be filtered with an optional argument. + + php %command.full_name% + +The command lists all functions, filters, etc. + + php %command.full_name% date + +The command lists everything that contains the word date. + + php %command.full_name% --format=json + +The command lists everything in a machine readable json format. +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $twig = $this->getTwigEnvironment(); + + if (null === $twig) { + $output->writeln('The Twig environment needs to be set.'); + + return 1; + } + + $types = array('functions', 'filters', 'tests', 'globals'); + + if ($input->getOption('format') === 'json') { + $data = array(); + foreach ($types as $type) { + foreach ($twig->{'get'.ucfirst($type)}() as $name => $entity) { + $data[$type][$name] = $this->getMetadata($type, $entity); + } + } + $data['tests'] = array_keys($data['tests']); + $output->writeln(json_encode($data)); + + return 0; + } + + $filter = $input->getArgument('filter'); + + foreach ($types as $index => $type) { + $items = array(); + foreach ($twig->{'get'.ucfirst($type)}() as $name => $entity) { + if (!$filter || false !== strpos($name, $filter)) { + $items[$name] = $name.$this->getPrettyMetadata($type, $entity); + } + } + + if (!$items) { + continue; + } + if ($index > 0) { + $output->writeln(''); + } + $output->writeln(''.ucfirst($type).''); + ksort($items); + foreach ($items as $item) { + $output->writeln(' '.$item); + } + } + + return 0; + } + + private function getMetadata($type, $entity) + { + if ($type === 'globals') { + return $entity; + } + if ($type === 'tests') { + return; + } + if ($type === 'functions' || $type === 'filters') { + $args = array(); + $cb = $entity->getCallable(); + if (is_null($cb)) { + return; + } + if (is_array($cb)) { + if (!method_exists($cb[0], $cb[1])) { + return; + } + $refl = new \ReflectionMethod($cb[0], $cb[1]); + } elseif (is_object($cb) && method_exists($cb, '__invoke')) { + $refl = new \ReflectionMethod($cb, '__invoke'); + } elseif (function_exists($cb)) { + $refl = new \ReflectionFunction($cb); + } elseif (is_string($cb) && preg_match('{^(.+)::(.+)$}', $cb, $m) && method_exists($m[1], $m[2])) { + $refl = new \ReflectionMethod($m[1], $m[2]); + } else { + throw new \UnexpectedValueException('Unsupported callback type'); + } + + // filter out context/environment args + $args = array_filter($refl->getParameters(), function ($param) use ($entity) { + if ($entity->needsContext() && $param->getName() === 'context') { + return false; + } + + return !$param->getClass() || $param->getClass()->getName() !== 'Twig_Environment'; + }); + + // format args + $args = array_map(function ($param) { + if ($param->isDefaultValueAvailable()) { + return $param->getName().' = '.json_encode($param->getDefaultValue()); + } + + return $param->getName(); + }, $args); + + if ($type === 'filters') { + // remove the value the filter is applied on + array_shift($args); + } + + return $args; + } + } + + private function getPrettyMetadata($type, $entity) + { + if ($type === 'tests') { + return ''; + } + + try { + $meta = $this->getMetadata($type, $entity); + if ($meta === null) { + return '(unknown?)'; + } + } catch (\UnexpectedValueException $e) { + return ' '.$e->getMessage().''; + } + + if ($type === 'globals') { + if (is_object($meta)) { + return ' = object('.get_class($meta).')'; + } + + return ' = '.substr(@json_encode($meta), 0, 50); + } + + if ($type === 'functions') { + return '('.implode(', ', $meta).')'; + } + + if ($type === 'filters') { + return $meta ? '('.implode(', ', $meta).')' : ''; + } + } +} diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php new file mode 100644 index 000000000000..95d550a058bc --- /dev/null +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -0,0 +1,248 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Command; + +use Symfony\Component\Console\Command\Command; +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\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 = 'lint:twig') + { + 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 + ->setAliases(array('twig:lint')) + ->setDescription('Lints a template and outputs encountered errors') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->addArgument('filename', InputArgument::IS_ARRAY) + ->setHelp(<<%command.name% command lints a template and outputs to STDOUT +the first encountered syntax error. + +You can validate the syntax of contents passed from STDIN: + + cat filename | php %command.full_name% + +Or 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 + +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (false !== strpos($input->getFirstArgument(), ':l')) { + $output->writeln('The use of "twig:lint" command is deprecated since version 2.7 and will be removed in 3.0. Use the "lint:twig" instead.'); + } + + $twig = $this->getTwigEnvironment(); + + if (null === $twig) { + $output->writeln('The Twig environment needs to be set.'); + + return 1; + } + + $filenames = $input->getArgument('filename'); + + if (0 === count($filenames)) { + 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, uniqid('sf_')))); + } + + $filesInfo = $this->getFilesInfo($twig, $filenames); + + return $this->display($input, $output, $filesInfo); + } + + private function getFilesInfo(\Twig_Environment $twig, array $filenames) + { + $filesInfo = array(); + foreach ($filenames as $filename) { + foreach ($this->findFiles($filename) as $file) { + $filesInfo[] = $this->validate($twig, file_get_contents($file), $file); + } + } + + return $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) + { + $realLoader = $twig->getLoader(); + try { + $temporaryLoader = new \Twig_Loader_Array(array((string) $file => $template)); + $twig->setLoader($temporaryLoader); + $nodeTree = $twig->parse($twig->tokenize($template, (string) $file)); + $twig->compile($nodeTree); + $twig->setLoader($realLoader); + } catch (\Twig_Error $e) { + $twig->setLoader($realLoader); + + 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, defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0)); + + 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/DataCollector/TwigDataCollector.php b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php new file mode 100644 index 000000000000..763a5ec4b31e --- /dev/null +++ b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\DataCollector; + +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * TwigDataCollector. + * + * @author Fabien Potencier + */ +class TwigDataCollector extends DataCollector implements LateDataCollectorInterface +{ + private $profile; + private $computed; + + public function __construct(\Twig_Profiler_Profile $profile) + { + $this->profile = $profile; + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + } + + /** + * {@inheritdoc} + */ + public function lateCollect() + { + $this->data['profile'] = serialize($this->profile); + } + + public function getTime() + { + return $this->getProfile()->getDuration() * 1000; + } + + public function getTemplateCount() + { + return $this->getComputedData('template_count'); + } + + public function getTemplates() + { + return $this->getComputedData('templates'); + } + + public function getBlockCount() + { + return $this->getComputedData('block_count'); + } + + public function getMacroCount() + { + return $this->getComputedData('macro_count'); + } + + public function getHtmlCallGraph() + { + $dumper = new \Twig_Profiler_Dumper_Html(); + + return new \Twig_Markup($dumper->dump($this->getProfile()), 'UTF-8'); + } + + public function getProfile() + { + if (null === $this->profile) { + $this->profile = unserialize($this->data['profile']); + } + + return $this->profile; + } + + private function getComputedData($index) + { + if (null === $this->computed) { + $this->computed = $this->computeData($this->getProfile()); + } + + return $this->computed[$index]; + } + + private function computeData(\Twig_Profiler_Profile $profile) + { + $data = array( + 'template_count' => 0, + 'block_count' => 0, + 'macro_count' => 0, + ); + + $templates = array(); + foreach ($profile as $p) { + $d = $this->computeData($p); + + $data['template_count'] += ($p->isTemplate() ? 1 : 0) + $d['template_count']; + $data['block_count'] += ($p->isBlock() ? 1 : 0) + $d['block_count']; + $data['macro_count'] += ($p->isMacro() ? 1 : 0) + $d['macro_count']; + + if ($p->isTemplate()) { + if (!isset($templates[$p->getTemplate()])) { + $templates[$p->getTemplate()] = 1; + } else { + ++$templates[$p->getTemplate()]; + } + } + + foreach ($d['templates'] as $template => $count) { + if (!isset($templates[$template])) { + $templates[$template] = $count; + } else { + $templates[$template] += $count; + } + } + } + $data['templates'] = $templates; + + return $data; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'twig'; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php new file mode 100644 index 000000000000..a72f4503dd86 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +/** + * Twig extension for the Symfony Asset component. + * + * @author Fabien Potencier + */ +class AssetExtension extends \Twig_Extension +{ + private $packages; + private $foundationExtension; + + /** + * Passing an HttpFoundationExtension instance as a second argument must not be relied on + * as it's only there to maintain BC with older Symfony version. It will be removed in 3.0. + */ + public function __construct(Packages $packages, HttpFoundationExtension $foundationExtension = null) + { + $this->packages = $packages; + $this->foundationExtension = $foundationExtension; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('asset', array($this, 'getAssetUrl')), + new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')), + new \Twig_SimpleFunction('assets_version', array($this, 'getAssetsVersion'), array('deprecated' => true, 'alternative' => 'asset_version')), + ); + } + + /** + * Returns the public url/path of an asset. + * + * If the package used to generate the path is an instance of + * UrlPackage, you will always get a URL and not a path. + * + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * + * @return string The public path of the asset + */ + public function getAssetUrl($path, $packageName = null, $absolute = false, $version = null) + { + // BC layer to be removed in 3.0 + if (2 < $count = func_num_args()) { + @trigger_error('Generating absolute URLs with the Twig asset() function was deprecated in 2.7 and will be removed in 3.0. Please use absolute_url() instead.', E_USER_DEPRECATED); + if (4 === $count) { + @trigger_error('Forcing a version with the Twig asset() function was deprecated in 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + } + + $args = func_get_args(); + + return $this->getLegacyAssetUrl($path, $packageName, $args[2], isset($args[3]) ? $args[3] : null); + } + + return $this->packages->getUrl($path, $packageName); + } + + /** + * Returns the version of an asset. + * + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * + * @return string The asset version + */ + public function getAssetVersion($path, $packageName = null) + { + return $this->packages->getVersion($path, $packageName); + } + + public function getAssetsVersion($packageName = null) + { + @trigger_error('The Twig assets_version() function was deprecated in 2.7 and will be removed in 3.0. Please use asset_version() instead.', E_USER_DEPRECATED); + + return $this->packages->getVersion('/', $packageName); + } + + private function getLegacyAssetUrl($path, $packageName = null, $absolute = false, $version = null) + { + if ($version) { + $package = $this->packages->getPackage($packageName); + + $v = new \ReflectionProperty('Symfony\Component\Asset\Package', 'versionStrategy'); + $v->setAccessible(true); + + $currentVersionStrategy = $v->getValue($package); + + if (property_exists($currentVersionStrategy, 'format')) { + $f = new \ReflectionProperty($currentVersionStrategy, 'format'); + $f->setAccessible(true); + + $format = $f->getValue($currentVersionStrategy); + + $v->setValue($package, new StaticVersionStrategy($version, $format)); + } else { + $v->setValue($package, new StaticVersionStrategy($version)); + } + } + + try { + $url = $this->packages->getUrl($path, $packageName); + } catch (\Exception $e) { + if ($version) { + $v->setValue($package, $currentVersionStrategy); + } + + throw $e; + } + + if ($version) { + $v->setValue($package, $currentVersionStrategy); + } + + if ($absolute) { + return $this->foundationExtension->generateAbsoluteUrl($url); + } + + return $url; + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'asset'; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index d9ba2c05bd6c..b7c3605d9572 100644 --- a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php @@ -31,8 +31,8 @@ class CodeExtension extends \Twig_Extension */ public function __construct($fileLinkFormat, $rootDir, $charset) { - $this->fileLinkFormat = empty($fileLinkFormat) ? ini_get('xdebug.file_link_format') : $fileLinkFormat; - $this->rootDir = str_replace('\\', '/', $rootDir).'/'; + $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + $this->rootDir = str_replace('/', DIRECTORY_SEPARATOR, dirname($rootDir)).DIRECTORY_SEPARATOR; $this->charset = $charset; } @@ -160,12 +160,14 @@ public function fileExcerpt($file, $line) */ public function formatFile($file, $line, $text = null) { + $file = trim($file); + if (null === $text) { - $file = trim($file); - $text = $file; + $text = str_replace('/', DIRECTORY_SEPARATOR, $file); if (0 === strpos($text, $this->rootDir)) { - $text = str_replace($this->rootDir, '', str_replace('\\', '/', $text)); - $text = sprintf('kernel.root_dir/%s', $this->rootDir, $text); + $text = substr($text, strlen($this->rootDir)); + $text = explode(DIRECTORY_SEPARATOR, $text, 2); + $text = sprintf('%s%s', $this->rootDir, $text[0], isset($text[1]) ? DIRECTORY_SEPARATOR.$text[1] : ''); } } diff --git a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php new file mode 100644 index 000000000000..30318ecac6d0 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/DumpExtension.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\Bridge\Twig\Extension; + +use Symfony\Bridge\Twig\TokenParser\DumpTokenParser; +use Symfony\Component\VarDumper\Cloner\ClonerInterface; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; + +/** + * Provides integration of the dump() function with Twig. + * + * @author Nicolas Grekas + */ +class DumpExtension extends \Twig_Extension +{ + private $cloner; + + public function __construct(ClonerInterface $cloner) + { + $this->cloner = $cloner; + } + + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('dump', array($this, 'dump'), array('is_safe' => array('html'), 'needs_context' => true, 'needs_environment' => true)), + ); + } + + public function getTokenParsers() + { + return array(new DumpTokenParser()); + } + + public function getName() + { + return 'dump'; + } + + public function dump(\Twig_Environment $env, $context) + { + if (!$env->isDebug()) { + return; + } + + if (2 === func_num_args()) { + $vars = array(); + foreach ($context as $key => $value) { + if (!$value instanceof \Twig_Template) { + $vars[$key] = $value; + } + } + + $vars = array($vars); + } else { + $vars = func_get_args(); + unset($vars[0], $vars[1]); + } + + $dump = fopen('php://memory', 'r+b'); + $dumper = new HtmlDumper($dump); + + foreach ($vars as $value) { + $dumper->dump($this->cloner->cloneVar($value)); + } + rewind($dump); + + return stream_get_contents($dump); + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php b/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php new file mode 100644 index 000000000000..6b30a279419b --- /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/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index e972ac4354ba..3f0a42333731 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -61,7 +61,7 @@ public function getTokenParsers() public function getFunctions() { return array( - new \Twig_SimpleFunction('form_enctype', null, array('node_class' => 'Symfony\Bridge\Twig\Node\FormEnctypeNode', 'is_safe' => array('html'))), + new \Twig_SimpleFunction('form_enctype', null, array('node_class' => 'Symfony\Bridge\Twig\Node\FormEnctypeNode', 'is_safe' => array('html'), 'deprecated' => true, 'alternative' => 'form_start')), new \Twig_SimpleFunction('form_widget', null, array('node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => array('html'))), new \Twig_SimpleFunction('form_errors', null, array('node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => array('html'))), new \Twig_SimpleFunction('form_label', null, array('node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => array('html'))), diff --git a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php new file mode 100644 index 000000000000..69d6d326f4d0 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php @@ -0,0 +1,129 @@ + + * + * 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\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\RequestContext; + +/** + * Twig extension for the Symfony HttpFoundation component. + * + * @author Fabien Potencier + */ +class HttpFoundationExtension extends \Twig_Extension +{ + private $requestStack; + private $requestContext; + + public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) + { + $this->requestStack = $requestStack; + $this->requestContext = $requestContext; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('absolute_url', array($this, 'generateAbsoluteUrl')), + new \Twig_SimpleFunction('relative_path', array($this, 'generateRelativePath')), + ); + } + + /** + * Returns the absolute URL for the given absolute or relative path. + * + * This method returns the path unchanged if no request is available. + * + * @param string $path The path + * + * @return string The absolute URL + * + * @see Request::getUriForPath() + */ + public function generateAbsoluteUrl($path) + { + if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) { + return $path; + } + + if (!$request = $this->requestStack->getMasterRequest()) { + if (null !== $this->requestContext && '' !== $host = $this->requestContext->getHost()) { + $scheme = $this->requestContext->getScheme(); + $port = ''; + + if ('http' === $scheme && 80 != $this->requestContext->getHttpPort()) { + $port = ':'.$this->requestContext->getHttpPort(); + } elseif ('https' === $scheme && 443 != $this->requestContext->getHttpsPort()) { + $port = ':'.$this->requestContext->getHttpsPort(); + } + + if ('/' !== $path[0]) { + $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; + } + + return $scheme.'://'.$host.$port.$path; + } + + return $path; + } + + if (!$path || '/' !== $path[0]) { + $prefix = $request->getPathInfo(); + $last = strlen($prefix) - 1; + if ($last !== $pos = strrpos($prefix, '/')) { + $prefix = substr($prefix, 0, $pos).'/'; + } + + return $request->getUriForPath($prefix.$path); + } + + return $request->getSchemeAndHttpHost().$path; + } + + /** + * Returns a relative path based on the current Request. + * + * This method returns the path unchanged if no request is available. + * + * @param string $path The path + * + * @return string The relative path + * + * @see Request::getRelativeUriForPath() + */ + public function generateRelativePath($path) + { + if (false !== strpos($path, '://') || '//' === substr($path, 0, 2)) { + return $path; + } + + if (!$request = $this->requestStack->getMasterRequest()) { + return $path; + } + + return $request->getRelativeUriForPath($path); + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'request'; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php new file mode 100644 index 000000000000..7fc278758eb3 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; + +/** + * LogoutUrlHelper provides generator functions for the logout URL to Twig. + * + * @author Jeremy Mikola + */ +class LogoutUrlExtension extends \Twig_Extension +{ + private $generator; + + public function __construct(LogoutUrlGenerator $generator) + { + $this->generator = $generator; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + return array( + new \Twig_SimpleFunction('logout_url', array($this, 'getLogoutUrl')), + new \Twig_SimpleFunction('logout_path', array($this, 'getLogoutPath')), + ); + } + + /** + * Generates the relative logout URL for the firewall. + * + * @param string|null $key The firewall key or null to use the current firewall key + * + * @return string The relative logout URL + */ + public function getLogoutPath($key = null) + { + return $this->generator->getLogoutPath($key, UrlGeneratorInterface::ABSOLUTE_PATH); + } + + /** + * Generates the absolute logout URL for the firewall. + * + * @param string|null $key The firewall key or null to use the current firewall key + * + * @return string The absolute logout URL + */ + public function getLogoutUrl($key = null) + { + return $this->generator->getLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_URL); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'logout_url'; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php new file mode 100644 index 000000000000..648a6c8036d7 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.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\Bridge\Twig\Extension; + +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * @author Fabien Potencier + */ +class ProfilerExtension extends \Twig_Extension_Profiler +{ + private $stopwatch; + private $events; + + public function __construct(\Twig_Profiler_Profile $profile, Stopwatch $stopwatch = null) + { + parent::__construct($profile); + + $this->stopwatch = $stopwatch; + $this->events = new \SplObjectStorage(); + } + + public function enter(\Twig_Profiler_Profile $profile) + { + if ($this->stopwatch && $profile->isTemplate()) { + $this->events[$profile] = $this->stopwatch->start($profile->getName(), 'template'); + } + + parent::enter($profile); + } + + public function leave(\Twig_Profiler_Profile $profile) + { + parent::leave($profile); + + if ($this->stopwatch && $profile->isTemplate()) { + $this->events[$profile]->stop(); + unset($this->events[$profile]); + } + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'native_profiler'; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php index 49863a4e3f1c..b13d7c0f8533 100644 --- a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php @@ -12,7 +12,7 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Component\Security\Acl\Voter\FieldVote; -use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; /** * SecurityExtension exposes security context features. @@ -21,16 +21,16 @@ */ class SecurityExtension extends \Twig_Extension { - private $context; + private $securityChecker; - public function __construct(SecurityContextInterface $context = null) + public function __construct(AuthorizationCheckerInterface $securityChecker = null) { - $this->context = $context; + $this->securityChecker = $securityChecker; } public function isGranted($role, $object = null, $field = null) { - if (null === $this->context) { + if (null === $this->securityChecker) { return false; } @@ -38,7 +38,7 @@ public function isGranted($role, $object = null, $field = null) $object = new FieldVote($object, $field); } - return $this->context->isGranted($role, $object); + return $this->securityChecker->isGranted($role, $object); } /** diff --git a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php new file mode 100644 index 000000000000..52af92324c22 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.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\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; + + /** + * @var bool + */ + private $enabled; + + public function __construct(Stopwatch $stopwatch = null, $enabled = true) + { + $this->stopwatch = $stopwatch; + $this->enabled = $enabled; + } + + 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 && $this->enabled), + ); + } + + public function getName() + { + return 'stopwatch'; + } +} diff --git a/src/Symfony/Bridge/Twig/Form/TwigRenderer.php b/src/Symfony/Bridge/Twig/Form/TwigRenderer.php index 72798d103fa1..ac139e44a133 100644 --- a/src/Symfony/Bridge/Twig/Form/TwigRenderer.php +++ b/src/Symfony/Bridge/Twig/Form/TwigRenderer.php @@ -12,7 +12,6 @@ namespace Symfony\Bridge\Twig\Form; use Symfony\Component\Form\FormRenderer; -use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; /** * @author Bernhard Schussek @@ -24,9 +23,9 @@ 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); + parent::__construct($engine, $csrfTokenManager); $this->engine = $engine; } diff --git a/src/Symfony/Bridge/Twig/Node/DumpNode.php b/src/Symfony/Bridge/Twig/Node/DumpNode.php new file mode 100644 index 000000000000..522497ba655d --- /dev/null +++ b/src/Symfony/Bridge/Twig/Node/DumpNode.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Node; + +/** + * @author Julien Galenski + */ +class DumpNode extends \Twig_Node +{ + private $varPrefix; + + public function __construct($varPrefix, \Twig_Node $values = null, $lineno, $tag = null) + { + parent::__construct(array('values' => $values), array(), $lineno, $tag); + $this->varPrefix = $varPrefix; + } + + /** + * {@inheritdoc} + */ + public function compile(\Twig_Compiler $compiler) + { + $compiler + ->write("if (\$this->env->isDebug()) {\n") + ->indent(); + + $values = $this->getNode('values'); + + if (null === $values) { + // remove embedded templates (macros) from the context + $compiler + ->write(sprintf('$%svars = array();'."\n", $this->varPrefix)) + ->write(sprintf('foreach ($context as $%1$skey => $%1$sval) {'."\n", $this->varPrefix)) + ->indent() + ->write(sprintf('if (!$%sval instanceof \Twig_Template) {'."\n", $this->varPrefix)) + ->indent() + ->write(sprintf('$%1$svars[$%1$skey] = $%1$sval;'."\n", $this->varPrefix)) + ->outdent() + ->write("}\n") + ->outdent() + ->write("}\n") + ->addDebugInfo($this) + ->write(sprintf('\Symfony\Component\VarDumper\VarDumper::dump($%svars);'."\n", $this->varPrefix)); + } elseif (1 === $values->count()) { + $compiler + ->addDebugInfo($this) + ->write('\Symfony\Component\VarDumper\VarDumper::dump(') + ->subcompile($values->getNode(0)) + ->raw(");\n"); + } else { + $compiler + ->addDebugInfo($this) + ->write('\Symfony\Component\VarDumper\VarDumper::dump(array('."\n") + ->indent(); + foreach ($values as $node) { + $compiler->addIndentation(); + if ($node->hasAttribute('name')) { + $compiler + ->string($node->getAttribute('name')) + ->raw(' => '); + } + $compiler + ->subcompile($node) + ->raw(",\n"); + } + $compiler + ->outdent() + ->write("));\n"); + } + + $compiler + ->outdent() + ->write("}\n"); + } +} diff --git a/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php b/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php index fc2e53b5303d..14811e6d9749 100644 --- a/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php +++ b/src/Symfony/Bridge/Twig/Node/FormEnctypeNode.php @@ -14,8 +14,7 @@ /** * @author Bernhard Schussek * - * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use - * the helper "form_start()" instead. + * @deprecated since version 2.3, to be removed in 3.0. Use the helper "form_start()" instead. */ class FormEnctypeNode extends SearchAndRenderBlockNode { diff --git a/src/Symfony/Bridge/Twig/Node/StopwatchNode.php b/src/Symfony/Bridge/Twig/Node/StopwatchNode.php new file mode 100644 index 000000000000..06eeb492728c --- /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_Node $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 ad7a7362f227..f9333bf683d1 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php @@ -24,12 +24,12 @@ class Scope /** * @var array */ - private $data; + private $data = array(); /** * @var bool */ - private $left; + private $left = false; /** * @param Scope $parent @@ -37,8 +37,6 @@ class Scope public function __construct(Scope $parent = null) { $this->parent = $parent; - $this->left = false; - $this->data = array(); } /** @@ -69,9 +67,9 @@ public function leave() * @param string $key * @param mixed $value * - * @throws \LogicException - * * @return Scope Current scope + * + * @throws \LogicException */ public function set($key, $value) { diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig new file mode 100644 index 000000000000..5de20b1b8f18 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig @@ -0,0 +1,81 @@ +{% use "bootstrap_3_layout.html.twig" %} + +{% block form_start -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-horizontal')|trim}) %} + {{- parent() -}} +{%- endblock form_start %} + +{# Labels #} + +{% block form_label -%} +{% spaceless %} + {% if label is same as(false) %} +
+ {% else %} + {% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ block('form_label_class'))|trim}) %} + {{- parent() -}} + {% endif %} +{% endspaceless %} +{%- endblock form_label %} + +{% block form_label_class -%} +col-sm-2 +{%- endblock form_label_class %} + +{# Rows #} + +{% block form_row -%} +
+ {{- form_label(form) -}} +
+ {{- form_widget(form) -}} + {{- form_errors(form) -}} +
+{##}
+{%- endblock form_row %} + +{% block checkbox_row -%} + {{- block('checkbox_radio_row') -}} +{%- endblock checkbox_row %} + +{% block radio_row -%} + {{- block('checkbox_radio_row') -}} +{%- endblock radio_row %} + +{% block checkbox_radio_row -%} +{% spaceless %} +
+
+
+ {{ form_widget(form) }} + {{ form_errors(form) }} +
+
+{% endspaceless %} +{%- endblock checkbox_radio_row %} + +{% block submit_row -%} +{% spaceless %} +
+
+
+ {{ form_widget(form) }} +
+
+{% endspaceless %} +{% endblock submit_row %} + +{% block reset_row -%} +{% spaceless %} +
+
+
+ {{ form_widget(form) }} +
+
+{% endspaceless %} +{% endblock reset_row %} + +{% block form_group_class -%} +col-sm-10 +{%- endblock form_group_class %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig new file mode 100644 index 000000000000..ef2035ab43f0 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -0,0 +1,245 @@ +{% use "form_div_layout.html.twig" %} + +{# Widgets #} + +{% block form_widget_simple -%} + {% if type is not defined or type not in ['file', 'hidden'] %} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%} + {% endif %} + {{- parent() -}} +{%- endblock form_widget_simple %} + +{% block textarea_widget -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} + {{- parent() -}} +{%- endblock textarea_widget %} + +{% block button_widget -%} + {% set attr = attr|merge({class: (attr.class|default('btn-default') ~ ' btn')|trim}) %} + {{- parent() -}} +{%- endblock %} + +{% block money_widget -%} +
+ {% set prepend = '{{' == money_pattern[0:2] %} + {% if not prepend %} + {{ money_pattern|replace({ '{{ widget }}':''}) }} + {% endif %} + {{- block('form_widget_simple') -}} + {% if prepend %} + {{ money_pattern|replace({ '{{ widget }}':''}) }} + {% endif %} +
+{%- endblock money_widget %} + +{% block percent_widget -%} +
+ {{- block('form_widget_simple') -}} + % +
+{%- endblock percent_widget %} + +{% block datetime_widget -%} + {% if widget == 'single_text' %} + {{- block('form_widget_simple') -}} + {% else -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} +
+ {{- form_errors(form.date) -}} + {{- form_errors(form.time) -}} + {{- form_widget(form.date, { datetime: true } ) -}} + {{- form_widget(form.time, { datetime: true } ) -}} +
+ {%- endif %} +{%- endblock datetime_widget %} + +{% block date_widget -%} + {% if widget == 'single_text' %} + {{- block('form_widget_simple') -}} + {% else -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} + {% if datetime is not defined or not datetime -%} +
+ {%- endif %} + {{- date_pattern|replace({ + '{{ year }}': form_widget(form.year), + '{{ month }}': form_widget(form.month), + '{{ day }}': form_widget(form.day), + })|raw -}} + {% if datetime is not defined or not datetime -%} +
+ {%- endif -%} + {% endif %} +{%- endblock date_widget %} + +{% block time_widget -%} + {% if widget == 'single_text' %} + {{- block('form_widget_simple') -}} + {% else -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) -%} + {% if datetime is not defined or false == datetime -%} +
+ {%- endif -%} + {{- form_widget(form.hour) }}{% if with_minutes %}:{{ form_widget(form.minute) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second) }}{% endif %} + {% if datetime is not defined or false == datetime -%} +
+ {%- endif -%} + {% endif %} +{%- endblock time_widget %} + +{% block choice_widget_collapsed -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} + {{- parent() -}} +{%- endblock %} + +{% block choice_widget_expanded -%} + {% if '-inline' in label_attr.class|default('') -%} + {%- for child in form %} + {{- form_widget(child, { + parent_label_class: label_attr.class|default(''), + translation_domain: choice_translation_domain, + }) -}} + {% endfor -%} + {%- else -%} +
+ {%- for child in form %} + {{- form_widget(child, { + parent_label_class: label_attr.class|default(''), + translation_domain: choice_translation_domain, + }) -}} + {% endfor -%} +
+ {%- endif %} +{%- endblock choice_widget_expanded %} + +{% block checkbox_widget -%} + {%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%} + {% if 'checkbox-inline' in parent_label_class %} + {{- form_label(form, null, { widget: parent() }) -}} + {% else -%} +
+ {{- form_label(form, null, { widget: parent() }) -}} +
+ {%- endif %} +{%- endblock checkbox_widget %} + +{% block radio_widget -%} + {%- set parent_label_class = parent_label_class|default(label_attr.class|default('')) -%} + {% if 'radio-inline' in parent_label_class %} + {{- form_label(form, null, { widget: parent() }) -}} + {% else -%} +
+ {{- form_label(form, null, { widget: parent() }) -}} +
+ {%- endif %} +{%- endblock radio_widget %} + +{# Labels #} + +{% block form_label -%} + {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' control-label')|trim}) -%} + {{- parent() -}} +{%- endblock form_label %} + +{% block choice_label -%} + {# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #} + {%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': ''})|trim}) -%} + {{- block('form_label') -}} +{% endblock %} + +{% block checkbox_label -%} + {{- block('checkbox_radio_label') -}} +{%- endblock checkbox_label %} + +{% block radio_label -%} + {{- block('checkbox_radio_label') -}} +{%- endblock radio_label %} + +{% block checkbox_radio_label %} + {# Do not display the label if widget is not defined in order to prevent double label rendering #} + {% if widget is defined %} + {% if required %} + {% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) %} + {% endif %} + {% if parent_label_class is defined %} + {% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ parent_label_class)|trim}) %} + {% endif %} + {% if label is not same as(false) and label is empty %} + {%- if label_format is not empty -%} + {% set label = label_format|replace({ + '%name%': name, + '%id%': id, + }) %} + {%- else -%} + {% set label = name|humanize %} + {%- endif -%} + {% endif %} + + {{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans({}, translation_domain)) -}} + + {% endif %} +{% endblock checkbox_radio_label %} + +{# Rows #} + +{% block form_row -%} +
+ {{- form_label(form) -}} + {{- form_widget(form) -}} + {{- form_errors(form) -}} +
+{%- endblock form_row %} + +{% block button_row -%} +
+ {{- form_widget(form) -}} +
+{%- endblock button_row %} + +{% block choice_row -%} + {% set force_error = true %} + {{- block('form_row') }} +{%- endblock choice_row %} + +{% block date_row -%} + {% set force_error = true %} + {{- block('form_row') }} +{%- endblock date_row %} + +{% block time_row -%} + {% set force_error = true %} + {{- block('form_row') }} +{%- endblock time_row %} + +{% block datetime_row -%} + {% set force_error = true %} + {{- block('form_row') }} +{%- endblock datetime_row %} + +{% block checkbox_row -%} +
+ {{- form_widget(form) -}} + {{- form_errors(form) -}} +
+{%- endblock checkbox_row %} + +{% block radio_row -%} +
+ {{- form_widget(form) -}} + {{- form_errors(form) -}} +
+{%- endblock radio_row %} + +{# Errors #} + +{% block form_errors -%} + {% if errors|length > 0 -%} + {% if form.parent %}{% else %}
{% endif %} +
    + {%- for error in errors -%} +
  • {{ error.message }}
  • + {%- endfor -%} +
+ {% if form.parent %}{% else %}
{% endif %} + {%- endif %} +{%- endblock form_errors %} 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 6a98f65f9e1e..4e5b9dd59830 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 @@ -44,20 +44,20 @@ {%- block choice_widget_expanded -%}
- {% for child in form %} + {%- for child in form %} {{- form_widget(child) -}} - {{- form_label(child) -}} - {% endfor %} + {{- form_label(child, null, {translation_domain: choice_translation_domain}) -}} + {% endfor -%}
{%- endblock choice_widget_expanded -%} {%- block choice_widget_collapsed -%} - {%- if required and empty_value is none and not empty_value_in_choices and not multiple -%} + {%- if required and placeholder is none and not placeholder_in_choices and not multiple and (attr.size is not defined or attr.size <= 1) -%} {% set required = false %} {%- endif -%} +
{%- if form_method != method -%} {%- endif -%} @@ -282,32 +297,75 @@ {%- endblock form_errors -%} {%- block form_rest -%} - {%- for child in form -%} - {%- if not child.rendered -%} - {{ form_row(child) }} - {%- endif -%} - {%- endfor -%} -{%- endblock form_rest -%} + {% for child in form -%} + {% if not child.rendered %} + {{- form_row(child) -}} + {% endif %} + {%- endfor %} +{% endblock form_rest %} {# Support #} {%- block form_rows -%} - {%- for child in form -%} - {{ form_row(child) }} - {%- endfor -%} + {% for child in form %} + {{- form_row(child) -}} + {% endfor %} {%- endblock form_rows -%} {%- block widget_attributes -%} - 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 -%} + {%- for attrname, attrvalue in attr if 'readonly' != attrname or not read_only -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}" + {%- elseif attrvalue is same as(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not same as(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} {%- endblock widget_attributes -%} {%- block widget_container_attributes -%} - {% 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 same as(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not same as(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} {%- endblock widget_container_attributes -%} {%- block button_attributes -%} - 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 same as(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not same as(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} {%- endblock button_attributes -%} + +{% block attributes -%} + {%- for attrname, attrvalue in attr -%} + {{- " " -}} + {%- if attrname in ['placeholder', 'title'] -%} + {{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}" + {%- elseif attrvalue is same as(true) -%} + {{- attrname }}="{{ attrname }}" + {%- elseif attrvalue is not same as(false) -%} + {{- attrname }}="{{ attrvalue }}" + {%- endif -%} + {%- endfor -%} +{%- endblock attributes -%} diff --git a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php new file mode 100644 index 000000000000..51a95bc4b57e --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php @@ -0,0 +1,150 @@ +appVariable = new AppVariable(); + } + + /** + * @dataProvider debugDataProvider + */ + public function testDebug($debugFlag) + { + $this->appVariable->setDebug($debugFlag); + + $this->assertEquals($debugFlag, $this->appVariable->getDebug()); + } + + public function debugDataProvider() + { + return array( + 'debug on' => array(true), + 'debug off' => array(false), + ); + } + + public function testEnvironment() + { + $this->appVariable->setEnvironment('dev'); + + $this->assertEquals('dev', $this->appVariable->getEnvironment()); + } + + public function testGetSession() + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request->method('getSession')->willReturn($session = new Session()); + + $this->setRequestStack($request); + + $this->assertEquals($session, $this->appVariable->getSession()); + } + + public function testGetSessionWithNoRequest() + { + $this->setRequestStack(null); + + $this->assertNull($this->appVariable->getSession()); + } + + public function testGetRequest() + { + $this->setRequestStack($request = new Request()); + + $this->assertEquals($request, $this->appVariable->getRequest()); + } + + public function testGetUser() + { + $this->setTokenStorage($user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface')); + + $this->assertEquals($user, $this->appVariable->getUser()); + } + + public function testGetUserWithUsernameAsTokenUser() + { + $this->setTokenStorage($user = 'username'); + + $this->assertNull($this->appVariable->getUser()); + } + + public function testGetUserWithNoToken() + { + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $this->appVariable->setTokenStorage($tokenStorage); + + $this->assertNull($this->appVariable->getUser()); + } + + /** + * @expectedException \RuntimeException + */ + public function testEnvironmentNotSet() + { + $this->appVariable->getEnvironment(); + } + + /** + * @expectedException \RuntimeException + */ + public function testDebugNotSet() + { + $this->appVariable->getDebug(); + } + + /** + * @expectedException \RuntimeException + */ + public function testGetUserWithTokenStorageNotSet() + { + $this->appVariable->getUser(); + } + + /** + * @expectedException \RuntimeException + */ + public function testGetRequestWithRequestStackNotSet() + { + $this->appVariable->getRequest(); + } + + /** + * @expectedException \RuntimeException + */ + public function testGetSessionWithRequestStackNotSet() + { + $this->appVariable->getSession(); + } + + protected function setRequestStack($request) + { + $requestStackMock = $this->getMock('Symfony\Component\HttpFoundation\RequestStack'); + $requestStackMock->method('getCurrentRequest')->willReturn($request); + + $this->appVariable->setRequestStack($requestStackMock); + } + + protected function setTokenStorage($user) + { + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $this->appVariable->setTokenStorage($tokenStorage); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $tokenStorage->method('getToken')->willReturn($token); + + $token->method('getUser')->willReturn($user); + } +} 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 000000000000..56c03574b6ee --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\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; + +class LintCommandTest extends \PHPUnit_Framework_TestCase +{ + private $files; + + public function testLintCorrectFile() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('{{ foo }}'); + + $ret = $tester->execute(array('filename' => array($filename)), array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false)); + + $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' => array($filename)), array('decorated' => false)); + + $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' => array($filename)), array('decorated' => false)); + } + + public function testLintFileCompileTimeException() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile("{{ 2|number_format(2, decimal_point='.', ',') }}"); + + $ret = $tester->execute(array('filename' => array($filename)), array('decorated' => false)); + + $this->assertEquals(1, $ret, 'Returns 1 in case of error'); + $this->assertRegExp('/^KO in /', $tester->getDisplay()); + } + + /** + * @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('lint:twig'); + + 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; + } + + protected function setUp() + { + $this->files = array(); + } + + protected function tearDown() + { + foreach ($this->files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php new file mode 100644 index 000000000000..474d8c33956b --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.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\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\AssetExtension; +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +class AssetExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @group legacy + */ + public function testLegacyGetAssetUrl() + { + $extension = $this->createExtension(new Package(new StaticVersionStrategy('22', '%s?version=%s'))); + + $this->assertEquals('me.png?version=42', $extension->getAssetUrl('me.png', null, false, '42')); + $this->assertEquals('http://localhost/me.png?version=22', $extension->getAssetUrl('me.png', null, true)); + $this->assertEquals('http://localhost/me.png?version=42', $extension->getAssetUrl('me.png', null, true, '42')); + } + + /** + * @group legacy + */ + public function testGetAssetUrlWithPackageSubClass() + { + $extension = $this->createExtension(new PathPackage('foo', new StaticVersionStrategy('22', '%s?version=%s'))); + + $this->assertEquals('/foo/me.png?version=42', $extension->getAssetUrl('me.png', null, false, 42)); + } + + /** + * @group legacy + */ + public function testGetAssetUrlWithEmptyVersionStrategy() + { + $extension = $this->createExtension(new PathPackage('foo', new EmptyVersionStrategy())); + + $this->assertEquals('/foo/me.png?42', $extension->getAssetUrl('me.png', null, false, 42)); + } + + private function createExtension(Package $package) + { + $foundationExtension = $this->getMockBuilder('Symfony\Bridge\Twig\Extension\HttpFoundationExtension')->disableOriginalConstructor()->getMock(); + $foundationExtension + ->expects($this->any()) + ->method('generateAbsoluteUrl') + ->will($this->returnCallback(function ($arg) { return 'http://localhost/'.$arg; })) + ; + + return new AssetExtension(new Packages($package), $foundationExtension); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.php new file mode 100644 index 000000000000..af93114e5ae5 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/DumpExtensionTest.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\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\DumpExtension; +use Symfony\Component\VarDumper\VarDumper; +use Symfony\Component\VarDumper\Cloner\VarCloner; + +class DumpExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getDumpTags + */ + public function testDumpTag($template, $debug, $expectedOutput, $expectedDumped) + { + $extension = new DumpExtension(new VarCloner()); + $twig = new \Twig_Environment(new \Twig_Loader_Array(array('template' => $template)), array( + 'debug' => $debug, + 'cache' => false, + 'optimizations' => 0, + )); + $twig->addExtension($extension); + + $dumped = null; + $exception = null; + $prevDumper = VarDumper::setHandler(function ($var) use (&$dumped) {$dumped = $var;}); + + try { + $this->assertEquals($expectedOutput, $twig->render('template')); + } catch (\Exception $exception) { + } + + VarDumper::setHandler($prevDumper); + + if (null !== $exception) { + throw $exception; + } + + $this->assertSame($expectedDumped, $dumped); + } + + public function getDumpTags() + { + return array( + array('A{% dump %}B', true, 'AB', array()), + array('A{% set foo="bar"%}B{% dump %}C', true, 'ABC', array('foo' => 'bar')), + array('A{% dump %}B', false, 'AB', null), + ); + } + + /** + * @dataProvider getDumpArgs + */ + public function testDump($context, $args, $expectedOutput, $debug = true) + { + $extension = new DumpExtension(new VarCloner()); + $twig = new \Twig_Environment($this->getMock('Twig_LoaderInterface'), array( + 'debug' => $debug, + 'cache' => false, + 'optimizations' => 0, + )); + + array_unshift($args, $context); + array_unshift($args, $twig); + + $dump = call_user_func_array(array($extension, 'dump'), $args); + + if ($debug) { + $this->assertStringStartsWith('\n"), + array( + array(), + array(123, 456), + "
123\n
\n" + ."
456\n
\n", + ), + array( + array('foo' => 'bar'), + array(), + "
array:1 [\n"
+                ."  \"foo\" => \"bar\"\n"
+                ."]\n"
+                ."
\n", + ), + ); + } +} 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 000000000000..0c7cc1bd4168 --- /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_Array(array('template' => $template)), array('debug' => true, 'cache' => false, 'autoescape' => 'html', '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 85102ea56ad8..1c8e5dcd490f 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, $throw = true) { - } -} else { - class StubFilesystemLoader extends \Twig_Loader_Filesystem - { - protected function findTemplate($name, $throw = true) - { - // strip away bundle name - $parts = explode(':', $name); + // strip away bundle name + $parts = explode(':', $name); - return parent::findTemplate(end($parts), $throw); - } + return parent::findTemplate(end($parts), $throw); } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/templates/form/custom_widgets.html.twig b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/templates/form/custom_widgets.html.twig index 12fd7c66d824..4eda8d76d373 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/templates/form/custom_widgets.html.twig +++ b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/templates/form/custom_widgets.html.twig @@ -6,11 +6,20 @@ {% endspaceless %} {% endblock _text_id_widget %} -{% block _name_entry_label %} +{% block _names_entry_label %} {% spaceless %} {% if label is empty %} {% set label = name|humanize %} {% endif %} {% endspaceless %} -{% endblock _name_entry_label %} +{% endblock _names_entry_label %} + +{% block _name_c_entry_label %} +{% spaceless %} + {% if label is empty %} + {% set label = name|humanize %} + {% endif %} + +{% endspaceless %} +{% endblock _name_c_entry_label %} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php new file mode 100644 index 000000000000..c7195ab57782 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Form\TwigRenderer; +use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Tests\AbstractBootstrap3HorizontalLayoutTest; + +class FormExtensionBootstrap3HorizontalLayoutTest extends AbstractBootstrap3HorizontalLayoutTest +{ + /** + * @var FormExtension + */ + protected $extension; + + protected $testableFeatures = array( + 'choice_attr', + ); + + protected function setUp() + { + parent::setUp(); + + $rendererEngine = new TwigRendererEngine(array( + 'bootstrap_3_horizontal_layout.html.twig', + 'custom_widgets.html.twig', + )); + $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')); + + $this->extension = new FormExtension($renderer); + + $loader = new StubFilesystemLoader(array( + __DIR__.'/../../Resources/views/Form', + __DIR__.'/Fixtures/templates/form', + )); + + $environment = new \Twig_Environment($loader, array('strict_variables' => true)); + $environment->addExtension(new TranslationExtension(new StubTranslator())); + $environment->addExtension($this->extension); + + $this->extension->initRuntime($environment); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->extension = null; + } + + protected function renderForm(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->renderBlock($view, 'form', $vars); + } + + protected function renderEnctype(FormView $view) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'enctype'); + } + + protected function renderLabel(FormView $view, $label = null, array $vars = array()) + { + if ($label !== null) { + $vars += array('label' => $label); + } + + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'label', $vars); + } + + protected function renderErrors(FormView $view) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'errors'); + } + + protected function renderWidget(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'widget', $vars); + } + + protected function renderRow(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'row', $vars); + } + + protected function renderRest(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'rest', $vars); + } + + protected function renderStart(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->renderBlock($view, 'form_start', $vars); + } + + protected function renderEnd(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->renderBlock($view, 'form_end', $vars); + } + + protected function setTheme(FormView $view, array $themes) + { + $this->extension->renderer->setTheme($view, $themes); + } + + public function testRange() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testRangeWithMinMaxValues() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testLabelWithoutTranslationOnButton() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testButtonlabelWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testAttributesNotTranslatedWhenTranslationDomainIsFalse() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php new file mode 100644 index 000000000000..406c1cef16bf --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Form\TwigRenderer; +use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Tests\AbstractBootstrap3LayoutTest; + +class FormExtensionBootstrap3LayoutTest extends AbstractBootstrap3LayoutTest +{ + /** + * @var FormExtension + */ + protected $extension; + + protected $testableFeatures = array( + 'choice_attr', + ); + + protected function setUp() + { + parent::setUp(); + + $rendererEngine = new TwigRendererEngine(array( + 'bootstrap_3_layout.html.twig', + 'custom_widgets.html.twig', + )); + $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')); + + $this->extension = new FormExtension($renderer); + + $loader = new StubFilesystemLoader(array( + __DIR__.'/../../Resources/views/Form', + __DIR__.'/Fixtures/templates/form', + )); + + $environment = new \Twig_Environment($loader, array('strict_variables' => true)); + $environment->addExtension(new TranslationExtension(new StubTranslator())); + $environment->addExtension($this->extension); + + $this->extension->initRuntime($environment); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->extension = null; + } + + protected function renderForm(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->renderBlock($view, 'form', $vars); + } + + protected function renderEnctype(FormView $view) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'enctype'); + } + + protected function renderLabel(FormView $view, $label = null, array $vars = array()) + { + if ($label !== null) { + $vars += array('label' => $label); + } + + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'label', $vars); + } + + protected function renderErrors(FormView $view) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'errors'); + } + + protected function renderWidget(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'widget', $vars); + } + + protected function renderRow(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'row', $vars); + } + + protected function renderRest(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->searchAndRenderBlock($view, 'rest', $vars); + } + + protected function renderStart(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->renderBlock($view, 'form_start', $vars); + } + + protected function renderEnd(FormView $view, array $vars = array()) + { + return (string) $this->extension->renderer->renderBlock($view, 'form_end', $vars); + } + + protected function setTheme(FormView $view, array $themes) + { + $this->extension->renderer->setTheme($view, $themes); + } + + public function testRange() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testRangeWithMinMaxValues() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testLabelWithoutTranslationOnButton() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testButtonlabelWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testAttributesNotTranslatedWhenTranslationDomainIsFalse() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index 2c5c7618a942..9e39ac35dc42 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -17,8 +17,8 @@ use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; +use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Extension\Core\View\ChoiceView; use Symfony\Component\Form\Tests\AbstractDivLayoutTest; class FormExtensionDivLayoutTest extends AbstractDivLayoutTest @@ -28,6 +28,10 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest */ protected $extension; + protected $testableFeatures = array( + 'choice_attr', + ); + protected function setUp() { parent::setUp(); @@ -36,7 +40,7 @@ protected function setUp() 'form_div_layout.html.twig', 'custom_widgets.html.twig', )); - $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface')); + $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')); $this->extension = new FormExtension($renderer); @@ -204,4 +208,39 @@ public static function themeInheritanceProvider() array(array('parent_label.html.twig'), array('child_label.html.twig')), ); } + + public function testRange() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testRangeWithMinMaxValues() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testLabelWithoutTranslationOnButton() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testButtonlabelWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testAttributesNotTranslatedWhenTranslationDomainIsFalse() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php index 22331781dabf..7fa88eef00c7 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php @@ -27,6 +27,10 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest */ protected $extension; + protected $testableFeatures = array( + 'choice_attr', + ); + protected function setUp() { parent::setUp(); @@ -35,7 +39,7 @@ protected function setUp() 'form_table_layout.html.twig', 'custom_widgets.html.twig', )); - $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface')); + $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')); $this->extension = new FormExtension($renderer); @@ -112,4 +116,39 @@ protected function setTheme(FormView $view, array $themes) { $this->extension->renderer->setTheme($view, $themes); } + + public function testRange() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testRangeWithMinMaxValues() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testLabelWithoutTranslationOnButton() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testButtonlabelWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testAttributesNotTranslatedWhenTranslationDomainIsFalse() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php new file mode 100644 index 000000000000..339d43d7c6bd --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.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\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\HttpFoundationExtension; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\RequestContext; + +class HttpFoundationExtensionTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getGenerateAbsoluteUrlData() + */ + public function testGenerateAbsoluteUrl($expected, $path, $pathinfo) + { + $stack = new RequestStack(); + $stack->push(Request::create($pathinfo)); + $extension = new HttpFoundationExtension($stack); + + $this->assertEquals($expected, $extension->generateAbsoluteUrl($path)); + } + + public function getGenerateAbsoluteUrlData() + { + return array( + array('http://localhost/foo.png', '/foo.png', '/foo/bar.html'), + array('http://localhost/foo/foo.png', 'foo.png', '/foo/bar.html'), + array('http://localhost/foo/foo.png', 'foo.png', '/foo/bar'), + array('http://localhost/foo/bar/foo.png', 'foo.png', '/foo/bar/'), + + array('http://example.com/baz', 'http://example.com/baz', '/'), + array('https://example.com/baz', 'https://example.com/baz', '/'), + array('//example.com/baz', '//example.com/baz', '/'), + ); + } + + /** + * @dataProvider getGenerateAbsoluteUrlRequestContextData + */ + public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected) + { + if (!class_exists('Symfony\Component\Routing\RequestContext')) { + $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); + } + + $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $extension = new HttpFoundationExtension(new RequestStack(), $requestContext); + + $this->assertEquals($expected, $extension->generateAbsoluteUrl($path)); + } + + /** + * @dataProvider getGenerateAbsoluteUrlRequestContextData + */ + public function testGenerateAbsoluteUrlWithoutRequestAndRequestContext($path) + { + if (!class_exists('Symfony\Component\Routing\RequestContext')) { + $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); + } + + $extension = new HttpFoundationExtension(new RequestStack()); + + $this->assertEquals($path, $extension->generateAbsoluteUrl($path)); + } + + public function getGenerateAbsoluteUrlRequestContextData() + { + return array( + array('/foo.png', '/foo', 'localhost', 'http', 80, 443, 'http://localhost/foo.png'), + array('foo.png', '/foo', 'localhost', 'http', 80, 443, 'http://localhost/foo/foo.png'), + array('foo.png', '/foo/bar/', 'localhost', 'http', 80, 443, 'http://localhost/foo/bar/foo.png'), + array('/foo.png', '/foo', 'localhost', 'https', 80, 443, 'https://localhost/foo.png'), + array('foo.png', '/foo', 'localhost', 'https', 80, 443, 'https://localhost/foo/foo.png'), + array('foo.png', '/foo/bar/', 'localhost', 'https', 80, 443, 'https://localhost/foo/bar/foo.png'), + array('/foo.png', '/foo', 'localhost', 'http', 443, 80, 'http://localhost:443/foo.png'), + array('/foo.png', '/foo', 'localhost', 'https', 443, 80, 'https://localhost:80/foo.png'), + ); + } + + public function testGenerateAbsoluteUrlWithScriptFileName() + { + $request = Request::create('http://localhost/app/web/app_dev.php'); + $request->server->set('SCRIPT_FILENAME', '/var/www/app/web/app_dev.php'); + + $stack = new RequestStack(); + $stack->push($request); + $extension = new HttpFoundationExtension($stack); + + $this->assertEquals( + 'http://localhost/app/web/bundles/framework/css/structure.css', + $extension->generateAbsoluteUrl('/app/web/bundles/framework/css/structure.css') + ); + } + + /** + * @dataProvider getGenerateRelativePathData() + */ + public function testGenerateRelativePath($expected, $path, $pathinfo) + { + if (!method_exists('Symfony\Component\HttpFoundation\Request', 'getRelativeUriForPath')) { + $this->markTestSkipped('Your version of Symfony HttpFoundation is too old.'); + } + + $stack = new RequestStack(); + $stack->push(Request::create($pathinfo)); + $extension = new HttpFoundationExtension($stack); + + $this->assertEquals($expected, $extension->generateRelativePath($path)); + } + + public function getGenerateRelativePathData() + { + return array( + array('../foo.png', '/foo.png', '/foo/bar.html'), + array('../baz/foo.png', '/baz/foo.png', '/foo/bar.html'), + array('baz/foo.png', 'baz/foo.png', '/foo/bar.html'), + + array('http://example.com/baz', 'http://example.com/baz', '/'), + array('https://example.com/baz', 'https://example.com/baz', '/'), + array('//example.com/baz', '//example.com/baz', '/'), + ); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index ce22481921f5..48bebdc13f8f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -12,24 +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 { /** * @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->renderTemplate($kernel); + $this->assertEquals('html', $response); + } + + 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) @@ -38,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 1facf8c26595..1c1ac645c89f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php @@ -12,9 +12,8 @@ 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 { /** * @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 000000000000..8c0cdfe69b62 --- /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_Array(array('template' => $template)), array('debug' => true, 'cache' => false, 'autoescape' => 'html', '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 e8826f66a660..a02edc777a1b 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -15,9 +15,8 @@ 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 { public function testEscaping() { diff --git a/src/Symfony/Bridge/Twig/Tests/Fixtures/extractor/with_translations.html.twig b/src/Symfony/Bridge/Twig/Tests/Fixtures/extractor/with_translations.html.twig new file mode 100644 index 000000000000..8cdb42d293c2 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Fixtures/extractor/with_translations.html.twig @@ -0,0 +1 @@ +

{{ 'Hi!'|trans }}

diff --git a/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php new file mode 100644 index 000000000000..5d8ea6f0868a --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Node; + +use Symfony\Bridge\Twig\Node\DumpNode; + +class DumpNodeTest extends \PHPUnit_Framework_TestCase +{ + public function testNoVar() + { + $node = new DumpNode('bar', null, 7); + + $env = new \Twig_Environment($this->getMock('Twig_LoaderInterface')); + $compiler = new \Twig_Compiler($env); + + $expected = <<<'EOTXT' +if ($this->env->isDebug()) { + $barvars = array(); + foreach ($context as $barkey => $barval) { + if (!$barval instanceof \Twig_Template) { + $barvars[$barkey] = $barval; + } + } + // line 7 + \Symfony\Component\VarDumper\VarDumper::dump($barvars); +} + +EOTXT; + + $this->assertSame($expected, $compiler->compile($node)->getSource()); + } + + public function testIndented() + { + $node = new DumpNode('bar', null, 7); + + $env = new \Twig_Environment($this->getMock('Twig_LoaderInterface')); + $compiler = new \Twig_Compiler($env); + + $expected = <<<'EOTXT' + if ($this->env->isDebug()) { + $barvars = array(); + foreach ($context as $barkey => $barval) { + if (!$barval instanceof \Twig_Template) { + $barvars[$barkey] = $barval; + } + } + // line 7 + \Symfony\Component\VarDumper\VarDumper::dump($barvars); + } + +EOTXT; + + $this->assertSame($expected, $compiler->compile($node, 1)->getSource()); + } + + public function testOneVar() + { + $vars = new \Twig_Node(array( + new \Twig_Node_Expression_Name('foo', 7), + )); + $node = new DumpNode('bar', $vars, 7); + + $env = new \Twig_Environment($this->getMock('Twig_LoaderInterface')); + $compiler = new \Twig_Compiler($env); + + $expected = <<<'EOTXT' +if ($this->env->isDebug()) { + // line 7 + \Symfony\Component\VarDumper\VarDumper::dump(%foo%); +} + +EOTXT; + $expected = preg_replace('/%(.*?)%/', PHP_VERSION_ID >= 50400 ? '(isset($context["$1"]) ? $context["$1"] : null)' : '$this->getContext($context, "$1")', $expected); + + $this->assertSame($expected, $compiler->compile($node)->getSource()); + } + + public function testMultiVars() + { + $vars = new \Twig_Node(array( + new \Twig_Node_Expression_Name('foo', 7), + new \Twig_Node_Expression_Name('bar', 7), + )); + $node = new DumpNode('bar', $vars, 7); + + $env = new \Twig_Environment($this->getMock('Twig_LoaderInterface')); + $compiler = new \Twig_Compiler($env); + + $expected = <<<'EOTXT' +if ($this->env->isDebug()) { + // line 7 + \Symfony\Component\VarDumper\VarDumper::dump(array( + "foo" => %foo%, + "bar" => %bar%, + )); +} + +EOTXT; + $expected = preg_replace('/%(.*?)%/', PHP_VERSION_ID >= 50400 ? '(isset($context["$1"]) ? $context["$1"] : null)' : '$this->getContext($context, "$1")', $expected); + + $this->assertSame($expected, $compiler->compile($node)->getSource()); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php index 367ef4b564b8..0fadfdabc6e7 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 ce11519fa8a3..05b28c947dc7 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 bcae5919b597..aa9d204e5606 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 b0bb57058662..f9cf08bc2801 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 2190345c23af..16736031e087 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 94b7b3dd1256..2986cd1258d6 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 72e9d95f06cf..5e7c4ce5216d 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -14,9 +14,8 @@ 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 { /** * @dataProvider getExtractData @@ -74,14 +73,65 @@ public function getExtractData() /** * @expectedException \Twig_Error - * @expectedExceptionMessageRegExp /Unclosed "block" in "extractor(\/|\\)syntax_error\.twig" at line 1/ + * @expectedExceptionMessageRegExp /Unclosed "block" in ".*extractor(\/|\\)syntax_error\.twig" at line 1/ + * @dataProvider resourcesWithSyntaxErrorsProvider */ - public function testExtractSyntaxError() + public function testExtractSyntaxError($resources) { $twig = new \Twig_Environment($this->getMock('Twig_LoaderInterface')); $twig->addExtension(new TranslationExtension($this->getMock('Symfony\Component\Translation\TranslatorInterface'))); $extractor = new TwigExtractor($twig); - $extractor->extract(__DIR__.'/../Fixtures', new MessageCatalogue('en')); + $extractor->extract($resources, new MessageCatalogue('en')); + } + + /** + * @return array + */ + public function resourcesWithSyntaxErrorsProvider() + { + return array( + array(__DIR__.'/../Fixtures'), + array(__DIR__.'/../Fixtures/extractor/syntax_error.twig'), + array(new \SplFileInfo(__DIR__.'/../Fixtures/extractor/syntax_error.twig')), + ); + } + + /** + * @dataProvider resourceProvider + */ + public function testExtractWithFiles($resource) + { + $loader = new \Twig_Loader_Array(array()); + $twig = new \Twig_Environment($loader, array( + 'strict_variables' => true, + 'debug' => true, + 'cache' => false, + 'autoescape' => false, + )); + $twig->addExtension(new TranslationExtension($this->getMock('Symfony\Component\Translation\TranslatorInterface'))); + + $extractor = new TwigExtractor($twig); + $catalogue = new MessageCatalogue('en'); + $extractor->extract($resource, $catalogue); + + $this->assertTrue($catalogue->has('Hi!', 'messages')); + $this->assertEquals('Hi!', $catalogue->get('Hi!', 'messages')); + } + + /** + * @return array + */ + public function resourceProvider() + { + $directory = __DIR__.'/../Fixtures/extractor/'; + + return array( + array($directory.'with_translations.html.twig'), + array(array($directory.'with_translations.html.twig')), + array(array(new \SplFileInfo($directory.'with_translations.html.twig'))), + array(new \ArrayObject(array($directory.'with_translations.html.twig'))), + array(new \ArrayObject(array(new \SplFileInfo($directory.'with_translations.html.twig')))), + ); } } diff --git a/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php b/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php index 1e6a3c493367..e7047c354d08 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/DumpTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php new file mode 100644 index 000000000000..269ead64f5d4 --- /dev/null +++ b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php @@ -0,0 +1,51 @@ + + * + * 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\DumpNode; + +/** + * Token Parser for the 'dump' tag. + * + * Dump variables with: + *
+ *  {% dump %}
+ *  {% dump foo %}
+ *  {% dump foo, bar %}
+ * 
+ * + * @author Julien Galenski + */ +class DumpTokenParser extends \Twig_TokenParser +{ + /** + * {@inheritdoc} + */ + public function parse(\Twig_Token $token) + { + $values = null; + if (!$this->parser->getStream()->test(\Twig_Token::BLOCK_END_TYPE)) { + $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); + } + $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); + + return new DumpNode($this->parser->getVarName(), $values, $token->getLine(), $this->getTag()); + } + + /** + * {@inheritdoc} + */ + public function getTag() + { + return 'dump'; + } +} diff --git a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php new file mode 100644 index 000000000000..2983e4cb6b03 --- /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 4e8b817f77aa..d892fe7303e7 100644 --- a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php +++ b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php @@ -12,6 +12,8 @@ namespace Symfony\Bridge\Twig\Translation; use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Symfony\Component\Translation\Extractor\AbstractFileExtractor; use Symfony\Component\Translation\Extractor\ExtractorInterface; use Symfony\Component\Translation\MessageCatalogue; @@ -21,7 +23,7 @@ * @author Michel Salib * @author Fabien Potencier */ -class TwigExtractor implements ExtractorInterface +class TwigExtractor extends AbstractFileExtractor implements ExtractorInterface { /** * Default domain for found messages. @@ -52,16 +54,18 @@ public function __construct(\Twig_Environment $twig) /** * {@inheritdoc} */ - public function extract($directory, MessageCatalogue $catalogue) + public function extract($resource, MessageCatalogue $catalogue) { - // load any existing translation files - $finder = new Finder(); - $files = $finder->files()->name('*.twig')->in($directory); + $files = $this->extractFiles($resource); foreach ($files as $file) { try { $this->extractTemplate(file_get_contents($file->getPathname()), $catalogue); } catch (\Twig_Error $e) { - $e->setTemplateFile($file->getRelativePathname()); + if ($file instanceof SplFileInfo) { + $e->setTemplateFile($file->getRelativePathname()); + } elseif ($file instanceof \SplFileInfo) { + $e->setTemplateFile($file->getRealPath()); + } throw $e; } @@ -89,4 +93,26 @@ protected function extractTemplate($template, MessageCatalogue $catalogue) $visitor->disable(); } + + /** + * @param string $file + * + * @return bool + */ + protected function canBeExtracted($file) + { + return $this->isFile($file) && 'twig' === pathinfo($file, PATHINFO_EXTENSION); + } + + /** + * @param string|array $directory + * + * @return array + */ + protected function extractFromDirectory($directory) + { + $finder = new Finder(); + + return $finder->files()->name('*.twig')->in($directory); + } } diff --git a/src/Symfony/Bridge/Twig/TwigEngine.php b/src/Symfony/Bridge/Twig/TwigEngine.php index 9483a9d05ad1..3e3257e7fa0f 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 bool 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 bool 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 b089cb6c1e8d..1c0cc2f10568 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -16,42 +16,50 @@ } ], "require": { - "php": ">=5.3.3", + "php": ">=5.3.9", "twig/twig": "~1.23|~2.0" }, "require-dev": { + "symfony/asset": "~2.7", "symfony/finder": "~2.3", - "symfony/form": "~2.3.31", + "symfony/form": "~2.7.11|~2.8.4", "symfony/http-kernel": "~2.3", "symfony/intl": "~2.3", "symfony/routing": "~2.2", "symfony/templating": "~2.1", - "symfony/translation": "~2.2", + "symfony/translation": "~2.7", "symfony/yaml": "~2.0,>=2.0.5", - "symfony/security-acl": "~2.0,>=2.0.5", - "symfony/security": "~2.0,>=2.0.5" + "symfony/security": "~2.6", + "symfony/security-acl": "~2.6", + "symfony/stopwatch": "~2.2", + "symfony/console": "~2.7", + "symfony/var-dumper": "~2.6", + "symfony/expression-language": "~2.4" }, "suggest": { "symfony/finder": "", - "symfony/form": "", - "symfony/http-kernel": "", - "symfony/routing": "", - "symfony/templating": "", - "symfony/translation": "", - "symfony/yaml": "", - "symfony/security": "" + "symfony/asset": "For using the AssetExtension", + "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/var-dumper": "For using the DumpExtension", + "symfony/expression-language": "For using the ExpressionExtension" }, "autoload": { - "psr-0": { "Symfony\\Bridge\\Twig\\": "" }, + "psr-4": { "Symfony\\Bridge\\Twig\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bridge/Twig", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bundle/DebugBundle/.gitignore b/src/Symfony/Bundle/DebugBundle/.gitignore new file mode 100644 index 000000000000..c49a5d8df5c6 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Bundle/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php new file mode 100644 index 000000000000..3aa536dba786 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.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\Bundle\DebugBundle; + +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\DumpDataCollectorPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\VarDumper\VarDumper; + +/** + * @author Nicolas Grekas + */ +class DebugBundle extends Bundle +{ + public function boot() + { + if ($this->container->getParameter('kernel.debug')) { + $container = $this->container; + + // This code is here to lazy load the dump stack. This default + // configuration for CLI mode is overridden in HTTP mode on + // 'kernel.request' event + VarDumper::setHandler(function ($var) use ($container) { + $dumper = $container->get('var_dumper.cli_dumper'); + $cloner = $container->get('var_dumper.cloner'); + $handler = function ($var) use ($dumper, $cloner) { + $dumper->dump($cloner->cloneVar($var)); + }; + VarDumper::setHandler($handler); + $handler($var); + }); + } + } + + /** + * {@inheritdoc} + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new DumpDataCollectorPass()); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/DumpDataCollectorPass.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/DumpDataCollectorPass.php new file mode 100644 index 000000000000..4dcb3908e999 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/DumpDataCollectorPass.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\Bundle\DebugBundle\DependencyInjection\Compiler; + +use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Registers the file link format for the {@link \Symfony\Component\HttpKernel\DataCollector\DumpDataCollector}. + * + * @author Christian Flothmann + */ +class DumpDataCollectorPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('data_collector.dump')) { + return; + } + + $definition = $container->getDefinition('data_collector.dump'); + + if ($container->hasParameter('templating.helper.code.file_link_format')) { + $definition->replaceArgument(1, $container->getParameter('templating.helper.code.file_link_format')); + } + + if (!$container->hasParameter('web_profiler.debug_toolbar.mode') || WebDebugToolbarListener::DISABLED === $container->getParameter('web_profiler.debug_toolbar.mode')) { + $definition->replaceArgument(3, null); + } + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php new file mode 100644 index 000000000000..5761e62a72e1 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.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\Bundle\DebugBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * DebugExtension configuration structure. + * + * @author Nicolas Grekas + */ +class Configuration implements ConfigurationInterface +{ + /** + * {@inheritdoc} + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('debug'); + + $rootNode + ->children() + ->integerNode('max_items') + ->info('Max number of displayed items past the first level, -1 means no limit') + ->min(-1) + ->defaultValue(2500) + ->end() + ->integerNode('max_string_length') + ->info('Max length of displayed strings, -1 means no limit') + ->min(-1) + ->defaultValue(-1) + ->end() + ->scalarNode('dump_destination') + ->info('A stream URL where dumps should be written to') + ->example('php://stderr') + ->defaultNull() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php new file mode 100644 index 000000000000..ce6d1b7c677e --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\DebugBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Reference; + +/** + * DebugExtension. + * + * @author Nicolas Grekas + */ +class DebugExtension extends Extension +{ + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.xml'); + + $container->getDefinition('var_dumper.cloner') + ->addMethodCall('setMaxItems', array($config['max_items'])) + ->addMethodCall('setMaxString', array($config['max_string_length'])); + + if (null !== $config['dump_destination']) { + $container->getDefinition('var_dumper.cli_dumper') + ->replaceArgument(0, $config['dump_destination']) + ; + $container->getDefinition('data_collector.dump') + ->replaceArgument(4, new Reference('var_dumper.cli_dumper')) + ; + } + } + + /** + * {@inheritdoc} + */ + public function getXsdValidationBasePath() + { + return __DIR__.'/../Resources/config/schema'; + } + + /** + * {@inheritdoc} + */ + public function getNamespace() + { + return 'http://symfony.com/schema/dic/debug'; + } +} diff --git a/src/Symfony/Bundle/DebugBundle/LICENSE b/src/Symfony/Bundle/DebugBundle/LICENSE new file mode 100644 index 000000000000..39fa189d2b5f --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd b/src/Symfony/Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd new file mode 100644 index 000000000000..a582ff8b2b70 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/schema/debug-1.0.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml new file mode 100644 index 000000000000..16e27a22c584 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + null + %kernel.charset% + + null + + + + + + + + + + + null + %kernel.charset% + + + + diff --git a/src/Symfony/Bundle/DebugBundle/Resources/views/Profiler/dump.html.twig b/src/Symfony/Bundle/DebugBundle/Resources/views/Profiler/dump.html.twig new file mode 100644 index 000000000000..e7a8245fb3b0 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/views/Profiler/dump.html.twig @@ -0,0 +1,102 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set dumps_count = collector.dumpsCount %} + + {% if dumps_count %} + {% set icon %} + + {{ dumps_count }} + {% endset %} + + {% set text %} +
+ dump() +
+ {% for dump in collector.getDumps('html') %} +
+ in + {% if dump.file %} + {% set link = dump.file|file_link(dump.line) %} + {% if link %} + {{ dump.name }} + {% else %} + {{ dump.name }} + {% endif %} + {% else %} + {{ dump.name }} + {% endif %} + line {{ dump.line }}: + {{ dump.data|raw }} +
+ {% endfor %} + + {% endset %} + + {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': true } %} + {% endif %} +{% endblock %} + +{% block menu %} + + + {{- "" -}} + + {{- "" -}} + + dump() + + {{ collector.dumpsCount }} + + +{% endblock %} + +{% block panel %} +

dump()

+ + + + {% if collector.dumpsCount %} +
    + {% for dump in collector.getDumps('html') %} +
  • + in + {% if dump.line %} + {% set link = dump.file|file_link(dump.line) %} + {% if link %} + {{ dump.name }} + {% else %} + {{ dump.name }} + {% endif %} + {% else %} + {{ dump.name }} + {% endif %} + line {{ dump.line }}: + + + {% if dump.fileExcerpt %}{{ dump.fileExcerpt|raw }}{% else %}{{ dump.file|file_excerpt(dump.line) }}{% endif %} + + + {{ dump.data|raw }} +
  • + {% endfor %} +
+ {% else %} +

+ No dumped variable +

+ {% endif %} +{% endblock %} diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/DumpDataCollectorPassTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/DumpDataCollectorPassTest.php new file mode 100644 index 000000000000..6500a85a8430 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/DumpDataCollectorPassTest.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\Bundle\DebugBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\DumpDataCollectorPass; +use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\HttpFoundation\RequestStack; + +class DumpDataCollectorPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcessWithFileLinkFormatParameter() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new DumpDataCollectorPass()); + $container->setParameter('templating.helper.code.file_link_format', 'file-link-format'); + + $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, null)); + $container->setDefinition('data_collector.dump', $definition); + + $container->compile(); + + $this->assertSame('file-link-format', $definition->getArgument(1)); + } + + public function testProcessWithoutFileLinkFormatParameter() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new DumpDataCollectorPass()); + + $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, null)); + $container->setDefinition('data_collector.dump', $definition); + + $container->compile(); + + $this->assertNull($definition->getArgument(1)); + } + + public function testProcessWithToolbarEnabled() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new DumpDataCollectorPass()); + $requestStack = new RequestStack(); + + $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, $requestStack)); + $container->setDefinition('data_collector.dump', $definition); + $container->setParameter('web_profiler.debug_toolbar.mode', WebDebugToolbarListener::ENABLED); + + $container->compile(); + + $this->assertSame($requestStack, $definition->getArgument(3)); + } + + public function testProcessWithToolbarDisabled() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new DumpDataCollectorPass()); + + $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, new RequestStack())); + $container->setDefinition('data_collector.dump', $definition); + $container->setParameter('web_profiler.debug_toolbar.mode', WebDebugToolbarListener::DISABLED); + + $container->compile(); + + $this->assertNull($definition->getArgument(3)); + } + + public function testProcessWithoutToolbar() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new DumpDataCollectorPass()); + + $definition = new Definition('Symfony\Component\HttpKernel\DataCollector\DumpDataCollector', array(null, null, null, new RequestStack())); + $container->setDefinition('data_collector.dump', $definition); + + $container->compile(); + + $this->assertNull($definition->getArgument(3)); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php new file mode 100644 index 000000000000..3efb196cc9a3 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.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\Bundle\DebugBundle\Tests\DependencyInjection; + +use Symfony\Bundle\DebugBundle\DependencyInjection\DebugExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + +class DebugExtensionTest extends \PHPUnit_Framework_TestCase +{ + public function testLoadWithoutConfiguration() + { + $container = $this->createContainer(); + $container->registerExtension(new DebugExtension()); + $container->loadFromExtension('debug', array()); + $this->compileContainer($container); + + $expectedTags = array( + array( + 'id' => 'dump', + 'template' => '@Debug/Profiler/dump.html.twig', + ), + ); + + $this->assertSame($expectedTags, $container->getDefinition('data_collector.dump')->getTag('data_collector')); + } + + private function createContainer() + { + $container = new ContainerBuilder(new ParameterBag(array( + 'kernel.cache_dir' => __DIR__, + 'kernel.root_dir' => __DIR__.'/Fixtures', + 'kernel.charset' => 'UTF-8', + 'kernel.debug' => true, + 'kernel.bundles' => array('DebugBundle' => 'Symfony\\Bundle\\DebugBundle\\DebugBundle'), + ))); + + return $container; + } + + private function compileContainer(ContainerBuilder $container) + { + $container->getCompilerPassConfig()->setOptimizationPasses(array()); + $container->getCompilerPassConfig()->setRemovingPasses(array()); + $container->compile(); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json new file mode 100644 index 000000000000..d4cba7fc2410 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -0,0 +1,45 @@ +{ + "name": "symfony/debug-bundle", + "type": "symfony-bundle", + "description": "Symfony DebugBundle", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.9", + "symfony/http-kernel": "~2.6", + "symfony/twig-bridge": "~2.6", + "symfony/var-dumper": "~2.6" + }, + "require-dev": { + "symfony/config": "~2.3", + "symfony/dependency-injection": "~2.3", + "symfony/web-profiler-bundle": "~2.3" + }, + "suggest": { + "symfony/config": "For service container configuration", + "symfony/dependency-injection": "For using as a service from the container" + }, + "autoload": { + "psr-4": { "Symfony\\Bundle\\DebugBundle\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + } +} diff --git a/src/Symfony/Bridge/Propel1/phpunit.xml.dist b/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist similarity index 92% rename from src/Symfony/Bridge/Propel1/phpunit.xml.dist rename to src/Symfony/Bundle/DebugBundle/phpunit.xml.dist index d6d959c7b01c..90ec0a5dba51 100644 --- a/src/Symfony/Bridge/Propel1/phpunit.xml.dist +++ b/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist @@ -11,7 +11,7 @@ - + ./Tests/ diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 11ab65cff483..ad8cb2e6e988 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,42 @@ CHANGELOG ========= +2.7.0 +----- + + * Added possibility to extract translation messages from a file or files besides extracting from a directory + * Added `TranslationsCacheWarmer` to create catalogues at warmup + +2.6.0 +----- + + * Added helper commands (`server:start`, `server:stop` and `server:status`) to control the built-in web + server in the background + * Added `Controller::isCsrfTokenValid` helper + * Added configuration for the PropertyAccess component + * Added `Controller::redirectToRoute` helper + * Added `Controller::addFlash` helper + * Added `Controller::isGranted` helper + * Added `Controller::denyAccessUnlessGranted` helper + * Deprecated `app.security` in twig as `app.user` and `is_granted()` are already available + +2.5.0 +----- + + * Added `translation:debug` command + * Added `--no-backup` option to `translation:update` command + * Added `config:debug` command + * 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/CacheWarmer/TranslationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php new file mode 100644 index 000000000000..223f0216ba9a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.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\CacheWarmer; + +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * Generates the catalogues for translations. + * + * @author Xavier Leune + */ +class TranslationsCacheWarmer implements CacheWarmerInterface +{ + private $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + if ($this->translator instanceof WarmableInterface) { + $this->translator->warmUp($cacheDir); + } + } + + /** + * {@inheritdoc} + */ + public function isOptional() + { + return true; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Client.php b/src/Symfony/Bundle/FrameworkBundle/Client.php index 6de49b310415..04fbd3ab0fab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Client.php +++ b/src/Symfony/Bundle/FrameworkBundle/Client.php @@ -29,6 +29,7 @@ class Client extends BaseClient { private $hasPerformedRequest = false; private $profiler = false; + private $reboot = true; /** * {@inheritdoc} @@ -84,6 +85,25 @@ public function enableProfiler() } } + /** + * Disables kernel reboot between requests. + * + * By default, the Client reboots the Kernel for each request. This method + * allows to keep the same kernel across requests. + */ + public function disableReboot() + { + $this->reboot = false; + } + + /** + * Enables kernel reboot between requests. + */ + public function enableReboot() + { + $this->reboot = true; + } + /** * {@inheritdoc} * @@ -95,7 +115,7 @@ protected function doRequest($request) { // avoid shutting down the Kernel if no request has been performed yet // WebTestCase::createClient() boots the Kernel but do not handle a request - if ($this->hasPerformedRequest) { + if ($this->hasPerformedRequest && $this->reboot) { $this->kernel->shutdown(); } else { $this->hasPerformedRequest = true; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php new file mode 100644 index 000000000000..aceaa0cbdc6d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\StyleInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; + +/** + * A console command for dumping available configuration reference. + * + * @author Kevin Bond + * @author Wouter J + * @author Grégoire Pineau + */ +abstract class AbstractConfigCommand extends ContainerDebugCommand +{ + protected function listBundles(OutputInterface $output) + { + $headers = array('Bundle name', 'Extension alias'); + $rows = array(); + + $bundles = $this->getContainer()->get('kernel')->getBundles(); + usort($bundles, function ($bundleA, $bundleB) { + return strcmp($bundleA->getName(), $bundleB->getName()); + }); + + foreach ($bundles as $bundle) { + $extension = $bundle->getContainerExtension(); + $rows[] = array($bundle->getName(), $extension ? $extension->getAlias() : ''); + } + + $message = 'Available registered bundles with their extension alias if available:'; + if ($output instanceof StyleInterface) { + $output->writeln(' '.$message); + $output->table($headers, $rows); + } else { + $output->writeln($message); + $table = new Table($output); + $table->setHeaders($headers)->setRows($rows)->render($output); + } + } + + protected function findExtension($name) + { + $bundles = $this->initializeBundles(); + foreach ($bundles as $bundle) { + if ($name === $bundle->getName()) { + if (!$bundle->getContainerExtension()) { + throw new \LogicException(sprintf('Bundle "%s" does not have a container extension.', $name)); + } + + return $bundle->getContainerExtension(); + } + + $extension = $bundle->getContainerExtension(); + if ($extension && $name === $extension->getAlias()) { + return $extension; + } + } + + if ('Bundle' !== substr($name, -6)) { + $message = sprintf('No extensions with configuration available for "%s"', $name); + } else { + $message = sprintf('No extension with alias "%s" is enabled', $name); + } + + throw new \LogicException($message); + } + + public function validateConfiguration(ExtensionInterface $extension, $configuration) + { + if (!$configuration) { + throw new \LogicException(sprintf('The extension with alias "%s" does not have its getConfiguration() method setup', $extension->getAlias())); + } + + if (!$configuration instanceof ConfigurationInterface) { + throw new \LogicException(sprintf('Configuration class "%s" should implement ConfigurationInterface in order to be dumpable', get_class($configuration))); + } + } + + private function initializeBundles() + { + // Re-build bundle manually to initialize DI extensions that can be extended by other bundles in their build() method + // as this method is not called when the container is loaded from the cache. + $container = $this->getContainerBuilder(); + $bundles = $this->getContainer()->get('kernel')->registerBundles(); + foreach ($bundles as $bundle) { + if ($extension = $bundle->getContainerExtension()) { + $container->registerExtension($extension); + } + } + + foreach ($bundles as $bundle) { + $bundle->build($container); + } + + return $bundles; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php index 643368976ffb..94daa82cc9a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Finder\Finder; /** @@ -47,7 +48,7 @@ protected function configure() "Resources/public" directory of each bundle will be copied into it. To create a symlink to each bundle instead of copying its assets, use the ---symlink option: +--symlink option (will fall back to hard copies when symbolic links aren't possible: php %command.full_name% web --symlink @@ -73,17 +74,20 @@ protected function execute(InputInterface $input, OutputInterface $output) throw new \InvalidArgumentException(sprintf('The target directory "%s" does not exist.', $input->getArgument('target'))); } - if (!function_exists('symlink') && $input->getOption('symlink')) { - throw new \InvalidArgumentException('The symlink() function is not available on your system. You need to install the assets without the --symlink option.'); - } - $filesystem = $this->getContainer()->get('filesystem'); // Create the bundles directory otherwise symlink will fail. $bundlesDir = $targetArg.'/bundles/'; $filesystem->mkdir($bundlesDir, 0777); - $output->writeln(sprintf('Installing assets using the %s option', $input->getOption('symlink') ? 'symlink' : 'hard copy')); + // relative implies symlink + $symlink = $input->getOption('symlink') || $input->getOption('relative'); + + if ($symlink) { + $output->writeln('Trying to install assets as symbolic links.'); + } else { + $output->writeln('Installing assets as hard copies.'); + } foreach ($this->getContainer()->get('kernel')->getBundles() as $bundle) { if (is_dir($originDir = $bundle->getPath().'/Resources/public')) { @@ -93,19 +97,54 @@ protected function execute(InputInterface $input, OutputInterface $output) $filesystem->remove($targetDir); - if ($input->getOption('symlink')) { + if ($symlink) { if ($input->getOption('relative')) { $relativeOriginDir = $filesystem->makePathRelative($originDir, realpath($bundlesDir)); } else { $relativeOriginDir = $originDir; } - $filesystem->symlink($relativeOriginDir, $targetDir); + + try { + $filesystem->symlink($relativeOriginDir, $targetDir); + if (!file_exists($targetDir)) { + throw new IOException('Symbolic link is broken'); + } + $output->writeln('The assets were installed using symbolic links.'); + } catch (IOException $e) { + if (!$input->getOption('relative')) { + $this->hardCopy($originDir, $targetDir); + $output->writeln('It looks like your system doesn\'t support symbolic links, so the assets were installed by copying them.'); + } + + // try again without the relative option + try { + $filesystem->symlink($originDir, $targetDir); + if (!file_exists($targetDir)) { + throw new IOException('Symbolic link is broken'); + } + $output->writeln('It looks like your system doesn\'t support relative symbolic links, so the assets were installed by using absolute symbolic links.'); + } catch (IOException $e) { + $this->hardCopy($originDir, $targetDir); + $output->writeln('It looks like your system doesn\'t support symbolic links, so the assets were installed by copying them.'); + } + } } else { - $filesystem->mkdir($targetDir, 0777); - // We use a custom iterator to ignore VCS files - $filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir)); + $this->hardCopy($originDir, $targetDir); } } } } + + /** + * @param string $originDir + * @param string $targetDir + */ + private function hardCopy($originDir, $targetDir) + { + $filesystem = $this->getContainer()->get('filesystem'); + + $filesystem->mkdir($targetDir, 0777); + // We use a custom iterator to ignore VCS files + $filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir)); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 4e4f3fa5f307..5e98e1d58f75 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -80,9 +80,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $warmupDir = substr($realCacheDir, 0, -1).('_' === substr($realCacheDir, -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); @@ -92,7 +98,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/CacheWarmupCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php index ea82751d8483..54d2fe9c426c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php @@ -42,6 +42,7 @@ protected function configure() command, too many classes that should be part of the cache are already loaded in memory). Use curl or any other similar tool to warm up the classes cache if you want. + EOF ) ; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php new file mode 100644 index 000000000000..20deffcc3339 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Yaml\Yaml; + +/** + * A console command for dumping available configuration reference. + * + * @author Grégoire Pineau + */ +class ConfigDebugCommand extends AbstractConfigCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('debug:config') + ->setAliases(array( + 'config:debug', + )) + ->setDefinition(array( + new InputArgument('name', InputArgument::OPTIONAL, 'The bundle name or the extension alias'), + )) + ->setDescription('Dumps the current configuration for an extension') + ->setHelp(<<%command.name% command dumps the current configuration for an +extension/bundle. + +Either the extension alias or bundle name can be used: + + php %command.full_name% framework + php %command.full_name% FrameworkBundle + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output = new SymfonyStyle($input, $output); + if (false !== strpos($input->getFirstArgument(), ':d')) { + $output->caution('The use of "config:debug" command is deprecated since version 2.7 and will be removed in 3.0. Use the "debug:config" instead.'); + } + + $name = $input->getArgument('name'); + + if (empty($name)) { + $this->listBundles($output); + + return; + } + + $extension = $this->findExtension($name); + $container = $this->compileContainer(); + + $configs = $container->getExtensionConfig($extension->getAlias()); + $configuration = $extension->getConfiguration($configs, $container); + + $this->validateConfiguration($extension, $configuration); + + $configs = $container->getParameterBag()->resolveValue($configs); + + $processor = new Processor(); + $config = $processor->processConfiguration($configuration, $configs); + + if ($name === $extension->getAlias()) { + $output->writeln(sprintf('# Current configuration for extension with alias: "%s"', $name)); + } else { + $output->writeln(sprintf('# Current configuration for "%s"', $name)); + } + + $output->writeln(Yaml::dump(array($extension->getAlias() => $config), 10)); + } + + private function compileContainer() + { + $kernel = clone $this->getContainer()->get('kernel'); + $kernel->boot(); + + $method = new \ReflectionMethod($kernel, 'buildContainer'); + $method->setAccessible(true); + $container = $method->invoke($kernel); + $container->getCompiler()->compile($container); + + return $container; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php index e116d83e88bb..d9752a0139ff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php @@ -11,18 +11,22 @@ 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; +use Symfony\Component\Console\Style\SymfonyStyle; /** * A console command for dumping available configuration reference. * * @author Kevin Bond + * @author Wouter J + * @author Grégoire Pineau */ -class ConfigDumpReferenceCommand extends ContainerDebugCommand +class ConfigDumpReferenceCommand extends AbstractConfigCommand { /** * {@inheritdoc} @@ -32,21 +36,25 @@ 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 output format (yaml or xml)', 'yaml'), )) - ->setDescription('Dumps default configuration for an extension') + ->setDescription('Dumps the default configuration for an extension') ->setHelp(<<<'EOF' -The %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: +Either the extension alias or bundle name can be used: php %command.full_name% framework + php %command.full_name% FrameworkBundle -or +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. + + php %command.full_name% FrameworkBundle --format=xml - php %command.full_name% FrameworkBundle EOF ) ; @@ -59,71 +67,41 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $bundles = $this->getContainer()->get('kernel')->getBundles(); - $containerBuilder = $this->getContainerBuilder(); - + $output = new SymfonyStyle($input, $output); $name = $input->getArgument('name'); if (empty($name)) { - $output->writeln('Available registered bundles with their extension alias if available:'); - - usort($bundles, function($bundleA, $bundleB) { - return strcmp($bundleA->getName(), $bundleB->getName()); - }); - - foreach ($bundles as $bundle) { - $extension = $bundle->getContainerExtension(); - $output->writeln($bundle->getName().($extension ? ': '.$extension->getAlias() : '')); - } + $this->listBundles($output); return; } - $extension = null; - - if (preg_match('/Bundle$/', $name)) { - // input is bundle name + $extension = $this->findExtension($name); - if (isset($bundles[$name])) { - $extension = $bundles[$name]->getContainerExtension(); - } + $configuration = $extension->getConfiguration(array(), $this->getContainerBuilder()); - if (!$extension) { - throw new \LogicException(sprintf('No extensions with configuration available for "%s"', $name)); - } + $this->validateConfiguration($extension, $configuration); - $message = 'Default configuration for "'.$name.'"'; + if ($name === $extension->getAlias()) { + $message = sprintf('Default configuration for extension with alias: "%s"', $name); } else { - foreach ($bundles as $bundle) { - $extension = $bundle->getContainerExtension(); - - if ($extension && $extension->getAlias() === $name) { - break; - } - - $extension = null; - } - - if (!$extension) { - throw new \LogicException(sprintf('No extension with alias "%s" is enabled', $name)); - } - - $message = 'Default configuration for extension with alias: "'.$name.'"'; + $message = sprintf('Default configuration for "%s"', $name); } - $configuration = $extension->getConfiguration(array(), $containerBuilder); - - if (!$configuration) { - throw new \LogicException(sprintf('The extension with alias "%s" does not have it\'s getConfiguration() method setup', $extension->getAlias())); - } - - if (!$configuration instanceof ConfigurationInterface) { - throw new \LogicException(sprintf('Configuration class "%s" should implement ConfigurationInterface in order to be dumpable', get_class($configuration))); + 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.'); } - $output->writeln($message); - - $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 d8fde9fe5800..edfa6d9cc94d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -11,15 +11,15 @@ 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; +use Symfony\Component\Console\Question\ChoiceQuestion; /** * A console command for retrieving information about services. @@ -39,14 +39,19 @@ class ContainerDebugCommand extends ContainerAwareCommand protected function configure() { $this - ->setName('container:debug') + ->setName('debug:container') + ->setAliases(array( + 'container:debug', + )) ->setDefinition(array( new InputArgument('name', InputArgument::OPTIONAL, 'A service name (foo)'), - new InputOption('show-private', null, InputOption::VALUE_NONE, 'Use to show public *and* private services'), - new InputOption('tag', null, InputOption::VALUE_REQUIRED, 'Show all services with a specific tag'), + new InputOption('show-private', null, InputOption::VALUE_NONE, 'Used to show public *and* private services'), + new InputOption('tag', null, InputOption::VALUE_REQUIRED, 'Shows 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('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), )) ->setDescription('Displays current services for an application') ->setHelp(<<<'EOF' @@ -78,6 +83,7 @@ protected function configure() Display a specific parameter by specifying its name with the --parameter option: php %command.full_name% --parameter=kernel.debug + EOF ) ; @@ -85,57 +91,49 @@ protected function configure() /** * {@inheritdoc} - * - * @throws \LogicException */ protected function execute(InputInterface $input, OutputInterface $output) { - $this->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; + if (false !== strpos($input->getFirstArgument(), ':d')) { + $output->writeln('The use of "container:debug" command is deprecated since version 2.7 and will be removed in 3.0. Use the "debug:container" instead.'); } - $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; - } + $this->validateInput($input); + $object = $this->getContainerBuilder(); - $tag = $input->getOption('tag'); - if (null !== $tag) { - $serviceIds = array_keys($this->containerBuilder->findTaggedServiceIds($tag)); + if ($input->getOption('parameters')) { + $object = $object->getParameterBag(); + $options = array(); + } elseif ($parameter = $input->getOption('parameter')) { + $options = array('parameter' => $parameter); + } elseif ($input->getOption('tags')) { + $options = array('group_by' => 'tags', 'show_private' => $input->getOption('show-private')); + } elseif ($tag = $input->getOption('tag')) { + $options = array('tag' => $tag, 'show_private' => $input->getOption('show-private')); + } elseif ($name = $input->getArgument('name')) { + $name = $this->findProperServiceName($input, $output, $object, $name); + $options = array('id' => $name); } else { - $serviceIds = $this->containerBuilder->getServiceIds(); + $options = array('show_private' => $input->getOption('show-private')); } - // sort so that it reads like an index of services - asort($serviceIds); + $helper = new DescriptorHelper(); + $options['format'] = $input->getOption('format'); + $options['raw_text'] = $input->getOption('raw'); + $helper->describe($output, $object, $options); - $name = $input->getArgument('name'); - if ($name) { - $this->outputService($output, $name); - } else { - $this->outputServices($output, $serviceIds, $input->getOption('show-private'), $tag); + if (!$input->getArgument('name') && $input->isInteractive()) { + $output->writeln('To search for a service, re-run this command with a search term. debug:container log'); } } + /** + * Validates input arguments and options. + * + * @param InputInterface $input + * + * @throws \InvalidArgumentException + */ protected function validateInput(InputInterface $input) { $options = array('tags', 'tag', 'parameters', 'parameter'); @@ -155,211 +153,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() ?: '-'; - $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. * @@ -369,6 +162,10 @@ protected function outputParameters(OutputInterface $output, $parameters) */ protected function getContainerBuilder() { + if ($this->containerBuilder) { + return $this->containerBuilder; + } + if (!$this->getApplication()->getKernel()->isDebug()) { throw new \LogicException(sprintf('Debug information about the container is only available in debug mode.')); } @@ -382,79 +179,38 @@ protected function getContainerBuilder() $loader = new XmlFileLoader($container, new FileLocator()); $loader->load($cachedFile); - return $container; + return $this->containerBuilder = $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) + private function findProperServiceName(InputInterface $input, OutputInterface $output, ContainerBuilder $builder, $name) { - if ($this->containerBuilder->hasDefinition($serviceId)) { - return $this->containerBuilder->getDefinition($serviceId); + if ($builder->has($name) || !$input->isInteractive()) { + return $name; } - // Some service IDs don't have a Definition, they're simply an Alias - if ($this->containerBuilder->hasAlias($serviceId)) { - return $this->containerBuilder->getAlias($serviceId); + $matchingServices = $this->findServiceIdsContaining($builder, $name); + if (empty($matchingServices)) { + throw new \InvalidArgumentException(sprintf('No services found that match "%s".', $name)); } - // the service has been injected in some special way, just return the service - return $this->containerBuilder->get($serviceId); + $question = new ChoiceQuestion('Choose a number for more information on the service', $matchingServices); + $question->setErrorMessage('Service %s is invalid.'); + + return $this->getHelper('question')->ask($input, $output, $question); } - /** - * Renders list of tagged services grouped by tag. - * - * @param OutputInterface $output - * @param bool $showPrivate - */ - protected function outputTags(OutputInterface $output, $showPrivate = false) + private function findServiceIdsContaining(ContainerBuilder $builder, $name) { - $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) { + $serviceIds = $builder->getServiceIds(); + $foundServiceIds = array(); + $name = strtolower($name); + foreach ($serviceIds as $serviceId) { + if (false === strpos($serviceId, $name)) { 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); + $foundServiceIds[] = $serviceId; } - return $value; + return $foundServiceIds; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php new file mode 100644 index 000000000000..f30b59cf62ac --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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\EventDispatcher\EventDispatcherInterface; + +/** + * A console command for retrieving information about event dispatcher. + * + * @author Matthieu Auger + */ +class EventDispatcherDebugCommand extends ContainerAwareCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('debug:event-dispatcher') + ->setDefinition(array( + new InputArgument('event', InputArgument::OPTIONAL, 'An event name'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), + )) + ->setDescription('Displays configured listeners for an application') + ->setHelp(<<%command.name% command displays all configured listeners: + + php %command.full_name% + +To get specific listeners for an event, specify its name: + + php %command.full_name% kernel.request +EOF + ) + ; + } + + /** + * {@inheritdoc} + * + * @throws \LogicException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $dispatcher = $this->getEventDispatcher(); + + if ($event = $input->getArgument('event')) { + if (!$dispatcher->hasListeners($event)) { + $formatter = $this->getHelperSet()->get('formatter'); + + $formattedBlock = $formatter->formatBlock( + sprintf('[NOTE] The event "%s" does not have any registered listeners.', $event), + 'fg=yellow', + true + ); + + $output->writeln($formattedBlock); + + return; + } + + $options = array('event' => $event); + } else { + $options = array(); + } + + $helper = new DescriptorHelper(); + $options['format'] = $input->getOption('format'); + $options['raw_text'] = $input->getOption('raw'); + $helper->describe($output, $dispatcher, $options); + } + + /** + * Loads the Event Dispatcher from the container. + * + * @return EventDispatcherInterface + */ + protected function getEventDispatcher() + { + return $this->getContainer()->get('event_dispatcher'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php index 0355dd6627c6..d90e2378658b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterApacheDumperCommand.php @@ -21,6 +21,10 @@ /** * RouterApacheDumperCommand. * + * @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 @@ -52,13 +56,14 @@ protected function configure() new InputArgument('script_name', InputArgument::OPTIONAL, 'The script name of the application\'s front controller'), new InputOption('base-uri', null, InputOption::VALUE_REQUIRED, 'The base URI'), )) - ->setDescription('Dumps all routes as Apache rewrite rules') + ->setDescription('[DEPRECATED] Dumps all routes as Apache rewrite rules') ->setHelp(<<<'EOF' The %command.name% dumps all routes as Apache rewrite rules. These can then be used with the ApacheUrlMatcher to use Apache for route matching. php %command.full_name% + EOF ) ; @@ -69,6 +74,10 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { + $formatter = $this->getHelper('formatter'); + + $output->writeln($formatter->formatSection('warning', 'The router:dump-apache command is deprecated since version 2.5 and will be removed in 3.0', 'comment')); + $router = $this->getContainer()->get('router'); $dumpOptions = array(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index 84172218aa5a..8c29492040e3 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. @@ -46,15 +49,22 @@ public function isEnabled() protected function configure() { $this - ->setName('router:debug') + ->setName('debug:router') + ->setAliases(array( + '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, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), )) ->setDescription('Displays current routes for an application') ->setHelp(<<<'EOF' The %command.name% displays the configured routes: php %command.full_name% + EOF ) ; @@ -67,126 +77,47 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { + if (false !== strpos($input->getFirstArgument(), ':d')) { + $output->writeln('The use of "router:debug" command is deprecated since version 2.7 and will be removed in 3.0. Use the "debug:router" instead.'); + } + $name = $input->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)); - } - - $output->writeln($this->getHelper('formatter')->formatSection('router', sprintf('Route "%s"', $name))); - - $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); + $routes = $this->getContainer()->get('router')->getRouteCollection(); - $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); + foreach ($routes as $route) { + $this->convertController($route); + } - $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); - } - } - - protected function formatValue($value) - { - if (is_object($value)) { - return sprintf('object(%s)', get_class($value)); - } - - if (is_string($value)) { - return $value; + $helper->describe($output, $routes, array( + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'show_controllers' => $input->getOption('show-controllers'), + )); } - - return preg_replace("/\n\s*/s", '', var_export($value, true)); } - private function formatConfigs(array $array) + private function convertController(Route $route) { - $string = ''; - ksort($array); - foreach ($array as $name => $value) { - $string .= ($string ? "\n".str_repeat(' ', 13) : '').$name.': '.$this->formatValue($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 $string; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php index 5ad347ce590b..92cdee844fa3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php @@ -13,6 +13,7 @@ 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\Console\Input\ArrayInput; use Symfony\Component\Routing\RouterInterface; @@ -50,12 +51,20 @@ protected function configure() ->setName('router:match') ->setDefinition(array( new InputArgument('path_info', InputArgument::REQUIRED, 'A path info'), + new InputOption('method', null, InputOption::VALUE_REQUIRED, 'Sets the HTTP method'), + new InputOption('scheme', null, InputOption::VALUE_REQUIRED, 'Sets the URI scheme (usually http or https)'), + new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Sets the URI host'), )) ->setDescription('Helps debug routes by simulating a path info match') ->setHelp(<<<'EOF' -The %command.name% simulates a path info match: +The %command.name% shows which routes match a given request and which don't and for what reason: php %command.full_name% /foo + +or + + php %command.full_name% /foo --method POST --scheme https --host symfony.com --verbose + EOF ) ; @@ -67,7 +76,18 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $router = $this->getContainer()->get('router'); - $matcher = new TraceableUrlMatcher($router->getRouteCollection(), $router->getContext()); + $context = $router->getContext(); + if (null !== $method = $input->getOption('method')) { + $context->setMethod($method); + } + if (null !== $scheme = $input->getOption('scheme')) { + $context->setScheme($scheme); + } + if (null !== $host = $input->getOption('host')) { + $context->setHost($host); + } + + $matcher = new TraceableUrlMatcher($router->getRouteCollection(), $context); $traces = $matcher->getTraces($input->getArgument('path_info')); @@ -78,7 +98,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } elseif (TraceableUrlMatcher::ROUTE_MATCHES == $trace['level']) { $output->writeln(sprintf('Route "%s" matches', $trace['name'])); - $routerDebugcommand = $this->getApplication()->find('router:debug'); + $routerDebugcommand = $this->getApplication()->find('debug:router'); $output->writeln(''); $routerDebugcommand->run(new ArrayInput(array('name' => $trace['name'])), $output); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerCommand.php new file mode 100644 index 000000000000..acf71c666605 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ServerCommand.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\Bundle\FrameworkBundle\Command; + +/** + * Base methods for commands related to PHP's built-in web server. + * + * @author Christian Flothmann + */ +abstract class ServerCommand extends ContainerAwareCommand +{ + /** + * {@inheritdoc} + */ + public function isEnabled() + { + if (PHP_VERSION_ID < 50400 || defined('HHVM_VERSION')) { + return false; + } + + if (!class_exists('Symfony\Component\Process\Process')) { + return false; + } + + return parent::isEnabled(); + } + + /** + * Determines the name of the lock file for a particular PHP web server process. + * + * @param string $address An address/port tuple + * + * @return string The filename + */ + protected function getLockFile($address) + { + return sys_get_temp_dir().'/'.strtr($address, '.:', '--').'.pid'; + } + + protected function isOtherServerProcessRunning($address) + { + $lockFile = $this->getLockFile($address); + + if (file_exists($lockFile)) { + return true; + } + + list($hostname, $port) = explode(':', $address); + + $fp = @fsockopen($hostname, $port, $errno, $errstr, 5); + + if (false !== $fp) { + fclose($fp); + + return true; + } + + return false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php index 573b67020dc9..4c2b214a3130 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ServerRunCommand.php @@ -23,24 +23,8 @@ * * @author Michał Pipa */ -class ServerRunCommand extends ContainerAwareCommand +class ServerRunCommand extends ServerCommand { - /** - * {@inheritdoc} - */ - public function isEnabled() - { - if (PHP_VERSION_ID < 50400 || defined('HHVM_VERSION')) { - return false; - } - - if (!class_exists('Symfony\Component\Process\Process')) { - return false; - } - - return parent::isEnabled(); - } - /** * {@inheritdoc} */ @@ -48,7 +32,7 @@ protected function configure() { $this ->setDefinition(array( - new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', 'localhost:8000'), + new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:8000'), new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root', null), new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), )) @@ -72,10 +56,11 @@ protected function configure() %command.full_name% --router=app/config/router.php -Specifing a router script is required when the used environment is not "dev" or -"prod". +Specifing a router script is required when the used environment is not "dev", +"prod", or "test". See also: http://www.php.net/manual/en/features.commandline.webserver.php + EOF ) ; @@ -117,26 +102,29 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln('Running PHP built-in server in production environment is NOT recommended!'); } + $output->writeln(sprintf("Server running on http://%s\n", $address)); + $output->writeln('Quit the server with CONTROL-C.'); + if (null === $builder = $this->createPhpProcessBuilder($output, $address, $input->getOption('router'), $env)) { return 1; } - $output->writeln(sprintf("Server running on http://%s\n", $address)); - $output->writeln('Quit the server with CONTROL-C.'); - $builder->setWorkingDirectory($documentRoot); $builder->setTimeout(null); $process = $builder->getProcess(); - $process->run(function ($type, $buffer) use ($output) { - if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { - $output->write($buffer); - } - }); + + if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) { + $process->disableOutput(); + } + + $this + ->getHelper('process') + ->run($output, $process, null, null, OutputInterface::VERBOSITY_VERBOSE); if (!$process->isSuccessful()) { $output->writeln('Built-in server terminated unexpectedly'); - if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) { + if ($process->isOutputDisabled()) { $output->writeln('Run the command again with -v option for more details'); } } @@ -144,21 +132,6 @@ protected function execute(InputInterface $input, OutputInterface $output) return $process->getExitCode(); } - private function isOtherServerProcessRunning($address) - { - list($hostname, $port) = explode(':', $address); - - $fp = @fsockopen($hostname, $port, $errno, $errstr, 5); - - if (false !== $fp) { - fclose($fp); - - return true; - } - - return false; - } - private function createPhpProcessBuilder(OutputInterface $output, $address, $router, $env) { $router = $router ?: $this diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php new file mode 100644 index 000000000000..04906317fa94 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStartCommand.php @@ -0,0 +1,227 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Question\ConfirmationQuestion; +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\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + +/** + * Runs PHP's built-in web server in a background process. + * + * @author Christian Flothmann + */ +class ServerStartCommand extends ServerCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:8000'), + new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root', null), + new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), + )) + ->setName('server:start') + ->setDescription('Starts PHP built-in web server in the background') + ->setHelp(<<%command.name% runs PHP's built-in web server: + + php %command.full_name% + +To change the default bind address and the default port use the address argument: + + php %command.full_name% 127.0.0.1:8080 + +To change the default document root directory use the --docroot option: + + php %command.full_name% --docroot=htdocs/ + +If you have a custom document root directory layout, you can specify your own +router script using the --router option: + + php %command.full_name% --router=app/config/router.php + +Specifying a router script is required when the used environment is not "dev" or +"prod". + +See also: http://www.php.net/manual/en/features.commandline.webserver.php + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!extension_loaded('pcntl')) { + $output->writeln('This command needs the pcntl extension to run.'); + $output->writeln('You can either install it or use the server:run command instead to run the built-in web server.'); + + if ($this->getHelper('question')->ask($input, $output, new ConfirmationQuestion('Do you want to start server:run immediately? [Yn] ', true))) { + $command = $this->getApplication()->find('server:run'); + + return $command->run($input, $output); + } + + return 1; + } + + $documentRoot = $input->getOption('docroot'); + + if (null === $documentRoot) { + $documentRoot = $this->getContainer()->getParameter('kernel.root_dir').'/../web'; + } + + if (!is_dir($documentRoot)) { + $output->writeln(sprintf('The given document root directory "%s" does not exist', $documentRoot)); + + return 1; + } + + $env = $this->getContainer()->getParameter('kernel.environment'); + + if (false === $router = $this->determineRouterScript($input->getOption('router'), $env, $output)) { + return 1; + } + + $address = $input->getArgument('address'); + + if (false === strpos($address, ':')) { + $output->writeln('The address has to be of the form bind-address:port.'); + + return 1; + } + + if ($this->isOtherServerProcessRunning($address)) { + $output->writeln(sprintf('A process is already listening on http://%s.', $address)); + + return 1; + } + + if ('prod' === $env) { + $output->writeln('Running PHP built-in server in production environment is NOT recommended!'); + } + + $pid = pcntl_fork(); + + if ($pid < 0) { + $output->writeln('Unable to start the server process'); + + return 1; + } + + if ($pid > 0) { + $output->writeln(sprintf('Web server listening on http://%s', $address)); + + return; + } + + if (posix_setsid() < 0) { + $output->writeln('Unable to set the child process as session leader'); + + return 1; + } + + if (null === $process = $this->createServerProcess($output, $address, $documentRoot, $router)) { + return 1; + } + + $process->disableOutput(); + $process->start(); + $lockFile = $this->getLockFile($address); + touch($lockFile); + + if (!$process->isRunning()) { + $output->writeln('Unable to start the server process'); + unlink($lockFile); + + return 1; + } + + // stop the web server when the lock file is removed + while ($process->isRunning()) { + if (!file_exists($lockFile)) { + $process->stop(); + } + + sleep(1); + } + } + + /** + * Determine the absolute file path for the router script, using the environment to choose a standard script + * if no custom router script is specified. + * + * @param string|null $router File path of the custom router script, if set by the user; otherwise null + * @param string $env The application environment + * @param OutputInterface $output An OutputInterface instance + * + * @return string|bool The absolute file path of the router script, or false on failure + */ + private function determineRouterScript($router, $env, OutputInterface $output) + { + if (null === $router) { + $router = $this + ->getContainer() + ->get('kernel') + ->locateResource(sprintf('@FrameworkBundle/Resources/config/router_%s.php', $env)) + ; + } + + if (false === $path = realpath($router)) { + $output->writeln(sprintf('The given router script "%s" does not exist', $router)); + + return false; + } + + return $path; + } + + /** + * Creates a process to start PHP's built-in web server. + * + * @param OutputInterface $output A OutputInterface instance + * @param string $address IP address and port to listen to + * @param string $documentRoot The application's document root + * @param string $router The router filename + * + * @return Process The process + */ + private function createServerProcess(OutputInterface $output, $address, $documentRoot, $router) + { + $finder = new PhpExecutableFinder(); + if (false === $binary = $finder->find()) { + $output->writeln('Unable to find PHP binary to start server'); + + return; + } + + $script = implode(' ', array_map(array('Symfony\Component\Process\ProcessUtils', 'escapeArgument'), array( + $binary, + '-S', + $address, + $router, + ))); + + return new Process('exec '.$script, $documentRoot, null, null, null); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStatusCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStatusCommand.php new file mode 100644 index 000000000000..fa5c537a0c97 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStatusCommand.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Shows the status of a process that is running PHP's built-in web server in + * the background. + * + * @author Christian Flothmann + */ +class ServerStatusCommand extends ServerCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:8000'), + new InputOption('port', 'p', InputOption::VALUE_REQUIRED, 'Address port number', '8000'), + )) + ->setName('server:status') + ->setDescription('Outputs the status of the built-in web server for the given address') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $address = $input->getArgument('address'); + + if (false === strpos($address, ':')) { + $address = $address.':'.$input->getOption('port'); + } + + // remove an orphaned lock file + if (file_exists($this->getLockFile($address)) && !$this->isServerRunning($address)) { + unlink($this->getLockFile($address)); + } + + if (file_exists($this->getLockFile($address))) { + $output->writeln(sprintf('Web server still listening on http://%s', $address)); + } else { + $output->writeln(sprintf('No web server is listening on http://%s', $address)); + } + } + + private function isServerRunning($address) + { + list($hostname, $port) = explode(':', $address); + + if (false !== $fp = @fsockopen($hostname, $port, $errno, $errstr, 1)) { + fclose($fp); + + return true; + } + + return false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ServerStopCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStopCommand.php new file mode 100644 index 000000000000..9b0656c220b6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ServerStopCommand.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Stops a background process running PHP's built-in web server. + * + * @author Christian Flothmann + */ +class ServerStopCommand extends ServerCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:8000'), + )) + ->setName('server:stop') + ->setDescription('Stops PHP\'s built-in web server that was started with the server:start command') + ->setHelp(<<%command.name% stops PHP's built-in web server: + + php %command.full_name% + +To change the default bind address and the default port use the address argument: + + php %command.full_name% 127.0.0.1:8080 + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $address = $input->getArgument('address'); + $lockFile = $this->getLockFile($address); + + if (!file_exists($lockFile)) { + $output->writeln(sprintf('No web server is listening on http://%s', $address)); + + return 1; + } + + unlink($lockFile); + $output->writeln(sprintf('Stopped the web server listening on http://%s', $address)); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php new file mode 100644 index 000000000000..30d44493d4c4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -0,0 +1,268 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Catalogue\MergeOperation; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Translator; + +/** + * Helps finding unused or missing translation messages in a given locale + * and comparing them with the fallback ones. + * + * @author Florian Voutzinos + */ +class TranslationDebugCommand extends ContainerAwareCommand +{ + const MESSAGE_MISSING = 0; + const MESSAGE_UNUSED = 1; + const MESSAGE_EQUALS_FALLBACK = 2; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('debug:translation') + ->setAliases(array( + 'translation:debug', + )) + ->setDefinition(array( + new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), + new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages, defaults to app/Resources folder'), + new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'The messages domain'), + new InputOption('only-missing', null, InputOption::VALUE_NONE, 'Displays only missing messages'), + new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Displays only unused messages'), + )) + ->setDescription('Displays translation messages information') + ->setHelp(<<%command.name% command helps finding unused or missing translation +messages and comparing them with the fallback ones by inspecting the +templates and translation files of a given bundle or the app folder. + +You can display information about bundle translations in a specific locale: + + php %command.full_name% en AcmeDemoBundle + +You can also specify a translation domain for the search: + + php %command.full_name% --domain=messages en AcmeDemoBundle + +You can only display missing messages: + + php %command.full_name% --only-missing en AcmeDemoBundle + +You can only display unused messages: + + php %command.full_name% --only-unused en AcmeDemoBundle + +You can display information about app translations in a specific locale: + + php %command.full_name% en + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output = new SymfonyStyle($input, $output); + if (false !== strpos($input->getFirstArgument(), ':d')) { + $output->caution('The use of "translation:debug" command is deprecated since version 2.7 and will be removed in 3.0. Use the "debug:translation" instead.'); + } + + $locale = $input->getArgument('locale'); + $domain = $input->getOption('domain'); + $loader = $this->getContainer()->get('translation.loader'); + $kernel = $this->getContainer()->get('kernel'); + + // Define Root Path to App folder + $transPaths = array($kernel->getRootDir().'/Resources/'); + + // Override with provided Bundle info + if (null !== $input->getArgument('bundle')) { + try { + $bundle = $kernel->getBundle($input->getArgument('bundle')); + $transPaths = array( + $bundle->getPath().'/Resources/', + sprintf('%s/Resources/%s/', $kernel->getRootDir(), $bundle->getName()), + ); + } catch (\InvalidArgumentException $e) { + // such a bundle does not exist, so treat the argument as path + $transPaths = array($input->getArgument('bundle').'/Resources/'); + if (!is_dir($transPaths[0])) { + throw new \InvalidArgumentException(sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); + } + } + } + + // Extract used messages + $extractedCatalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + $path .= 'views'; + + if (is_dir($path)) { + $this->getContainer()->get('translation.extractor')->extract($path, $extractedCatalogue); + } + } + + // Load defined messages + $currentCatalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + $path .= 'translations'; + if (is_dir($path)) { + $loader->loadMessages($path, $currentCatalogue); + } + } + + // Merge defined and extracted messages to get all message ids + $mergeOperation = new MergeOperation($extractedCatalogue, $currentCatalogue); + $allMessages = $mergeOperation->getResult()->all($domain); + if (null !== $domain) { + $allMessages = array($domain => $allMessages); + } + + // No defined or extracted messages + if (empty($allMessages) || null !== $domain && empty($allMessages[$domain])) { + $outputMessage = sprintf('No defined or extracted messages for locale "%s"', $locale); + + if (null !== $domain) { + $outputMessage .= sprintf(' and domain "%s"', $domain); + } + + $output->warning($outputMessage); + + return; + } + + // Load the fallback catalogues + $fallbackCatalogues = array(); + $translator = $this->getContainer()->get('translator'); + if ($translator instanceof Translator) { + foreach ($translator->getFallbackLocales() as $fallbackLocale) { + if ($fallbackLocale === $locale) { + continue; + } + + $fallbackCatalogue = new MessageCatalogue($fallbackLocale); + foreach ($transPaths as $path) { + $path = $path.'translations'; + if (is_dir($path)) { + $loader->loadMessages($path, $fallbackCatalogue); + } + } + $fallbackCatalogues[] = $fallbackCatalogue; + } + } + + // Display header line + $headers = array('State', 'Domain', 'Id', sprintf('Message Preview (%s)', $locale)); + foreach ($fallbackCatalogues as $fallbackCatalogue) { + $headers[] = sprintf('Fallback Message Preview (%s)', $fallbackCatalogue->getLocale()); + } + $rows = array(); + // Iterate all message ids and determine their state + foreach ($allMessages as $domain => $messages) { + foreach (array_keys($messages) as $messageId) { + $value = $currentCatalogue->get($messageId, $domain); + $states = array(); + + if ($extractedCatalogue->defines($messageId, $domain)) { + if (!$currentCatalogue->defines($messageId, $domain)) { + $states[] = self::MESSAGE_MISSING; + } + } elseif ($currentCatalogue->defines($messageId, $domain)) { + $states[] = self::MESSAGE_UNUSED; + } + + if (!in_array(self::MESSAGE_UNUSED, $states) && true === $input->getOption('only-unused') + || !in_array(self::MESSAGE_MISSING, $states) && true === $input->getOption('only-missing')) { + continue; + } + + foreach ($fallbackCatalogues as $fallbackCatalogue) { + if ($fallbackCatalogue->defines($messageId, $domain) && $value === $fallbackCatalogue->get($messageId, $domain)) { + $states[] = self::MESSAGE_EQUALS_FALLBACK; + + break; + } + } + + $row = array($this->formatStates($states), $domain, $this->formatId($messageId), $this->sanitizeString($value)); + foreach ($fallbackCatalogues as $fallbackCatalogue) { + $row[] = $this->sanitizeString($fallbackCatalogue->get($messageId, $domain)); + } + + $rows[] = $row; + } + } + + $output->table($headers, $rows); + } + + private function formatState($state) + { + if (self::MESSAGE_MISSING === $state) { + return 'missing'; + } + + if (self::MESSAGE_UNUSED === $state) { + return 'unused'; + } + + if (self::MESSAGE_EQUALS_FALLBACK === $state) { + return 'fallback'; + } + + return $state; + } + + private function formatStates(array $states) + { + $result = array(); + foreach ($states as $state) { + $result[] = $this->formatState($state); + } + + return implode(' ', $result); + } + + private function formatId($id) + { + return sprintf('%s', $id); + } + + private function sanitizeString($string, $length = 40) + { + $string = trim(preg_replace('/\s+/', ' ', $string)); + + if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($string)) { + if (mb_strlen($string, $encoding) > $length) { + return mb_substr($string, 0, $length - 3, $encoding).'...'; + } + } elseif (strlen($string) > $length) { + return substr($string, 0, $length - 3).'...'; + } + + return $string; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index 03f058dddcca..f310613d1031 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Catalogue\DiffOperation; use Symfony\Component\Translation\Catalogue\MergeOperation; use Symfony\Component\Console\Input\InputInterface; @@ -35,22 +36,29 @@ protected function configure() ->setName('translation:update') ->setDefinition(array( new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), - new InputArgument('bundle', InputArgument::REQUIRED, 'The bundle where to load the messages'), + new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages, defaults to app/Resources folder'), new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'), new InputOption('output-format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'yml'), new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'), new InputOption('force', null, InputOption::VALUE_NONE, 'Should the update be done'), + new InputOption('no-backup', null, InputOption::VALUE_NONE, 'Should backup be disabled'), new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'), )) ->setDescription('Updates the translation file') ->setHelp(<<<'EOF' The %command.name% command extracts translation strings from templates -of a given bundle. It can display them or merge the new ones into the translation files. +of a given bundle or the app folder. It can display them or merge the new ones into the translation files. + When new translation strings are found it can automatically add a prefix to the translation message. +Example running against a Bundle (AcmeBundle) php %command.full_name% --dump-messages en AcmeBundle php %command.full_name% --force --prefix="new_" fr AcmeBundle + +Example running against app messages (app/Resources folder) + php %command.full_name% --dump-messages en + php %command.full_name% --force --prefix="new_" fr EOF ) ; @@ -61,11 +69,11 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $kernel = $this->getContainer()->get('kernel'); + $output = new SymfonyStyle($input, $output); // check presence of force or dump-message if ($input->getOption('force') !== true && $input->getOption('dump-messages') !== true) { - $output->writeln('You must choose one of --force or --dump-messages'); + $output->error('You must choose one of --force or --dump-messages'); return 1; } @@ -74,27 +82,45 @@ protected function execute(InputInterface $input, OutputInterface $output) $writer = $this->getContainer()->get('translation.writer'); $supportedFormats = $writer->getFormats(); if (!in_array($input->getOption('output-format'), $supportedFormats)) { - $output->writeln('Wrong output format'); - $output->writeln('Supported formats are '.implode(', ', $supportedFormats).'.'); + $output->error(array('Wrong output format', 'Supported formats are '.implode(', ', $supportedFormats).'.')); return 1; } + $kernel = $this->getContainer()->get('kernel'); - // get bundle directory - $foundBundle = $kernel->getBundle($input->getArgument('bundle')); - $bundleTransPaths = array( - $foundBundle->getPath().'/Resources/', - sprintf('%s/Resources/%s/', $kernel->getRootDir(), $foundBundle->getName()), - ); + // Define Root Path to App folder + $transPaths = array($kernel->getRootDir().'/Resources/'); + $currentName = 'app folder'; + + // Override with provided Bundle info + if (null !== $input->getArgument('bundle')) { + try { + $foundBundle = $kernel->getBundle($input->getArgument('bundle')); + $transPaths = array( + $foundBundle->getPath().'/Resources/', + sprintf('%s/Resources/%s/', $kernel->getRootDir(), $foundBundle->getName()), + ); + $currentName = $foundBundle->getName(); + } catch (\InvalidArgumentException $e) { + // such a bundle does not exist, so treat the argument as path + $transPaths = array($input->getArgument('bundle').'/Resources/'); + $currentName = $transPaths[0]; + + if (!is_dir($transPaths[0])) { + throw new \InvalidArgumentException(sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); + } + } + } - $output->writeln(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $foundBundle->getName())); + $output->title('Symfony translation update command'); + $output->text(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); // load any messages from templates $extractedCatalogue = new MessageCatalogue($input->getArgument('locale')); - $output->writeln('Parsing templates'); + $output->text('Parsing templates'); $extractor = $this->getContainer()->get('translation.extractor'); $extractor->setPrefix($input->getOption('prefix')); - foreach ($bundleTransPaths as $path) { + foreach ($transPaths as $path) { $path .= 'views'; if (is_dir($path)) { $extractor->extract($path, $extractedCatalogue); @@ -103,9 +129,9 @@ protected function execute(InputInterface $input, OutputInterface $output) // load any existing messages from the translation files $currentCatalogue = new MessageCatalogue($input->getArgument('locale')); - $output->writeln('Loading translation files'); + $output->text('Loading translation files'); $loader = $this->getContainer()->get('translation.loader'); - foreach ($bundleTransPaths as $path) { + foreach ($transPaths as $path) { $path .= 'translations'; if (is_dir($path)) { $loader->loadMessages($path, $currentCatalogue); @@ -119,26 +145,27 @@ protected function execute(InputInterface $input, OutputInterface $output) // Exit if no messages found. if (!count($operation->getDomains())) { - $output->writeln("\nNo translation found."); + $output->warning('No translation found.'); return; } // show compiled list of messages if ($input->getOption('dump-messages') === true) { + $output->newLine(); foreach ($operation->getDomains() as $domain) { - $output->writeln(sprintf("\nDisplaying messages for domain %s:\n", $domain)); + $output->section(sprintf('Displaying messages for domain %s:', $domain)); $newKeys = array_keys($operation->getNewMessages($domain)); $allKeys = array_keys($operation->getMessages($domain)); - foreach (array_diff($allKeys, $newKeys) as $id) { - $output->writeln($id); - } - foreach ($newKeys as $id) { - $output->writeln(sprintf('%s', $id)); - } - foreach (array_keys($operation->getObsoleteMessages($domain)) as $id) { - $output->writeln(sprintf('%s', $id)); - } + $output->listing(array_merge( + array_diff($allKeys, $newKeys), + array_map(function ($id) { + return sprintf('%s', $id); + }, $newKeys), + array_map(function ($id) { + return sprintf('%s', $id); + }, array_keys($operation->getObsoleteMessages($domain))) + )); } if ($input->getOption('output-format') == 'xlf') { @@ -146,20 +173,30 @@ protected function execute(InputInterface $input, OutputInterface $output) } } + if ($input->getOption('no-backup') === true) { + $writer->disableBackup(); + } + // save the files if ($input->getOption('force') === true) { - $output->writeln('Writing files'); + $output->text('Writing files'); + $bundleTransPath = false; - foreach ($bundleTransPaths as $path) { + foreach ($transPaths as $path) { $path .= 'translations'; if (is_dir($path)) { $bundleTransPath = $path; } } - if ($bundleTransPath) { - $writer->writeTranslations($operation->getResult(), $input->getOption('output-format'), array('path' => $bundleTransPath)); + if (!$bundleTransPath) { + $bundleTransPath = end($transPaths).'translations'; } + + $writer->writeTranslations($operation->getResult(), $input->getOption('output-format'), array('path' => $bundleTransPath, 'default_locale' => $this->getContainer()->getParameter('kernel.default_locale'))); } + + $output->newLine(); + $output->success('Success'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php new file mode 100644 index 000000000000..c4c4bb9cf1d2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.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\Bundle\FrameworkBundle\Command; + +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 outputs encountered errors. + * + * @author Grégoire Pineau + */ +class YamlLintCommand extends Command +{ + protected function configure() + { + $this + ->setName('lint:yaml') + ->setAliases(array('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) + { + if (false !== strpos($input->getFirstArgument(), ':l')) { + $output->writeln('The use of "yaml:lint" command is deprecated since version 2.7 and will be removed in 3.0. Use the "lint:yaml" instead.'); + } + + $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) + { + $parser = new Parser(); + try { + $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, defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0)); + + return min($errors, 1); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index ddbbf8eb371c..221a00425069 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -118,10 +118,20 @@ protected function registerCommands() $this->commandsRegistered = true; + $this->kernel->boot(); + + $container = $this->kernel->getContainer(); + foreach ($this->kernel->getBundles() as $bundle) { if ($bundle instanceof Bundle) { $bundle->registerCommands($this); } } + + if ($container->hasParameter('console.command.ids')) { + foreach ($container->getParameter('console.command.ids') as $id) { + $this->add($container->get($id)); + } + } } } 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 000000000000..33334e79920c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php @@ -0,0 +1,330 @@ + + * + * 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\Table; +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\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + * + * @internal + */ +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->describeContainerParameter($object->getParameter($options['parameter']), $options); + 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; + case $object instanceof EventDispatcherInterface: + $this->describeEventDispatcherListeners($object, $options); + break; + case is_callable($object): + $this->describeCallable($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + } + } + + /** + * Returns the output. + * + * @return OutputInterface The output + */ + protected function getOutput() + { + return $this->output; + } + + /** + * Writes content to output. + * + * @param string $content + * @param bool $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); + } + + /** + * Writes content to output. + * + * @param Table $table + * @param bool $decorated + */ + protected function renderTable(Table $table, $decorated = false) + { + if (!$decorated) { + $table->getStyle()->setCellRowFormat('%s'); + $table->getStyle()->setCellRowContentFormat('%s'); + $table->getStyle()->setCellHeaderFormat('%s'); + } + + $table->render(); + } + + /** + * 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()); + + /** + * Describes a container parameter. + * + * @param string $parameter + * @param array $options + */ + abstract protected function describeContainerParameter($parameter, array $options = array()); + + /** + * Describes event dispatcher listeners. + * + * Common options are: + * * name: name of listened event + * + * @param EventDispatcherInterface $eventDispatcher + * @param array $options + */ + abstract protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array()); + + /** + * Describes a callable. + * + * @param callable $callable + * @param array $options + */ + abstract protected function describeCallable($callable, 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 (preg_match('/^(.{60})./us', $jsonString, $matches)) { + return $matches[1].'...'; + } + + 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); + } + + if ('service_container' === $serviceId) { + return $builder; + } + + // the service has been injected in some special way, just return the service + return $builder->get($serviceId); + } + + /** + * @param ContainerBuilder $builder + * @param bool $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 000000000000..791593de07fe --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -0,0 +1,374 @@ + + * + * 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\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + * + * @internal + */ +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); + } + + /** + * {@inheritdoc} + */ + protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array()) + { + $this->writeData($this->getEventDispatcherListenersData($eventDispatcher, array_key_exists('event', $options) ? $options['event'] : null), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeCallable($callable, array $options = array()) + { + $this->writeData($this->getCallableData($callable, $options), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameter($parameter, array $options = array()) + { + $key = isset($options['parameter']) ? $options['parameter'] : ''; + + $this->writeData(array($key => $parameter), $options); + } + + /** + * Writes data as json. + * + * @param array $data + * @param array $options + * + * @return array|string + */ + private function writeData(array $data, array $options) + { + $flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0; + + if (defined('JSON_PRETTY_PRINT')) { + $flags |= JSON_PRETTY_PRINT; + } + + $this->write(json_encode($data, $flags)."\n"); + } + + /** + * @param Route $route + * + * @return array + */ + protected function getRouteData(Route $route) + { + $requirements = $route->getRequirements(); + unset($requirements['_scheme'], $requirements['_method']); + + return array( + 'path' => $route->getPath(), + 'pathRegex' => $route->compile()->getRegex(), + 'host' => '' !== $route->getHost() ? $route->getHost() : 'ANY', + 'hostRegex' => '' !== $route->getHost() ? $route->compile()->getHostRegex() : '', + '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(), + ); + } + + /** + * @param Definition $definition + * @param bool $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(), + 'lazy' => $definition->isLazy(), + ); + + if (method_exists($definition, 'isSynchronized')) { + $data['synchronized'] = $definition->isSynchronized(false); + } + + $data['abstract'] = $definition->isAbstract(); + $data['file'] = $definition->getFile(); + + if ($definition->getFactoryClass(false)) { + $data['factory_class'] = $definition->getFactoryClass(false); + } + + if ($definition->getFactoryService(false)) { + $data['factory_service'] = $definition->getFactoryService(false); + } + + if ($definition->getFactoryMethod(false)) { + $data['factory_method'] = $definition->getFactoryMethod(false); + } + + if ($factory = $definition->getFactory()) { + if (is_array($factory)) { + if ($factory[0] instanceof Reference) { + $data['factory_service'] = (string) $factory[0]; + } elseif ($factory[0] instanceof Definition) { + throw new \InvalidArgumentException('Factory is not describable.'); + } else { + $data['factory_class'] = $factory[0]; + } + $data['factory_method'] = $factory[1]; + } else { + $data['factory_function'] = $factory; + } + } + + 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(), + ); + } + + /** + * @param EventDispatcherInterface $eventDispatcher + * @param string|null $event + * + * @return array + */ + private function getEventDispatcherListenersData(EventDispatcherInterface $eventDispatcher, $event = null) + { + $data = array(); + + $registeredListeners = $eventDispatcher->getListeners($event); + if (null !== $event) { + foreach ($registeredListeners as $listener) { + $data[] = $this->getCallableData($listener); + } + } else { + ksort($registeredListeners); + + foreach ($registeredListeners as $eventListened => $eventListeners) { + foreach ($eventListeners as $eventListener) { + $data[$eventListened][] = $this->getCallableData($eventListener); + } + } + } + + return $data; + } + + /** + * @param callable $callable + * @param array $options + * + * @return array + */ + private function getCallableData($callable, array $options = array()) + { + $data = array(); + + if (is_array($callable)) { + $data['type'] = 'function'; + + if (is_object($callable[0])) { + $data['name'] = $callable[1]; + $data['class'] = get_class($callable[0]); + } else { + if (0 !== strpos($callable[1], 'parent::')) { + $data['name'] = $callable[1]; + $data['class'] = $callable[0]; + $data['static'] = true; + } else { + $data['name'] = substr($callable[1], 8); + $data['class'] = $callable[0]; + $data['static'] = true; + $data['parent'] = true; + } + } + + return $data; + } + + if (is_string($callable)) { + $data['type'] = 'function'; + + if (false === strpos($callable, '::')) { + $data['name'] = $callable; + } else { + $callableParts = explode('::', $callable); + + $data['name'] = $callableParts[1]; + $data['class'] = $callableParts[0]; + $data['static'] = true; + } + + return $data; + } + + if ($callable instanceof \Closure) { + $data['type'] = 'closure'; + + return $data; + } + + if (method_exists($callable, '__invoke')) { + $data['type'] = 'object'; + $data['name'] = get_class($callable); + + return $data; + } + + throw new \InvalidArgumentException('Callable is not describable.'); + } +} 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 000000000000..8f1227e7e997 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -0,0 +1,372 @@ + + * + * 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\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + * + * @internal + */ +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".'- Path Regex: '.$route->compile()->getRegex() + ."\n".'- Host: '.('' !== $route->getHost() ? $route->getHost() : 'ANY') + ."\n".'- Host Regex: '.('' !== $route->getHost() ? $route->compile()->getHostRegex() : '') + ."\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: '.($requirements ? $this->formatRouterConfig($requirements) : 'NO CUSTOM') + ."\n".'- Options: '.$this->formatRouterConfig($route->getOptions()); + + $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') + ."\n".'- Lazy: '.($definition->isLazy() ? 'yes' : 'no') + ; + + if (method_exists($definition, 'isSynchronized')) { + $output .= "\n".'- Synchronized: '.($definition->isSynchronized(false) ? 'yes' : 'no'); + } + + $output .= "\n".'- Abstract: '.($definition->isAbstract() ? 'yes' : 'no'); + + if ($definition->getFile()) { + $output .= "\n".'- File: `'.$definition->getFile().'`'; + } + + if ($definition->getFactoryClass(false)) { + $output .= "\n".'- Factory Class: `'.$definition->getFactoryClass(false).'`'; + } + + if ($definition->getFactoryService(false)) { + $output .= "\n".'- Factory Service: `'.$definition->getFactoryService(false).'`'; + } + + if ($definition->getFactoryMethod(false)) { + $output .= "\n".'- Factory Method: `'.$definition->getFactoryMethod(false).'`'; + } + + if ($factory = $definition->getFactory()) { + if (is_array($factory)) { + if ($factory[0] instanceof Reference) { + $output .= "\n".'- Factory Service: `'.$factory[0].'`'; + } elseif ($factory[0] instanceof Definition) { + throw new \InvalidArgumentException('Factory is not describable.'); + } else { + $output .= "\n".'- Factory Class: `'.$factory[0].'`'; + } + $output .= "\n".'- Factory Method: `'.$factory[1].'`'; + } else { + $output .= "\n".'- Factory Function: `'.$factory.'`'; + } + } + + 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); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameter($parameter, array $options = array()) + { + $this->write(isset($options['parameter']) ? sprintf("%s\n%s\n\n%s", $options['parameter'], str_repeat('=', strlen($options['parameter'])), $this->formatParameter($parameter)) : $parameter); + } + + /** + * {@inheritdoc} + */ + protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array()) + { + $event = array_key_exists('event', $options) ? $options['event'] : null; + + $title = 'Registered listeners'; + if (null !== $event) { + $title .= sprintf(' for event `%s` ordered by descending priority', $event); + } + + $this->write(sprintf('# %s', $title)."\n"); + + $registeredListeners = $eventDispatcher->getListeners($event); + if (null !== $event) { + foreach ($registeredListeners as $order => $listener) { + $this->write("\n".sprintf('## Listener %d', $order + 1)."\n"); + $this->describeCallable($listener); + } + } else { + ksort($registeredListeners); + + foreach ($registeredListeners as $eventListened => $eventListeners) { + $this->write("\n".sprintf('## %s', $eventListened)."\n"); + + foreach ($eventListeners as $order => $eventListener) { + $this->write("\n".sprintf('### Listener %d', $order + 1)."\n"); + $this->describeCallable($eventListener); + } + } + } + } + + /** + * {@inheritdoc} + */ + protected function describeCallable($callable, array $options = array()) + { + $string = ''; + + if (is_array($callable)) { + $string .= "\n- Type: `function`"; + + if (is_object($callable[0])) { + $string .= "\n".sprintf('- Name: `%s`', $callable[1]); + $string .= "\n".sprintf('- Class: `%s`', get_class($callable[0])); + } else { + if (0 !== strpos($callable[1], 'parent::')) { + $string .= "\n".sprintf('- Name: `%s`', $callable[1]); + $string .= "\n".sprintf('- Class: `%s`', $callable[0]); + $string .= "\n- Static: yes"; + } else { + $string .= "\n".sprintf('- Name: `%s`', substr($callable[1], 8)); + $string .= "\n".sprintf('- Class: `%s`', $callable[0]); + $string .= "\n- Static: yes"; + $string .= "\n- Parent: yes"; + } + } + + return $this->write($string."\n"); + } + + if (is_string($callable)) { + $string .= "\n- Type: `function`"; + + if (false === strpos($callable, '::')) { + $string .= "\n".sprintf('- Name: `%s`', $callable); + } else { + $callableParts = explode('::', $callable); + + $string .= "\n".sprintf('- Name: `%s`', $callableParts[1]); + $string .= "\n".sprintf('- Class: `%s`', $callableParts[0]); + $string .= "\n- Static: yes"; + } + + return $this->write($string."\n"); + } + + if ($callable instanceof \Closure) { + $string .= "\n- Type: `closure`"; + + return $this->write($string."\n"); + } + + if (method_exists($callable, '__invoke')) { + $string .= "\n- Type: `object`"; + $string .= "\n".sprintf('- Name: `%s`', get_class($callable)); + + return $this->write($string."\n"); + } + + throw new \InvalidArgumentException('Callable is not describable.'); + } + + /** + * @param array $array + * + * @return string + */ + 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 000000000000..0fe5fa70f59b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -0,0 +1,449 @@ + + * + * 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\Table; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + * + * @internal + */ +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 Table($this->getOutput()); + $table->setStyle('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); + $table->render(); + } + + /** + * {@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(), + 'Path Regex '.$route->compile()->getRegex(), + 'Host '.('' !== $route->getHost() ? $route->getHost() : 'ANY'), + 'Host Regex '.('' !== $route->getHost() ? $route->compile()->getHostRegex() : ''), + 'Scheme '.($route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'), + 'Method '.($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY'), + 'Class '.get_class($route), + 'Defaults '.$this->formatRouterConfig($route->getDefaults()), + 'Requirements '.($requirements ? $this->formatRouterConfig($requirements) : 'NO CUSTOM'), + 'Options '.$this->formatRouterConfig($route->getOptions()), + ); + + if (isset($options['name'])) { + array_unshift($description, 'Name '.$options['name']); + array_unshift($description, $this->formatSection('router', sprintf('Route "%s"', $options['name']))); + } + + $this->writeText(implode("\n", $description)."\n", $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameters(ParameterBag $parameters, array $options = array()) + { + $table = new Table($this->getOutput()); + $table->setStyle('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); + $table->render(); + } + + /** + * {@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 Table($this->getOutput()); + $table->setStyle('compact'); + $table->setHeaders(array_merge(array('Service ID'), $tagsNames, array('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->getClass()))); + } else { + $table->addRow(array_merge(array(' "'), $tagValues, array(''))); + } + } + } else { + $table->addRow(array($serviceId, $definition->getClass())); + } + } elseif ($definition instanceof Alias) { + $alias = $definition; + $table->addRow(array_merge(array($serviceId, sprintf('alias for "%s"', $alias)), $tagsCount ? array_fill(0, $tagsCount, '') : array())); + } else { + $table->addRow(array_merge(array($serviceId, get_class($definition)), $tagsCount ? array_fill(0, $tagsCount, '') : array())); + } + } + + $table->render(); + } + + /** + * {@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('Lazy %s', $definition->isLazy() ? 'yes' : 'no'); + if (method_exists($definition, 'isSynchronized')) { + $description[] = sprintf('Synchronized %s', $definition->isSynchronized(false) ? 'yes' : 'no'); + } + $description[] = sprintf('Abstract %s', $definition->isAbstract() ? 'yes' : 'no'); + + if ($definition->getFile()) { + $description[] = sprintf('Required File %s', $definition->getFile() ?: '-'); + } + + if ($definition->getFactoryClass(false)) { + $description[] = sprintf('Factory Class %s', $definition->getFactoryClass(false)); + } + + if ($definition->getFactoryService(false)) { + $description[] = sprintf('Factory Service %s', $definition->getFactoryService(false)); + } + + if ($definition->getFactoryMethod(false)) { + $description[] = sprintf('Factory Method %s', $definition->getFactoryMethod(false)); + } + + if ($factory = $definition->getFactory()) { + if (is_array($factory)) { + if ($factory[0] instanceof Reference) { + $description[] = sprintf('Factory Service %s', $factory[0]); + } elseif ($factory[0] instanceof Definition) { + throw new \InvalidArgumentException('Factory is not describable.'); + } else { + $description[] = sprintf('Factory Class %s', $factory[0]); + } + $description[] = sprintf('Factory Method %s', $factory[1]); + } else { + $description[] = sprintf('Factory Function %s', $factory); + } + } + + $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\n", (string) $alias), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameter($parameter, array $options = array()) + { + $this->writeText($this->formatParameter($parameter), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array()) + { + $event = array_key_exists('event', $options) ? $options['event'] : null; + + $label = 'Registered listeners'; + if (null !== $event) { + $label .= sprintf(' for event %s', $event); + } else { + $label .= ' by event'; + } + + $this->writeText($this->formatSection('event_dispatcher', $label)."\n", $options); + + $registeredListeners = $eventDispatcher->getListeners($event); + + if (null !== $event) { + $this->writeText("\n"); + $table = new Table($this->getOutput()); + $table->getStyle()->setCellHeaderFormat('%s'); + $table->setHeaders(array('Order', 'Callable')); + + foreach ($registeredListeners as $order => $listener) { + $table->addRow(array(sprintf('#%d', $order + 1), $this->formatCallable($listener))); + } + + $table->render(); + } else { + ksort($registeredListeners); + foreach ($registeredListeners as $eventListened => $eventListeners) { + $this->writeText(sprintf("\n[Event] %s\n", $eventListened), $options); + + $table = new Table($this->getOutput()); + $table->getStyle()->setCellHeaderFormat('%s'); + $table->setHeaders(array('Order', 'Callable')); + + foreach ($eventListeners as $order => $eventListener) { + $table->addRow(array(sprintf('#%d', $order + 1), $this->formatCallable($eventListener))); + } + + $table->render(); + } + } + } + + /** + * {@inheritdoc} + */ + protected function describeCallable($callable, array $options = array()) + { + $this->writeText($this->formatCallable($callable), $options); + } + + /** + * @param array $array + * + * @return string + */ + private function formatRouterConfig(array $array) + { + if (!count($array)) { + return 'NONE'; + } + + $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 callable $callable + * + * @return string + */ + private function formatCallable($callable) + { + if (is_array($callable)) { + if (is_object($callable[0])) { + return sprintf('%s::%s()', get_class($callable[0]), $callable[1]); + } + + return sprintf('%s::%s()', $callable[0], $callable[1]); + } + + if (is_string($callable)) { + return sprintf('%s()', $callable); + } + + if ($callable instanceof \Closure) { + return '\Closure()'; + } + + if (method_exists($callable, '__invoke')) { + return sprintf('%s::__invoke()', get_class($callable)); + } + + throw new \InvalidArgumentException('Callable is not describable.'); + } + + /** + * @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 000000000000..c37a9009fcff --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -0,0 +1,537 @@ + + * + * 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\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jean-François Simon + * + * @internal + */ +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)); + } + + /** + * {@inheritdoc} + */ + protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array()) + { + $this->writeDocument($this->getEventDispatcherListenersDocument($eventDispatcher, array_key_exists('event', $options) ? $options['event'] : null)); + } + + /** + * {@inheritdoc} + */ + protected function describeCallable($callable, array $options = array()) + { + $this->writeDocument($this->getCallableDocument($callable)); + } + + /** + * {@inheritdoc} + */ + protected function describeContainerParameter($parameter, array $options = array()) + { + $this->writeDocument($this->getContainerParameterDocument($parameter, $options)); + } + + /** + * 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 bool $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 bool $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 bool $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 (method_exists($definition, 'getFactoryMethod')) { + if ($definition->getFactoryClass(false)) { + $serviceXML->setAttribute('factory-class', $definition->getFactoryClass(false)); + } + + if ($definition->getFactoryService(false)) { + $serviceXML->setAttribute('factory-service', $definition->getFactoryService(false)); + } + + if ($definition->getFactoryMethod(false)) { + $serviceXML->setAttribute('factory-method', $definition->getFactoryMethod(false)); + } + } + + if ($factory = $definition->getFactory()) { + $serviceXML->appendChild($factoryXML = $dom->createElement('factory')); + + if (is_array($factory)) { + if ($factory[0] instanceof Reference) { + $factoryXML->setAttribute('service', (string) $factory[0]); + } elseif ($factory[0] instanceof Definition) { + throw new \InvalidArgumentException('Factory is not describable.'); + } else { + $factoryXML->setAttribute('class', $factory[0]); + } + $factoryXML->setAttribute('method', $factory[1]); + } else { + $factoryXML->setAttribute('function', $factory); + } + } + + $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'); + if (method_exists($definition, 'isSynchronized')) { + $serviceXML->setAttribute('synchronized', $definition->isSynchronized(false) ? '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; + } + + /** + * @param string $parameter + * @param array $options + * + * @return \DOMDocument + */ + private function getContainerParameterDocument($parameter, $options = array()) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($parameterXML = $dom->createElement('parameter')); + + if (isset($options['parameter'])) { + $parameterXML->setAttribute('key', $options['parameter']); + } + + $parameterXML->appendChild(new \DOMText($this->formatParameter($parameter))); + + return $dom; + } + + /** + * @param EventDispatcherInterface $eventDispatcher + * @param string|null $event + * + * @return \DOMDocument + */ + private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, $event = null) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($eventDispatcherXML = $dom->createElement('event-dispatcher')); + + $registeredListeners = $eventDispatcher->getListeners($event); + if (null !== $event) { + foreach ($registeredListeners as $listener) { + $callableXML = $this->getCallableDocument($listener); + + $eventDispatcherXML->appendChild($eventDispatcherXML->ownerDocument->importNode($callableXML->childNodes->item(0), true)); + } + } else { + ksort($registeredListeners); + + foreach ($registeredListeners as $eventListened => $eventListeners) { + $eventDispatcherXML->appendChild($eventXML = $dom->createElement('event')); + $eventXML->setAttribute('name', $eventListened); + + foreach ($eventListeners as $eventListener) { + $callableXML = $this->getCallableDocument($eventListener); + + $eventXML->appendChild($eventXML->ownerDocument->importNode($callableXML->childNodes->item(0), true)); + } + } + } + + return $dom; + } + + /** + * @param callable $callable + * + * @return \DOMDocument + */ + private function getCallableDocument($callable) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($callableXML = $dom->createElement('callable')); + + if (is_array($callable)) { + $callableXML->setAttribute('type', 'function'); + + if (is_object($callable[0])) { + $callableXML->setAttribute('name', $callable[1]); + $callableXML->setAttribute('class', get_class($callable[0])); + } else { + if (0 !== strpos($callable[1], 'parent::')) { + $callableXML->setAttribute('name', $callable[1]); + $callableXML->setAttribute('class', $callable[0]); + $callableXML->setAttribute('static', 'true'); + } else { + $callableXML->setAttribute('name', substr($callable[1], 8)); + $callableXML->setAttribute('class', $callable[0]); + $callableXML->setAttribute('static', 'true'); + $callableXML->setAttribute('parent', 'true'); + } + } + + return $dom; + } + + if (is_string($callable)) { + $callableXML->setAttribute('type', 'function'); + + if (false === strpos($callable, '::')) { + $callableXML->setAttribute('name', $callable); + } else { + $callableParts = explode('::', $callable); + + $callableXML->setAttribute('name', $callableParts[1]); + $callableXML->setAttribute('class', $callableParts[0]); + $callableXML->setAttribute('static', 'true'); + } + + return $dom; + } + + if ($callable instanceof \Closure) { + $callableXML->setAttribute('type', 'closure'); + + return $dom; + } + + if (method_exists($callable, '__invoke')) { + $callableXML->setAttribute('type', 'object'); + $callableXML->setAttribute('name', get_class($callable)); + + return $dom; + } + + throw new \InvalidArgumentException('Callable is not describable.'); + } +} 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 000000000000..2599f0090e5d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Helper/DescriptorHelper.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\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 + * + * @internal + */ +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 8dc35f2962cf..ecdb73598c1b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -18,6 +18,8 @@ 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\Security\Csrf\CsrfToken; use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormBuilder; @@ -62,7 +64,7 @@ public function generateUrl($route, $parameters = array(), $referenceType = UrlG public function forward($controller, array $path = array(), array $query = array()) { $path['_controller'] = $controller; - $subRequest = $this->container->get('request')->duplicate($query, null, $path); + $subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate($query, null, $path); return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); } @@ -80,6 +82,73 @@ public function redirect($url, $status = 302) return new RedirectResponse($url, $status); } + /** + * Returns a RedirectResponse to the given route with the given parameters. + * + * @param string $route The name of the route + * @param array $parameters An array of parameters + * @param int $status The status code to use for the Response + * + * @return RedirectResponse + */ + protected function redirectToRoute($route, array $parameters = array(), $status = 302) + { + return $this->redirect($this->generateUrl($route, $parameters), $status); + } + + /** + * Adds a flash message to the current session for type. + * + * @param string $type The type + * @param string $message The message + * + * @throws \LogicException + */ + protected function addFlash($type, $message) + { + if (!$this->container->has('session')) { + throw new \LogicException('You can not use the addFlash method if sessions are disabled.'); + } + + $this->container->get('session')->getFlashBag()->add($type, $message); + } + + /** + * Checks if the attributes are granted against the current authentication token and optionally supplied object. + * + * @param mixed $attributes The attributes + * @param mixed $object The object + * + * @return bool + * + * @throws \LogicException + */ + protected function isGranted($attributes, $object = null) + { + if (!$this->container->has('security.authorization_checker')) { + throw new \LogicException('The SecurityBundle is not registered in your application.'); + } + + return $this->container->get('security.authorization_checker')->isGranted($attributes, $object); + } + + /** + * Throws an exception unless the attributes are granted against the current authentication token and optionally + * supplied object. + * + * @param mixed $attributes The attributes + * @param mixed $object The object + * @param string $message The message passed to the exception + * + * @throws AccessDeniedException + */ + protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.') + { + if (!$this->isGranted($attributes, $object)) { + throw $this->createAccessDeniedException($message); + } + } + /** * Returns a rendered view. * @@ -140,8 +209,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 */ @@ -150,6 +219,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. * @@ -181,10 +267,16 @@ public function createFormBuilder($data = null, array $options = array()) * Shortcut to return the request service. * * @return Request + * + * @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'); + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.4 and will be removed in 3.0. The only reliable way to get the "Request" object is to inject it in the action method.', E_USER_DEPRECATED); + + return $this->container->get('request_stack')->getCurrentRequest(); } /** @@ -204,7 +296,7 @@ public function getDoctrine() } /** - * Get a user from the Security Context. + * Get a user from the Security Token Storage. * * @return mixed * @@ -214,15 +306,16 @@ public function getDoctrine() */ public function getUser() { - if (!$this->container->has('security.context')) { + if (!$this->container->has('security.token_storage')) { throw new \LogicException('The SecurityBundle is not registered in your application.'); } - if (null === $token = $this->container->get('security.context')->getToken()) { + if (null === $token = $this->container->get('security.token_storage')->getToken()) { return; } if (!is_object($user = $token->getUser())) { + // e.g. anonymous authentication return; } @@ -242,7 +335,7 @@ public function has($id) } /** - * Gets a service by id. + * Gets a container service by its id. * * @param string $id The service id * @@ -250,6 +343,39 @@ public function has($id) */ public function get($id) { + if ('request' === $id) { + @trigger_error('The "request" service is deprecated and will be removed in 3.0. Add a typehint for Symfony\\Component\\HttpFoundation\\Request to your controller parameters to retrieve the request instead.', E_USER_DEPRECATED); + } + return $this->container->get($id); } + + /** + * Gets a container configuration parameter by its name. + * + * @param string $name The parameter name + * + * @return mixed + */ + protected function getParameter($name) + { + return $this->container->getParameter($name); + } + + /** + * Checks the validity of a CSRF token. + * + * @param string $id The id used when generating the token + * @param string $token The actual token sent with the request that should be validated + * + * @return bool + */ + protected function isCsrfTokenValid($id, $token) + { + if (!$this->container->has('security.csrf.token_manager')) { + throw new \LogicException('CSRF protection is not enabled in your application.'); + } + + return $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($id, $token)); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php index 8972f7d1fee1..374d0eba10ff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php @@ -46,6 +46,7 @@ public function __construct(KernelInterface $kernel) */ public function parse($controller) { + $originalController = $controller; if (3 !== count($parts = explode(':', $controller))) { throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "a:b:c" controller string.', $controller)); } @@ -54,15 +55,31 @@ public function parse($controller) $controller = str_replace('/', '\\', $controller); $bundles = array(); - // this throws an exception if there is no such bundle - foreach ($this->kernel->getBundle($bundle, false) as $b) { + try { + // this throws an exception if there is no such bundle + $allBundles = $this->kernel->getBundle($bundle, false); + } catch (\InvalidArgumentException $e) { + $message = sprintf( + 'The "%s" (from the _controller value "%s") does not exist or is not enabled in your kernel!', + $bundle, + $originalController + ); + + if ($alternative = $this->findAlternative($bundle)) { + $message .= sprintf(' Did you mean "%s:%s:%s"?', $alternative, $controller, $action); + } + + throw new \InvalidArgumentException($message, 0, $e); + } + + foreach ($allBundles as $b) { $try = $b->getNamespace().'\\Controller\\'.$controller.'Controller'; if (class_exists($try)) { return $try.'::'.$action.'Action'; } $bundles[] = $b->getName(); - $msg = sprintf('Unable to find controller "%s:%s" - class "%s" does not exist.', $bundle, $controller, $try); + $msg = sprintf('The _controller value "%s:%s:%s" maps to a "%s" class, but this class was not found. Create this class or check the spelling of the class and its namespace.', $bundle, $controller, $action, $try); } if (count($bundles) > 1) { @@ -100,4 +117,35 @@ public function build($controller) throw new \InvalidArgumentException(sprintf('Unable to find a bundle that defines controller "%s".', $controller)); } + + /** + * Attempts to find a bundle that is *similar* to the given bundle name. + * + * @param string $nonExistentBundleName + * + * @return string + */ + private function findAlternative($nonExistentBundleName) + { + $bundleNames = array_map(function ($b) { + return $b->getName(); + }, $this->kernel->getBundles()); + + $alternative = null; + $shortest = null; + foreach ($bundleNames as $bundleName) { + // if there's a partial match, return it immediately + if (false !== strpos($bundleName, $nonExistentBundleName)) { + return $bundleName; + } + + $lev = levenshtein($nonExistentBundleName, $bundleName); + if ($lev <= strlen($nonExistentBundleName) / 3 && ($alternative === null || $lev < $shortest)) { + $alternative = $bundleName; + $shortest = $lev; + } + } + + return $alternative; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php index 5cf1fba6bafc..31593b9b8050 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php @@ -63,22 +63,31 @@ protected function createController($controller) list($service, $method) = explode(':', $controller, 2); return array($this->container->get($service), $method); + } elseif ($this->container->has($controller) && method_exists($service = $this->container->get($controller), '__invoke')) { + return $service; } else { throw new \LogicException(sprintf('Unable to parse the controller name "%s".', $controller)); } } - list($class, $method) = explode('::', $controller, 2); + return parent::createController($controller); + } - if (!class_exists($class)) { - throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + /** + * {@inheritdoc} + */ + protected function instantiateController($class) + { + if ($this->container->has($class)) { + return $this->container->get($class); } - $controller = new $class(); + $controller = parent::instantiateController($class); + if ($controller instanceof ContainerAwareInterface) { $controller->setContainer($this->container); } - return array($controller, $method); + return $controller; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/AjaxDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AjaxDataCollector.php new file mode 100644 index 000000000000..0dff2b2a57d4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AjaxDataCollector.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\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; + +/** + * AjaxDataCollector. + * + * @author Bart van den Burg + */ +class AjaxDataCollector extends DataCollector +{ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + // all collecting is done client side + } + + public function getName() + { + return 'ajax'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddConsoleCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddConsoleCommandPass.php new file mode 100644 index 000000000000..4ab0d82ed120 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddConsoleCommandPass.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\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()); + if (!is_subclass_of($class, '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/Compiler/AddExpressionLanguageProvidersPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php new file mode 100644 index 000000000000..6510d02c8306 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.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\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Registers the expression language providers. + * + * @author Fabien Potencier + */ +class AddExpressionLanguageProvidersPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + // routing + if ($container->has('router')) { + $definition = $container->findDefinition('router'); + foreach ($container->findTaggedServiceIds('routing.expression_language_provider') as $id => $attributes) { + $definition->addMethodCall('addExpressionLanguageProvider', array(new Reference($id))); + } + } + + // security + if ($container->has('security.access.expression_voter')) { + $definition = $container->findDefinition('security.access.expression_voter'); + foreach ($container->findTaggedServiceIds('security.expression_language_provider') as $id => $attributes) { + $definition->addMethodCall('addExpressionLanguageProvider', array(new Reference($id))); + } + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php index bf9f33811199..6f58fc21bebf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddValidatorInitializersPass.php @@ -19,15 +19,17 @@ class AddValidatorInitializersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { - if (!$container->hasDefinition('validator')) { + if (!$container->hasDefinition('validator.builder')) { return; } + $validatorBuilder = $container->getDefinition('validator.builder'); + $initializers = array(); foreach ($container->findTaggedServiceIds('validator.initializer') as $id => $attributes) { $initializers[] = new Reference($id); } - $container->getDefinition('validator')->replaceArgument(4, $initializers); + $validatorBuilder->addMethodCall('addObjectInitializers', array($initializers)); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php index 467ecff28307..98fa5c95a15e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php @@ -19,7 +19,7 @@ /** * Dumps the ContainerBuilder to a cache file so that it can be used by - * debugging tools such as the container:debug console command. + * debugging tools such as the debug:container console command. * * @author Ryan Weaver * @author Fabien Potencier diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/FragmentRendererPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/FragmentRendererPass.php index e3284ab45c24..667d92df0159 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/FragmentRendererPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/FragmentRendererPass.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; +@trigger_error('The '.__NAMESPACE__.'\FragmentRendererPass class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass instead.', E_USER_DEPRECATED); + use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -19,6 +21,8 @@ * Adds services tagged kernel.fragment_renderer as HTTP content rendering strategies. * * @author Fabien Potencier + * + * @deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass instead. */ class FragmentRendererPass implements CompilerPassInterface { @@ -33,9 +37,8 @@ public function process(ContainerBuilder $container) // We must assume that the class value has been correctly filled, even if the service is created by a factory $class = $container->getDefinition($id)->getClass(); - $refClass = new \ReflectionClass($class); $interface = 'Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface'; - if (!$refClass->implementsInterface($interface)) { + if (!is_subclass_of($class, $interface)) { throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface)); } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php new file mode 100644 index 000000000000..dda6bc2959a7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.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\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Abdellatif Ait boudad + */ +class LoggingTranslatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasAlias('logger') || !$container->hasAlias('translator')) { + return; + } + + // skip if the symfony/translation version is lower than 2.6 + if (!interface_exists('Symfony\Component\Translation\TranslatorBagInterface')) { + return; + } + + if ($container->hasParameter('translator.logging') && $container->getParameter('translator.logging')) { + $translatorAlias = $container->getAlias('translator'); + $definition = $container->getDefinition((string) $translatorAlias); + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + + if (is_subclass_of($class, 'Symfony\Component\Translation\TranslatorInterface') && is_subclass_of($class, 'Symfony\Component\Translation\TranslatorBagInterface')) { + $container->getDefinition('translator.logging')->setDecoratedService('translator'); + $container->getDefinition('translation.warmer')->replaceArgument(0, new Reference('translator.logging.inner')); + } + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingAssetHelperPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingAssetHelperPass.php new file mode 100644 index 000000000000..4a97e0f746eb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingAssetHelperPass.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +@trigger_error('The '.__NAMESPACE__.'\TemplatingAssetHelperPass class is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + +/** + * @deprecated since 2.7, will be removed in 3.0 + */ +class TemplatingAssetHelperPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('templating.helper.assets')) { + return; + } + + $assetsHelperDefinition = $container->getDefinition('templating.helper.assets'); + $args = $assetsHelperDefinition->getArguments(); + + if ('request' === $this->getPackageScope($container, $args[0])) { + $assetsHelperDefinition->setScope('request'); + + return; + } + + if (!array_key_exists(1, $args)) { + return; + } + + if (!is_array($args[1])) { + return; + } + + foreach ($args[1] as $arg) { + if ('request' === $this->getPackageScope($container, $arg)) { + $assetsHelperDefinition->setScope('request'); + + break; + } + } + } + + private function getPackageScope(ContainerBuilder $container, $package) + { + if ($package instanceof Reference) { + return $container->findDefinition((string) $package)->getScope(); + } + + if ($package instanceof Definition) { + return $package->getScope(); + } + + // Someone did some voodoo with a compiler pass. So we ignore this + // 'package'. Can we be sure, it's a package anyway? + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index c7086f72c45c..b21b3ee8df76 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -22,6 +22,16 @@ */ class Configuration implements ConfigurationInterface { + private $debug; + + /** + * @param bool $debug Whether debugging is enabled or not + */ + public function __construct($debug) + { + $this->debug = (bool) $debug; + } + /** * Generates the configuration tree builder. * @@ -33,10 +43,87 @@ public function getConfigTreeBuilder() $rootNode = $treeBuilder->root('framework'); $rootNode + // Check deprecations before the config is processed to ensure + // the setting has been explicitly defined in a configuration file. + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['csrf_protection']['field_name']); }) + ->then(function ($v) { + @trigger_error('The framework.csrf_protection.field_name configuration key is deprecated since version 2.4 and will be removed in 3.0. Use the framework.form.csrf_protection.field_name configuration key instead', E_USER_DEPRECATED); + + return $v; + }) + ->end() + ->validate() + ->ifTrue(function ($v) { return !isset($v['assets']); }) + ->then(function ($v) { + if (!isset($v['templating']) + || !$v['templating']['assets_version'] + && !count($v['templating']['assets_base_urls']['http']) + && !count($v['templating']['assets_base_urls']['ssl']) + && !count($v['templating']['packages']) + ) { + $v['assets'] = array( + 'version' => null, + 'version_format' => '%%s?%%s', + 'base_path' => '', + 'base_urls' => array(), + 'packages' => array(), + ); + } + + return $v; + }) + ->end() + ->validate() + ->ifTrue(function ($v) { return isset($v['templating']); }) + ->then(function ($v) { + if ($v['templating']['assets_version'] + || count($v['templating']['assets_base_urls']['http']) + || count($v['templating']['assets_base_urls']['ssl']) + || count($v['templating']['packages']) + ) { + @trigger_error('The assets settings under framework.templating are deprecated since version 2.7 and will be removed in 3.0. Use the framework.assets configuration key instead', E_USER_DEPRECATED); + + // convert the old configuration to the new one + if (isset($v['assets'])) { + throw new \LogicException('You cannot use assets settings under "framework.templating" and "assets" configurations in the same project.'); + } + + $v['assets'] = array( + 'version' => $v['templating']['assets_version'], + 'version_format' => $v['templating']['assets_version_format'], + 'base_path' => '', + 'base_urls' => array_values(array_unique(array_merge($v['templating']['assets_base_urls']['http'], $v['templating']['assets_base_urls']['ssl']))), + 'packages' => array(), + ); + + foreach ($v['templating']['packages'] as $name => $config) { + $v['assets']['packages'][$name] = array( + 'version' => null === $config['version'] ? null : (string) $config['version'], + 'version_format' => $config['version_format'], + 'base_path' => '', + 'base_urls' => array_values(array_unique(array_merge($config['base_urls']['http'], $config['base_urls']['ssl']))), + ); + } + } + + unset($v['templating']['assets_version'], $v['templating']['assets_version_format'], $v['templating']['assets_base_urls'], $v['templating']['packages']); + + return $v; + }) + ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['validation']['api']); }) + ->then(function ($v) { + @trigger_error('The validation.api configuration key is deprecated since version 2.7 and will be removed in 3.0', E_USER_DEPRECATED); + + return $v; + }) + ->end() ->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') @@ -79,21 +166,43 @@ public function getConfigTreeBuilder() ->end() ; + $this->addCsrfSection($rootNode); $this->addFormSection($rootNode); $this->addEsiSection($rootNode); + $this->addSsiSection($rootNode); $this->addFragmentsSection($rootNode); $this->addProfilerSection($rootNode); $this->addRouterSection($rootNode); $this->addSessionSection($rootNode); + $this->addRequestSection($rootNode); $this->addTemplatingSection($rootNode); + $this->addAssetsSection($rootNode); $this->addTranslatorSection($rootNode); $this->addValidationSection($rootNode); $this->addAnnotationsSection($rootNode); $this->addSerializerSection($rootNode); + $this->addPropertyAccessSection($rootNode); return $treeBuilder; } + private function addCsrfSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('csrf_protection') + ->canBeEnabled() + ->children() + ->scalarNode('field_name') + ->defaultValue('_token') + ->info('Deprecated since version 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 @@ -101,11 +210,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() @@ -124,6 +239,17 @@ private function addEsiSection(ArrayNodeDefinition $rootNode) ; } + private function addSsiSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('ssi') + ->info('ssi configuration') + ->canBeEnabled() + ->end() + ->end(); + } + private function addFragmentsSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -157,13 +283,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() @@ -216,9 +346,42 @@ private function addSessionSection(ArrayNodeDefinition $rootNode) ->booleanNode('cookie_secure')->end() ->booleanNode('cookie_httponly')->end() ->scalarNode('gc_divisor')->end() - ->scalarNode('gc_probability')->end() + ->scalarNode('gc_probability')->defaultValue(1)->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() + ; + } + + private function addRequestSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('request') + ->info('request configuration') + ->canBeUnset() + ->fixXmlConfig('format') + ->children() + ->arrayNode('formats') + ->useAttributeAsKey('name') + ->prototype('array') + ->beforeNormalization() + ->ifTrue(function ($v) { return is_array($v) && isset($v['mime_type']); }) + ->then(function ($v) { return $v['mime_type']; }) + ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return array($v); }) + ->end() + ->prototype('scalar')->end() + ->end() + ->end() ->end() ->end() ->end() @@ -253,8 +416,8 @@ private function addTemplatingSection(ArrayNodeDefinition $rootNode) ->info('templating configuration') ->canBeUnset() ->children() - ->scalarNode('assets_version')->defaultValue(null)->end() - ->scalarNode('assets_version_format')->defaultValue('%%s?%%s')->end() + ->scalarNode('assets_version')->defaultNull()->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.')->end() + ->scalarNode('assets_version_format')->defaultValue('%%s?%%s')->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.')->end() ->scalarNode('hinclude_default_template')->defaultNull()->end() ->arrayNode('form') ->addDefaultsIfNotSet() @@ -276,6 +439,7 @@ private function addTemplatingSection(ArrayNodeDefinition $rootNode) ->fixXmlConfig('assets_base_url') ->children() ->arrayNode('assets_base_urls') + ->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.') ->performNoDeepMerging() ->addDefaultsIfNotSet() ->beforeNormalization() @@ -323,11 +487,18 @@ private function addTemplatingSection(ArrayNodeDefinition $rootNode) ->fixXmlConfig('package') ->children() ->arrayNode('packages') + ->info('Deprecated since 2.7, will be removed in 3.0. Use the new assets entry instead.') ->useAttributeAsKey('name') ->prototype('array') ->fixXmlConfig('base_url') ->children() - ->scalarNode('version')->defaultNull()->end() + ->scalarNode('version') + ->defaultNull() + ->beforeNormalization() + ->ifTrue(function ($v) { return '' === $v; }) + ->then(function ($v) { return; }) + ->end() + ->end() ->scalarNode('version_format')->defaultValue('%%s?%%s')->end() ->arrayNode('base_urls') ->performNoDeepMerging() @@ -358,6 +529,59 @@ private function addTemplatingSection(ArrayNodeDefinition $rootNode) ; } + private function addAssetsSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('assets') + ->info('assets configuration') + ->canBeUnset() + ->fixXmlConfig('base_url') + ->children() + ->scalarNode('version')->defaultNull()->end() + ->scalarNode('version_format')->defaultValue('%%s?%%s')->end() + ->scalarNode('base_path')->defaultValue('')->end() + ->arrayNode('base_urls') + ->requiresAtLeastOneElement() + ->beforeNormalization() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return array($v); }) + ->end() + ->prototype('scalar')->end() + ->end() + ->end() + ->fixXmlConfig('package') + ->children() + ->arrayNode('packages') + ->useAttributeAsKey('name') + ->prototype('array') + ->fixXmlConfig('base_url') + ->children() + ->scalarNode('version') + ->beforeNormalization() + ->ifTrue(function ($v) { return '' === $v; }) + ->then(function ($v) { return; }) + ->end() + ->end() + ->scalarNode('version_format')->defaultNull()->end() + ->scalarNode('base_path')->defaultValue('')->end() + ->arrayNode('base_urls') + ->requiresAtLeastOneElement() + ->beforeNormalization() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return array($v); }) + ->end() + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + private function addTranslatorSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -372,6 +596,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->prototype('scalar')->end() ->defaultValue(array('en')) ->end() + ->booleanNode('logging')->defaultValue($this->debug)->end() ->end() ->end() ->end() @@ -386,9 +611,33 @@ private function addValidationSection(ArrayNodeDefinition $rootNode) ->info('validation configuration') ->canBeEnabled() ->children() - ->scalarNode('cache')->end() + ->scalarNode('cache') + ->beforeNormalization() + // Can be removed in 3.0, once ApcCache support is dropped + ->ifString()->then(function ($v) { return 'apc' === $v ? 'validator.mapping.cache.apc' : $v; }) + ->end() + ->end() ->booleanNode('enable_annotations')->defaultFalse()->end() + ->arrayNode('static_method') + ->defaultValue(array('loadValidatorMetadata')) + ->prototype('scalar')->end() + ->treatFalseLike(array()) + ->validate() + ->ifTrue(function ($v) { return !is_array($v); }) + ->then(function ($v) { return (array) $v; }) + ->end() + ->end() ->scalarNode('translation_domain')->defaultValue('validators')->end() + ->booleanNode('strict_email')->defaultFalse()->end() + ->enumNode('api') + ->info('Deprecated since version 2.7, to be removed in 3.0') + ->values(array('2.4', '2.5', '2.5-bc', 'auto')) + ->beforeNormalization() + // XML/YAML parse as numbers, not as strings + ->ifTrue(function ($v) { return is_scalar($v); }) + ->then(function ($v) { return (string) $v; }) + ->end() + ->end() ->end() ->end() ->end() @@ -405,7 +654,7 @@ private function addAnnotationsSection(ArrayNodeDefinition $rootNode) ->children() ->scalarNode('cache')->defaultValue('file')->end() ->scalarNode('file_cache_dir')->defaultValue('%kernel.cache_dir%/annotations')->end() - ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() + ->booleanNode('debug')->defaultValue($this->debug)->end() ->end() ->end() ->end() @@ -419,6 +668,26 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode) ->arrayNode('serializer') ->info('serializer configuration') ->canBeEnabled() + ->children() + ->booleanNode('enable_annotations')->defaultFalse()->end() + ->scalarNode('cache')->end() + ->end() + ->end() + ->end() + ; + } + + private function addPropertyAccessSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('property_access') + ->addDefaultsIfNotSet() + ->info('Property access configuration') + ->children() + ->booleanNode('magic_call')->defaultFalse()->end() + ->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end() + ->end() ->end() ->end() ; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 4248623d3df9..15b1e1fdf3b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\Config\Resource\FileResource; @@ -20,20 +23,33 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Validator\Validation; /** * FrameworkExtension. * * @author Fabien Potencier * @author Jeremy Mikola + * @author Kévin Dunglas */ class FrameworkExtension extends Extension { + private $formConfigEnabled = false; + private $translationConfigEnabled = false; + private $sessionConfigEnabled = false; + + /** + * @var string|null + */ + private $kernelRootHash; + /** * Responds to the app.config configuration parameter. * * @param array $configs * @param ContainerBuilder $container + * + * @throws LogicException */ public function load(array $configs, ContainerBuilder $container) { @@ -48,18 +64,8 @@ public function load(array $configs, ContainerBuilder $container) // will be used and everything will still work as expected. $loader->load('translation.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); - } + // Property access is used by both the Form and the Validator component + $loader->load('property_access.xml'); $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); @@ -78,12 +84,34 @@ public function load(array $configs, ContainerBuilder $container) } if (isset($config['session'])) { + $this->sessionConfigEnabled = true; $this->registerSessionConfiguration($config['session'], $container, $loader); } + if (isset($config['request'])) { + $this->registerRequestConfiguration($config['request'], $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 (!class_exists('Symfony\Component\Validator\Validation')) { + throw new LogicException('The Validator component is required to use the Form component.'); + } + + if ($this->isConfigEnabled($container, $config['form']['csrf_protection'])) { + $config['csrf_protection']['enabled'] = true; + } + } + + $this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader); + + if (isset($config['assets'])) { + $this->registerAssetsConfiguration($config['assets'], $container, $loader); } if (isset($config['templating'])) { @@ -92,23 +120,51 @@ public function load(array $configs, ContainerBuilder $container) $this->registerValidationConfiguration($config['validation'], $container, $loader); $this->registerEsiConfiguration($config['esi'], $container, $loader); + $this->registerSsiConfiguration($config['ssi'], $container, $loader); $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); - $this->registerProfilerConfiguration($config['profiler'], $container, $loader); $this->registerTranslatorConfiguration($config['translator'], $container); + $this->registerProfilerConfiguration($config['profiler'], $container, $loader); if (isset($config['router'])) { $this->registerRouterConfiguration($config['router'], $container, $loader); } $this->registerAnnotationsConfiguration($config['annotations'], $container, $loader); + $this->registerPropertyAccessConfiguration($config['property_access'], $container); + + if (isset($config['serializer'])) { + $this->registerSerializerConfiguration($config['serializer'], $container, $loader); + } + + $loader->load('debug_prod.xml'); + $definition = $container->findDefinition('debug.debug_handlers_listener'); - if (isset($config['serializer']) && $config['serializer']['enabled']) { - $loader->load('serializer.xml'); + if ($container->hasParameter('templating.helper.code.file_link_format')) { + $definition->replaceArgument(5, '%templating.helper.code.file_link_format%'); + } + + if ($container->getParameter('kernel.debug')) { + $definition->replaceArgument(2, -1 & ~(E_COMPILE_ERROR | E_PARSE | E_ERROR | E_CORE_ERROR | E_RECOVERABLE_ERROR)); + + $loader->load('debug.xml'); + + $definition = $container->findDefinition('http_kernel'); + $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'); + } else { + $definition->replaceArgument(1, null); } $this->addClassesToCompile(array( 'Symfony\\Component\\Config\\FileLocator', + 'Symfony\\Component\\Debug\\ErrorHandler', + 'Symfony\\Component\\EventDispatcher\\Event', 'Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher', @@ -131,6 +187,14 @@ public function load(array $configs, ContainerBuilder $container) )); } + /** + * {@inheritdoc} + */ + public function getConfiguration(array $config, ContainerBuilder $container) + { + return new Configuration($container->getParameter('kernel.debug')); + } + /** * Loads Form configuration. * @@ -143,17 +207,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); } @@ -175,6 +242,22 @@ private function registerEsiConfiguration(array $config, ContainerBuilder $conta $loader->load('esi.xml'); } + /** + * Loads the SSI configuration. + * + * @param array $config An SSI configuration array + * @param ContainerBuilder $container A ContainerBuilder instance + * @param XmlFileLoader $loader An XmlFileLoader instance + */ + private function registerSsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!$this->isConfigEnabled($container, $config)) { + return; + } + + $loader->load('ssi.xml'); + } + /** * Loads the fragments configuration. * @@ -213,6 +296,15 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $loader->load('profiling.xml'); $loader->load('collectors.xml'); + if ($this->formConfigEnabled) { + $loader->load('form_debug.xml'); + } + + if ($this->translationConfigEnabled) { + $loader->load('translation_debug.xml'); + $container->getDefinition('translator.data_collector')->setDecoratedService('translator'); + } + $container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']); $container->setParameter('profiler_listener.only_master_requests', $config['only_master_requests']); @@ -241,7 +333,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 +341,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 +417,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 +444,26 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c $container->findDefinition('session.storage')->getClass(), )); } + + $container->setParameter('session.metadata.update_threshold', $config['metadata_update_threshold']); + } + + /** + * Loads the request configuration. + * + * @param array $config A request configuration array + * @param ContainerBuilder $container A ContainerBuilder instance + * @param XmlFileLoader $loader An XmlFileLoader instance + */ + private function registerRequestConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if ($config['formats']) { + $loader->load('request.xml'); + $container + ->getDefinition('request.add_request_formats_listener') + ->replaceArgument(0, $config['formats']) + ; + } } /** @@ -354,7 +477,6 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c private function registerTemplatingConfiguration(array $config, $ide, ContainerBuilder $container, XmlFileLoader $loader) { $loader->load('templating.xml'); - $loader->load('templating_php.xml'); if (!$container->hasParameter('templating.helper.code.file_link_format')) { $links = array( @@ -366,41 +488,18 @@ private function registerTemplatingConfiguration(array $config, $ide, ContainerB $container->setParameter('templating.helper.code.file_link_format', str_replace('%', '%%', ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')) ?: (isset($links[$ide]) ? $links[$ide] : $ide)); } - $container->setParameter('templating.helper.form.resources', $config['form']['resources']); + $container->setParameter('fragment.renderer.hinclude.global_template', $config['hinclude_default_template']); if ($container->getParameter('kernel.debug')) { - $loader->load('templating_debug.xml'); - - $container->setDefinition('templating.engine.php', $container->findDefinition('debug.templating.engine.php')); - $container->setAlias('debug.templating.engine.php', 'templating.engine.php'); - } - - // create package definitions and add them to the assets helper - $defaultPackage = $this->createPackageDefinition($container, $config['assets_base_urls']['http'], $config['assets_base_urls']['ssl'], $config['assets_version'], $config['assets_version_format']); - $container->setDefinition('templating.asset.default_package', $defaultPackage); - $namedPackages = array(); - foreach ($config['packages'] as $name => $package) { - $namedPackage = $this->createPackageDefinition($container, $package['base_urls']['http'], $package['base_urls']['ssl'], $package['version'], $package['version_format'], $name); - $container->setDefinition('templating.asset.package.'.$name, $namedPackage); - $namedPackages[$name] = new Reference('templating.asset.package.'.$name); - } - $container->getDefinition('templating.helper.assets')->setArguments(array( - new Reference('templating.asset.default_package'), - $namedPackages, - )); - - // Apply request scope to assets helper if one or more packages are request-scoped - $requireRequestScope = array_reduce( - $namedPackages, - function ($v, Reference $ref) use ($container) { - return $v || 'request' === $container->getDefinition($ref)->getScope(); - }, - 'request' === $defaultPackage->getScope() - ); + $logger = new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE); - if ($requireRequestScope) { - $container->getDefinition('templating.helper.assets')->setScope('request'); + $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)); } if (!empty($config['loaders'])) { @@ -432,14 +531,6 @@ function ($v, Reference $ref) use ($container) { $container->findDefinition('templating.locator')->getClass(), )); - if (in_array('php', $config['engines'], true)) { - $this->addClassesToCompile(array( - 'Symfony\\Component\\Templating\\Storage\\FileStorage', - 'Symfony\\Bundle\\FrameworkBundle\\Templating\\PhpEngine', - 'Symfony\\Bundle\\FrameworkBundle\\Templating\\Loader\\FilesystemLoader', - )); - } - $container->setParameter('templating.engines', $config['engines']); $engines = array_map(function ($engine) { return new Reference('templating.engine.'.$engine); }, $config['engines']); @@ -452,73 +543,116 @@ function ($v, Reference $ref) use ($container) { } $container->setAlias('templating', 'templating.engine.delegating'); } + + $container->getDefinition('fragment.renderer.hinclude') + ->addTag('kernel.fragment_renderer', array('alias' => 'hinclude')) + ->replaceArgument(0, new Reference('templating')) + ; + + // configure the PHP engine if needed + if (in_array('php', $config['engines'], true)) { + $loader->load('templating_php.xml'); + + $container->setParameter('templating.helper.form.resources', $config['form']['resources']); + + if ($container->getParameter('kernel.debug')) { + $loader->load('templating_debug.xml'); + + $container->setDefinition('templating.engine.php', $container->findDefinition('debug.templating.engine.php')); + $container->setAlias('debug.templating.engine.php', 'templating.engine.php'); + } + + $this->addClassesToCompile(array( + 'Symfony\\Component\\Templating\\Storage\\FileStorage', + 'Symfony\\Bundle\\FrameworkBundle\\Templating\\PhpEngine', + 'Symfony\\Bundle\\FrameworkBundle\\Templating\\Loader\\FilesystemLoader', + )); + } + + if ($container->hasDefinition('assets.packages')) { + $container->getDefinition('templating.helper.assets')->replaceArgument(0, new Reference('assets.packages')); + } else { + $container->removeDefinition('templating.helper.assets'); + } } /** - * Returns a definition for an asset package. + * Loads the assets configuration. + * + * @param array $config A assets configuration array + * @param ContainerBuilder $container A ContainerBuilder instance + * @param XmlFileLoader $loader An XmlFileLoader instance */ - private function createPackageDefinition(ContainerBuilder $container, array $httpUrls, array $sslUrls, $version, $format, $name = null) + private function registerAssetsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) { - if (!$httpUrls) { - $package = new DefinitionDecorator('templating.asset.path_package'); - $package - ->setPublic(false) - ->setScope('request') - ->replaceArgument(1, $version) - ->replaceArgument(2, $format) - ; + $loader->load('assets.xml'); + + $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], '_default'); + + $defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion); + $container->setDefinition('assets._default_package', $defaultPackage); + + $namedPackages = array(); + foreach ($config['packages'] as $name => $package) { + if (!array_key_exists('version', $package)) { + $version = $defaultVersion; + } else { + $format = $package['version_format'] ?: $config['version_format']; + $version = $this->createVersion($container, $package['version'], $format, $name); + } - return $package; + $container->setDefinition('assets._package_'.$name, $this->createPackageDefinition($package['base_path'], $package['base_urls'], $version)); + $namedPackages[$name] = new Reference('assets._package_'.$name); } - if ($httpUrls == $sslUrls) { - $package = new DefinitionDecorator('templating.asset.url_package'); - $package + $container->getDefinition('assets.packages') + ->replaceArgument(0, new Reference('assets._default_package')) + ->replaceArgument(1, $namedPackages) + ; + } + + /** + * Returns a definition for an asset package. + */ + private function createPackageDefinition($basePath, array $baseUrls, Reference $version) + { + if ($basePath && $baseUrls) { + throw new \LogicException('An asset package cannot have base URLs and base paths.'); + } + + if (!$baseUrls) { + $package = new DefinitionDecorator('assets.path_package'); + + return $package ->setPublic(false) - ->replaceArgument(0, $sslUrls) + ->replaceArgument(0, $basePath) ->replaceArgument(1, $version) - ->replaceArgument(2, $format) ; - - return $package; } - $prefix = $name ? 'templating.asset.package.'.$name : 'templating.asset.default_package'; + $package = new DefinitionDecorator('assets.url_package'); - $httpPackage = new DefinitionDecorator('templating.asset.url_package'); - $httpPackage - ->replaceArgument(0, $httpUrls) + return $package + ->setPublic(false) + ->replaceArgument(0, $baseUrls) ->replaceArgument(1, $version) - ->replaceArgument(2, $format) ; - $container->setDefinition($prefix.'.http', $httpPackage); + } - if ($sslUrls) { - $sslPackage = new DefinitionDecorator('templating.asset.url_package'); - $sslPackage - ->replaceArgument(0, $sslUrls) - ->replaceArgument(1, $version) - ->replaceArgument(2, $format) - ; - } else { - $sslPackage = new DefinitionDecorator('templating.asset.path_package'); - $sslPackage - ->setScope('request') - ->replaceArgument(1, $version) - ->replaceArgument(2, $format) - ; + private function createVersion(ContainerBuilder $container, $version, $format, $name) + { + if (null === $version) { + return new Reference('assets.empty_version_strategy'); } - $container->setDefinition($prefix.'.ssl', $sslPackage); - $package = new DefinitionDecorator('templating.asset.request_aware_package'); - $package - ->setPublic(false) - ->setScope('request') - ->replaceArgument(1, $prefix.'.http') - ->replaceArgument(2, $prefix.'.ssl') + $def = new DefinitionDecorator('assets.static_version_strategy'); + $def + ->replaceArgument(0, $version) + ->replaceArgument(1, $format) ; + $container->setDefinition('assets._version_'.$name, $def); - return $package; + return new Reference('assets._version_'.$name); } /** @@ -532,16 +666,19 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder if (!$this->isConfigEnabled($container, $config)) { return; } + $this->translationConfigEnabled = true; // Use the "real" translator instead of the identity default $container->setAlias('translator', 'translator.default'); $translator = $container->findDefinition('translator.default'); $translator->addMethodCall('setFallbackLocales', array($config['fallbacks'])); + $container->setParameter('translator.logging', $config['logging']); + // Discover translation directories $dirs = array(); - if (class_exists('Symfony\Component\Validator\Validator')) { - $r = new \ReflectionClass('Symfony\Component\Validator\Validator'); + if (class_exists('Symfony\Component\Validator\Validation')) { + $r = new \ReflectionClass('Symfony\Component\Validator\Validation'); $dirs[] = dirname($r->getFileName()).'/Resources/translations'; } @@ -553,14 +690,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'); - if (file_exists(dirname($r->getFileName()).'/../composer.json')) { - // with Symfony 2.4, the Security component was split into several subpackages - // and the translations have been moved to the symfony/security-core package - $dirs[] = dirname($r->getFileName()).'/../Resources/translations'; - } else { - // in Symfony 2.3, translations are located in the symfony/security package - $dirs[] = dirname($r->getFileName()).'/../../Resources/translations'; - } + $dirs[] = dirname($r->getFileName()).'/../Resources/translations'; } $rootDir = $container->getParameter('kernel.root_dir'); foreach ($container->getParameter('kernel.bundles') as $bundle => $class) { @@ -581,6 +711,8 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder foreach ($dirs as $dir) { $container->addResource(new DirectoryResource($dir)); } + + $files = array(); $finder = Finder::create() ->files() ->filter(function (\SplFileInfo $file) { @@ -590,10 +722,20 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder ; foreach ($finder as $file) { - // filename is domain.locale.format - list($domain, $locale, $format) = explode('.', $file->getBasename(), 3); - $translator->addMethodCall('addResource', array($format, (string) $file, $locale, $domain)); + list(, $locale) = explode('.', $file->getBasename(), 3); + if (!isset($files[$locale])) { + $files[$locale] = array(); + } + + $files[$locale][] = (string) $file; } + + $options = array_merge( + $translator->getArgument(3), + array('resource_files' => $files) + ); + + $translator->replaceArgument(3, $options); } } @@ -612,57 +754,82 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $loader->load('validator.xml'); + $validatorBuilder = $container->getDefinition('validator.builder'); + $container->setParameter('validator.translation_domain', $config['translation_domain']); - $container->setParameter('validator.mapping.loader.xml_files_loader.mapping_files', $this->getValidatorXmlMappingFiles($container)); - $container->setParameter('validator.mapping.loader.yaml_files_loader.mapping_files', $this->getValidatorYamlMappingFiles($container)); + + list($xmlMappings, $yamlMappings) = $this->getValidatorMappingFiles($container); + if (count($xmlMappings) > 0) { + $validatorBuilder->addMethodCall('addXmlMappings', array($xmlMappings)); + } + + if (count($yamlMappings) > 0) { + $validatorBuilder->addMethodCall('addYamlMappings', array($yamlMappings)); + } + + $definition = $container->findDefinition('validator.email'); + $definition->replaceArgument(0, $config['strict_email']); if (array_key_exists('enable_annotations', $config) && $config['enable_annotations']) { - $loaderChain = $container->getDefinition('validator.mapping.loader.loader_chain'); - $arguments = $loaderChain->getArguments(); - array_unshift($arguments[0], new Reference('validator.mapping.loader.annotation_loader')); - $loaderChain->setArguments($arguments); + $validatorBuilder->addMethodCall('enableAnnotationMapping', array(new Reference('annotation_reader'))); + } + + if (array_key_exists('static_method', $config) && $config['static_method']) { + foreach ($config['static_method'] as $methodName) { + $validatorBuilder->addMethodCall('addMethodMapping', array($methodName)); + } } if (isset($config['cache'])) { - $container->getDefinition('validator.mapping.class_metadata_factory') - ->replaceArgument(1, new Reference('validator.mapping.cache.'.$config['cache'])); $container->setParameter( 'validator.mapping.cache.prefix', - 'validator_'.md5($container->getParameter('kernel.root_dir')) + 'validator_'.$this->getKernelRootHash($container) ); + + $validatorBuilder->addMethodCall('setMetadataCache', array(new Reference($config['cache']))); } + + // You can use this parameter to check the API version in your own + // bundle extension classes + // This is set to 2.5-bc for compatibility with Symfony 2.5 and 2.6. + // @deprecated since version 2.7, to be removed in 3.0 + $container->setParameter('validator.api', '2.5-bc'); } - private function getValidatorXmlMappingFiles(ContainerBuilder $container) + private function getValidatorMappingFiles(ContainerBuilder $container) { - $files = array(); + $files = array(array(), array()); if (interface_exists('Symfony\Component\Form\FormInterface')) { $reflClass = new \ReflectionClass('Symfony\Component\Form\FormInterface'); - $files[] = dirname($reflClass->getFileName()).'/Resources/config/validation.xml'; - $container->addResource(new FileResource($files[0])); + $files[0][] = dirname($reflClass->getFileName()).'/Resources/config/validation.xml'; + $container->addResource(new FileResource($files[0][0])); } - foreach ($container->getParameter('kernel.bundles') as $bundle) { + $bundles = $container->getParameter('kernel.bundles'); + foreach ($bundles as $bundle) { $reflection = new \ReflectionClass($bundle); - if (is_file($file = dirname($reflection->getFileName()).'/Resources/config/validation.xml')) { - $files[] = realpath($file); + $dirname = dirname($reflection->getFileName()); + + if (is_file($file = $dirname.'/Resources/config/validation.xml')) { + $files[0][] = realpath($file); $container->addResource(new FileResource($file)); } - } - return $files; - } + if (is_file($file = $dirname.'/Resources/config/validation.yml')) { + $files[1][] = realpath($file); + $container->addResource(new FileResource($file)); + } - private function getValidatorYamlMappingFiles(ContainerBuilder $container) - { - $files = array(); + if (is_dir($dir = $dirname.'/Resources/config/validation')) { + foreach (Finder::create()->files()->in($dir)->name('*.xml') as $file) { + $files[0][] = $file->getRealpath(); + } + foreach (Finder::create()->files()->in($dir)->name('*.yml') as $file) { + $files[1][] = $file->getRealpath(); + } - foreach ($container->getParameter('kernel.bundles') as $bundle) { - $reflection = new \ReflectionClass($bundle); - if (is_file($file = dirname($reflection->getFileName()).'/Resources/config/validation.yml')) { - $files[] = realpath($file); - $container->addResource(new FileResource($file)); + $container->addResource(new DirectoryResource($dir)); } } @@ -695,6 +862,134 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde } } + private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container) + { + $container + ->getDefinition('property_accessor') + ->replaceArgument(0, $config['magic_call']) + ->replaceArgument(1, $config['throw_exception_on_invalid_index']) + ; + } + + /** + * 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'); + } + + /** + * Loads the serializer configuration. + * + * @param array $config A serializer configuration array + * @param ContainerBuilder $container A ContainerBuilder instance + * @param XmlFileLoader $loader An XmlFileLoader instance + */ + private function registerSerializerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!$config['enabled']) { + return; + } + + $loader->load('serializer.xml'); + $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); + + $serializerLoaders = array(); + if (isset($config['enable_annotations']) && $config['enable_annotations']) { + $annotationLoader = new Definition( + 'Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader', + array(new Reference('annotation_reader')) + ); + $annotationLoader->setPublic(false); + + $serializerLoaders[] = $annotationLoader; + } + + $bundles = $container->getParameter('kernel.bundles'); + foreach ($bundles as $bundle) { + $reflection = new \ReflectionClass($bundle); + $dirname = dirname($reflection->getFileName()); + + if (is_file($file = $dirname.'/Resources/config/serialization.xml')) { + $definition = new Definition('Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader', array(realpath($file))); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + $container->addResource(new FileResource($file)); + } + + if (is_file($file = $dirname.'/Resources/config/serialization.yml')) { + $definition = new Definition('Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader', array(realpath($file))); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + $container->addResource(new FileResource($file)); + } + + if (is_dir($dir = $dirname.'/Resources/config/serialization')) { + foreach (Finder::create()->files()->in($dir)->name('*.xml') as $file) { + $definition = new Definition('Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader', array($file->getRealpath())); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + } + foreach (Finder::create()->files()->in($dir)->name('*.yml') as $file) { + $definition = new Definition('Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader', array($file->getRealpath())); + $definition->setPublic(false); + + $serializerLoaders[] = $definition; + } + + $container->addResource(new DirectoryResource($dir)); + } + } + + $chainLoader->replaceArgument(0, $serializerLoaders); + + if (isset($config['cache']) && $config['cache']) { + $container->setParameter( + 'serializer.mapping.cache.prefix', + 'serializer_'.$this->getKernelRootHash($container) + ); + + $container->getDefinition('serializer.mapping.class_metadata_factory')->replaceArgument( + 1, new Reference($config['cache']) + ); + } + } + + /** + * Gets a hash of the kernel root directory. + * + * @param ContainerBuilder $container + * + * @return string + */ + private function getKernelRootHash(ContainerBuilder $container) + { + if (!$this->kernelRootHash) { + $this->kernelRootHash = hash('sha256', $container->getParameter('kernel.root_dir')); + } + + return $this->kernelRootHash; + } + /** * 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 014ecb3ebde2..6c248d8a4288 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/SessionListener.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/SessionListener.php @@ -11,18 +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 @@ -34,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()) { + if (!$this->container->has('session')) { return; } - $request = $event->getRequest(); - if (!$this->container->has('session') || $request->hasSession()) { - return; - } - - $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 3b568074f3f2..b32faa2f0566 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; - } - - $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/Fragment/ContainerAwareHIncludeFragmentRenderer.php b/src/Symfony/Bundle/FrameworkBundle/Fragment/ContainerAwareHIncludeFragmentRenderer.php index 698d97908205..919ec8af3050 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Fragment/ContainerAwareHIncludeFragmentRenderer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Fragment/ContainerAwareHIncludeFragmentRenderer.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Fragment; +@trigger_error('The '.__NAMESPACE__.'\ContainerAwareHIncludeFragmentRenderer class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bundle\FrameworkBundle\Fragment\HIncludeFragmentRenderer instead.', E_USER_DEPRECATED); + use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\UriSigner; @@ -20,6 +22,8 @@ * Implements the Hinclude rendering strategy. * * @author Fabien Potencier + * + * @deprecated since version 2.7, to be removed in 3.0. Use Symfony\Bundle\FrameworkBundle\Fragment\HIncludeFragmentRenderer instead. */ class ContainerAwareHIncludeFragmentRenderer extends HIncludeFragmentRenderer { diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 3aaf22a623ab..499f39c6c95c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -13,25 +13,29 @@ 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; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheWarmerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheClearerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CompilerDebugDumpPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FragmentRendererPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass; +use Symfony\Component\Debug\ErrorHandler; 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\HttpKernel\DependencyInjection\FragmentRendererPass; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Bundle\Bundle; -use Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass; /** * Bundle. @@ -42,6 +46,8 @@ class FrameworkBundle extends Bundle { public function boot() { + ErrorHandler::register(null, false)->throwAt($this->container->getParameter('debug.error_handler.throw_at'), true); + if ($trustedProxies = $this->container->getParameter('kernel.trusted_proxies')) { Request::setTrustedProxies($trustedProxies); } @@ -71,10 +77,13 @@ 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 LoggingTranslatorPass()); $container->addCompilerPass(new AddCacheWarmerPass()); $container->addCompilerPass(new AddCacheClearerPass()); + $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); $container->addCompilerPass(new TranslationExtractorPass()); $container->addCompilerPass(new TranslationDumperPass()); $container->addCompilerPass(new FragmentRendererPass(), PassConfig::TYPE_AFTER_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index 2e5a2312dd2c..05dbfb3bd0cf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -39,7 +39,7 @@ public function __construct(HttpKernelInterface $kernel, $cacheDir = null) $this->kernel = $kernel; $this->cacheDir = $cacheDir; - parent::__construct($kernel, $this->createStore(), $this->createEsi(), array_merge(array('debug' => $kernel->isDebug()), $this->getOptions())); + parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge(array('debug' => $kernel->isDebug()), $this->getOptions())); } /** @@ -55,7 +55,7 @@ protected function forward(Request $request, $raw = false, Response $entry = nul { $this->getKernel()->boot(); $this->getKernel()->getContainer()->set('cache', $this); - $this->getKernel()->getContainer()->set('esi', $this->getEsi()); + $this->getKernel()->getContainer()->set($this->getSurrogate()->getName(), $this->getSurrogate()); return parent::forward($request, $raw, $entry); } @@ -70,11 +70,25 @@ protected function getOptions() return array(); } - protected function createEsi() + protected function createSurrogate() { return new Esi(); } + /** + * Creates new ESI instance. + * + * @return Esi + * + * @deprecated since version 2.6, to be removed in 3.0. Use createSurrogate() instead + */ + protected function createEsi() + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use createSurrogate() instead.', E_USER_DEPRECATED); + + return $this->createSurrogate(); + } + protected function createStore() { return new Store($this->cacheDir ?: $this->kernel->getCacheDir().'/http_cache'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml new file mode 100644 index 000000000000..4f2e1fbf362a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index 0e07cdb5d9f6..3e049c0f2ceb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -26,12 +26,17 @@ + + + + + @@ -43,6 +48,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml index e7d1c3c7d47b..819f9623b004 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml @@ -6,32 +6,22 @@ Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher - Symfony\Component\Stopwatch\Stopwatch %kernel.cache_dir%/%kernel.container_class%.xml Symfony\Component\HttpKernel\Controller\TraceableControllerResolver + -1 - - - + - - - - - - deprecation - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml index 36872ad529ca..36347eccb20d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml @@ -5,15 +5,23 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Component\HttpKernel\EventListener\ErrorsLoggerListener + Symfony\Component\HttpKernel\EventListener\DebugHandlersListener + Symfony\Component\Stopwatch\Stopwatch + 0 - + - - emergency + + null + null + null + true + null + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml index c0202e1516a7..277054442ff1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml @@ -6,8 +6,7 @@ Symfony\Component\HttpKernel\HttpCache\Esi - Symfony\Component\HttpKernel\EventListener\EsiListener - Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer + Symfony\Component\HttpKernel\EventListener\SurrogateListener @@ -17,13 +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 bf63332ced1e..e2b0df0820b6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -10,7 +10,7 @@ Symfony\Component\Form\FormFactory Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser - Symfony\Component\PropertyAccess\PropertyAccessor + Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler @@ -54,12 +54,24 @@ - - - + + + + + + + + + + + + + + + - + @@ -70,6 +82,7 @@ + @@ -152,9 +165,19 @@ + + + + + + + + + + @@ -166,5 +189,10 @@ + + + + %validator.translation_domain% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml index 57cad204aa38..f20552ed1822 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml @@ -4,19 +4,14 @@ 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% + + - + %form.type_extension.csrf.enabled% %form.type_extension.csrf.field_name% 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 000000000000..1e8e3c89834d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml @@ -0,0 +1,36 @@ + + + + + + Symfony\Component\Form\Extension\DataCollector\Proxy\ResolvedTypeFactoryDataCollectorProxy + Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension + Symfony\Component\Form\Extension\DataCollector\FormDataCollector + Symfony\Component\Form\Extension\DataCollector\FormDataExtractor + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml index 595db6d2741c..d3687da13a5d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml @@ -5,33 +5,49 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Component\HttpKernel\Fragment\FragmentHandler + Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer - Symfony\Bundle\FrameworkBundle\Fragment\ContainerAwareHIncludeFragmentRenderer + Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer + Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer /_fragment - + %kernel.debug% - + - + %fragment.path% - - + %fragment.renderer.hinclude.global_template% %fragment.path% + + + + + + + %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 a8666606e150..cee86b3b72cf 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 000000000000..0e0fc65dea10 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml @@ -0,0 +1,17 @@ + + + + + + Symfony\Component\PropertyAccess\PropertyAccessor + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml new file mode 100644 index 000000000000..cc836c35e3b0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml @@ -0,0 +1,17 @@ + + + + + + Symfony\Component\HttpKernel\EventListener\AddRequestFormatsListener + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_test.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_test.php new file mode 100644 index 000000000000..5b020d5d2297 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_test.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/* + * This file implements rewrite rules for PHP built-in web server. + * + * See: http://www.php.net/manual/en/features.commandline.webserver.php + * + * If you have custom directory layout, then you have to write your own router + * and pass it as a value to 'router' option of server:run command. + * + * @author: Michał Pipa + * @author: Albert Jessurum + */ + +if (is_file($_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { + return false; +} + +$_SERVER = array_merge($_SERVER, $_ENV); +$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.'app_test.php'; + +require 'app_test.php'; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 9e21db451915..6b2f7c968869 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 39e9ada7c9ef..fa7aa2b2bd80 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 @@ -7,8 +7,18 @@ + + + + + + + + + + @@ -16,10 +26,13 @@ + + + @@ -31,7 +44,15 @@ + + + + + + + + @@ -93,12 +114,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -114,7 +170,7 @@ - + @@ -130,12 +186,19 @@ + + + + + + + @@ -143,4 +206,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 000000000000..5922d146649a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security.xml @@ -0,0 +1,15 @@ + + + + + + Symfony\Component\Security\Core\Util\SecureRandom + + + + + + + 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 000000000000..143c8a68efe8 --- /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/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 491ccbcf2cf5..e5b53e94938e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -8,18 +8,51 @@ Symfony\Component\Serializer\Serializer Symfony\Component\Serializer\Encoder\XmlEncoder Symfony\Component\Serializer\Encoder\JsonEncoder + - + + + + + + + + null + + + + + + + + + + + + + + + null + + + + + + %serializer.mapping.cache.prefix% + + + - + - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 674e28f1c98a..2021505726fe 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,12 @@ + + false + + @@ -39,6 +44,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 a6a129ee0a73..591f92e456dd 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/ssi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml new file mode 100644 index 000000000000..fe320e9812eb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml index b06c9af82f95..5aeb38e28422 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml @@ -14,6 +14,8 @@ Symfony\Component\Templating\Loader\CacheLoader Symfony\Component\Templating\Loader\ChainLoader Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplateFinder + Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper + Symfony\Bundle\FrameworkBundle\Templating\Helper\RouterHelper @@ -52,13 +54,25 @@ %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 0c6e0dcba4dc..49e79416e1eb 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 3caa29fb8433..ddcccc8df6bd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml @@ -7,20 +7,16 @@ Symfony\Bundle\FrameworkBundle\Templating\PhpEngine Symfony\Component\Templating\Helper\SlotsHelper - Symfony\Component\Templating\Helper\CoreAssetsHelper Symfony\Bundle\FrameworkBundle\Templating\Helper\ActionsHelper - Symfony\Bundle\FrameworkBundle\Templating\Helper\RouterHelper Symfony\Bundle\FrameworkBundle\Templating\Helper\RequestHelper Symfony\Bundle\FrameworkBundle\Templating\Helper\SessionHelper 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 - Symfony\Bundle\FrameworkBundle\Templating\Asset\PathPackage - Symfony\Component\Templating\Asset\UrlPackage - Symfony\Bundle\FrameworkBundle\Templating\Asset\PackageFactory @@ -36,47 +32,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + @@ -101,6 +64,11 @@ + + + + + %templating.helper.form.resources% @@ -108,7 +76,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index 5e6ec40ff4cb..0007a360c6e4 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 @@ -42,6 +44,13 @@ %kernel.cache_dir%/translations %kernel.debug% + + + + + + + @@ -90,6 +99,10 @@ + + + + @@ -122,6 +135,10 @@ + + + + @@ -135,5 +152,10 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml new file mode 100644 index 000000000000..37b5cd8de85f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index 0bc70040a206..ccfd44e5ca48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -5,34 +5,36 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Component\Validator\Validator - Symfony\Component\Validator\Mapping\ClassMetadataFactory + Symfony\Component\Validator\Validator\ValidatorInterface + Symfony\Component\Validator\ValidatorBuilderInterface + Symfony\Component\Validator\Validation Symfony\Component\Validator\Mapping\Cache\ApcCache - Symfony\Component\Validator\Mapping\Loader\LoaderChain - Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader - Symfony\Component\Validator\Mapping\Loader\AnnotationLoader - Symfony\Component\Validator\Mapping\Loader\XmlFilesLoader - Symfony\Component\Validator\Mapping\Loader\YamlFilesLoader Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory - - + Symfony\Component\Validator\Constraints\ExpressionValidator + Symfony\Component\Validator\Constraints\EmailValidator - - - - %validator.translation_domain% - + - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 177821a5afb2..9b2f3cb3a437 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -38,7 +38,13 @@ %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 63d16bd357dc..ac1077a205ae 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/button_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php index 64d44666888b..9dac32fc994c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php @@ -1,2 +1,4 @@ -humanize($name); } ?> + $name, '%id%' => $id)) + : $view['form']->humanize($name); } ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php new file mode 100644 index 000000000000..8d2d6d7fd63c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php @@ -0,0 +1,9 @@ +id="escape($id) ?>" name="escape($full_name) ?>" +disabled="disabled" + $v): ?> + +escape($k), $view->escape($k)) ?> + +escape($k), $view->escape($v)) ?> + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php index 0399e433929f..0c04641a835f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php @@ -1,5 +1,5 @@ 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 2b3e5abdaef3..5d7654f54cdc 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 9c47c58a4b3c..e90298701c68 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,13 @@ -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($k, 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 2a8e979b7f90..327925a53719 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 62e010d1fd08..c0b68a2d17a4 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); } @@ -77,8 +77,10 @@ public function warmUp($cacheDir) * Replaces placeholders with service container parameter values in: * - the route defaults, * - the route requirements, - * - the route pattern. - * - the route host. + * - the route path, + * - the route host, + * - the route schemes, + * - the route methods. * * @param RouteCollection $collection */ @@ -90,11 +92,28 @@ private function resolveParameters(RouteCollection $collection) } foreach ($route->getRequirements() as $name => $value) { + if ('_scheme' === $name || '_method' === $name) { + continue; // ignore deprecated requirements to not trigger deprecation warnings + } + $route->setRequirement($name, $this->resolve($value)); } $route->setPath($this->resolve($route->getPath())); $route->setHost($this->resolve($route->getHost())); + + $schemes = array(); + foreach ($route->getSchemes() as $scheme) { + $schemes = array_merge($schemes, explode('|', $this->resolve($scheme))); + } + $route->setSchemes($schemes); + + $methods = array(); + foreach ($route->getMethods() as $method) { + $methods = array_merge($methods, explode('|', $this->resolve($method))); + } + $route->setMethods($methods); + $route->setCondition($this->resolve($route->getCondition())); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php index 0a63a29cd6cc..9fb97d22a536 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PackageFactory.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Asset; +@trigger_error('The Symfony\Bundle\FrameworkBundle\Templating\Asset\PackageFactory is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Templating\Asset\PackageInterface; @@ -19,6 +21,8 @@ * Creates packages based on whether the current request is secure. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class PackageFactory { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php index 6aa8c5882466..1eb801c4546c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Asset/PathPackage.php @@ -14,10 +14,14 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Templating\Asset\PathPackage as BasePathPackage; +@trigger_error('The Symfony\Bundle\FrameworkBundle\Templating\Asset\PathPackage is deprecated since version 2.7 and will be removed in 3.0. Use the Asset component instead.', E_USER_DEPRECATED); + /** * The path packages adds a version and a base path to asset URLs. * * @author Kris Wallsmith + * + * @deprecated since 2.7, will be removed in 3.0. Use the Asset component instead. */ class PathPackage extends BasePathPackage { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php index 19a59381d0c5..55471027449b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Debugger.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; +@trigger_error('The '.__NAMESPACE__.'\Debugger class is deprecated since version 2.4 and will be removed in 3.0. Use the Psr\Log\LoggerInterface interface instead.', E_USER_DEPRECATED); + use Symfony\Component\Templating\DebuggerInterface; use Psr\Log\LoggerInterface; @@ -18,6 +20,9 @@ * Binds the Symfony templating loader debugger to the Symfony logger. * * @author Fabien Potencier + * + * @deprecated since version 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 0493d43d2949..c204ffa2f03b 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 edc087e867da..dcf58975176b 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 d4f744ce94e6..687588b1b91c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php @@ -12,12 +12,12 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\SecurityContext; -use Symfony\Component\HttpFoundation\Request; /** - * GlobalVariables is the entry point for Symfony global variables in Twig templates. + * GlobalVariables is the entry point for Symfony global variables in PHP templates. * * @author Fabien Potencier */ @@ -36,10 +36,14 @@ public function __construct(ContainerInterface $container) /** * Returns the security context service. * + * @deprecated since version 2.6, to be removed in 3.0. + * * @return SecurityContext|null The security context */ public function getSecurity() { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0.', E_USER_DEPRECATED); + if ($this->container->has('security.context')) { return $this->container->get('security.context'); } @@ -54,11 +58,13 @@ public function getSecurity() */ public function getUser() { - if (!$security = $this->getSecurity()) { + if (!$this->container->has('security.token_storage')) { return; } - if (!$token = $security->getToken()) { + $tokenStorage = $this->container->get('security.token_storage'); + + if (!$token = $tokenStorage->getToken()) { return; } @@ -77,8 +83,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/AssetsHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php new file mode 100644 index 000000000000..6acc04539393 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php @@ -0,0 +1,127 @@ + + * + * 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\Asset\Packages; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; +use Symfony\Component\Templating\Helper\Helper; + +/** + * AssetsHelper helps manage asset URLs. + * + * @author Fabien Potencier + */ +class AssetsHelper extends Helper +{ + private $packages; + + public function __construct(Packages $packages) + { + $this->packages = $packages; + } + + /** + * Returns the public url/path of an asset. + * + * If the package used to generate the path is an instance of + * UrlPackage, you will always get a URL and not a path. + * + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * + * @return string The public path of the asset + */ + public function getUrl($path, $packageName = null, $version = null) + { + // BC layer to be removed in 3.0 + if (3 === $count = func_num_args()) { + @trigger_error('Forcing a version for an asset was deprecated in 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); + + $args = func_get_args(); + + return $this->getLegacyAssetUrl($path, $packageName, $args[2]); + } + + return $this->packages->getUrl($path, $packageName); + } + + /** + * Returns the version of an asset. + * + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * + * @return string The asset version + */ + public function getVersion($path = null, $packageName = null) + { + // no arguments means old getVersion() for default package + if (null === $path) { + @trigger_error('The getVersion() method requires a path as a first argument since 2.7 and will be enforced as of 3.0.', E_USER_DEPRECATED); + + return $this->packages->getVersion('/', $packageName); + } + + // path and packageName can only be for the new version + if (null !== $packageName) { + return $this->packages->getVersion($path, $packageName); + } + + // packageName is null and path not, so path is a path or a packageName + try { + $package = $this->packages->getPackage($path); + } catch (\InvalidArgumentException $e) { + // path is not a package, so it should be a path + return $this->packages->getVersion($path); + } + + // path is a packageName, old version + @trigger_error('The getVersion() method requires a path as a first argument since 2.7 and will be enforced as of 3.0.', E_USER_DEPRECATED); + + return $this->packages->getVersion('/', $path); + } + + private function getLegacyAssetUrl($path, $packageName = null, $version = null) + { + if ($version) { + $package = $this->packages->getPackage($packageName); + + $v = new \ReflectionProperty('Symfony\Component\Asset\Package', 'versionStrategy'); + $v->setAccessible(true); + + $currentVersionStrategy = $v->getValue($package); + + $f = new \ReflectionProperty($currentVersionStrategy, 'format'); + $f->setAccessible(true); + + $format = $f->getValue($currentVersionStrategy); + + $v->setValue($package, new StaticVersionStrategy($version, $format)); + } + + $url = $this->packages->getUrl($path, $packageName); + + if ($version) { + $v->setValue($package, $currentVersionStrategy); + } + + return $url; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'assets'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php index 87aede2273af..b02feb54b15a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php @@ -33,7 +33,7 @@ class CodeHelper extends Helper */ public function __construct($fileLinkFormat, $rootDir, $charset) { - $this->fileLinkFormat = empty($fileLinkFormat) ? ini_get('xdebug.file_link_format') : $fileLinkFormat; + $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->rootDir = str_replace('\\', '/', $rootDir).'/'; $this->charset = $charset; } @@ -121,7 +121,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 060a6ce6bf6e..22848fcbfa35 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -131,13 +131,13 @@ public function end(FormView $view, array $variables = array()) * * @return string The HTML markup * - * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use - * {@link start} instead. + * @deprecated since version 2.3, to be removed in 3.0. + * Use {@link start} instead. */ public function enctype(FormView $view) { - // Uncomment this as soon as the deprecation note should be shown - // trigger_error('The form helper $view[\'form\']->enctype() is deprecated since version 2.3 and will be removed in 3.0. Use $view[\'form\']->start() instead.', E_USER_DEPRECATED); + @trigger_error('The form helper $view[\'form\']->enctype() is deprecated since version 2.3 and will be removed in 3.0. Use $view[\'form\']->start() instead.', E_USER_DEPRECATED); + return $this->renderer->searchAndRenderBlock($view, 'enctype'); } @@ -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 b7fbfcdca7ec..ec28f12ba63f 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,25 @@ 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 version 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) { + @trigger_error('Since version 2.5, passing a Request instance into the '.__METHOD__.' is deprecated and support for it will be removed in 3.0. Inject a Symfony\Component\HttpFoundation\RequestStack instance instead.', E_USER_DEPRECATED); + $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 +56,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 +66,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 675fe9dc9094..5d15fa3464b0 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,25 @@ 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 version 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) { + @trigger_error('Since version 2.5, passing a Request instance into the '.__METHOD__.' is deprecated and support for it will be removed in 3.0. Inject a Symfony\Component\HttpFoundation\RequestStack instance instead.', E_USER_DEPRECATED); + $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 +54,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 000000000000..47a9cf80ebcb --- /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 98c998deba98..4f6cffc103b8 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|bool 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 int $time The last modification time of the cached template (timestamp) - * - * @return bool + * {@inheritdoc} */ public function isFresh(TemplateReferenceInterface $template, $time) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php index a93098eb842c..41382f769c65 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 85825fe337f4..df3ab19c4d2e 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('/', str_replace('\\', '/', $file)); + if ($name instanceof TemplateReferenceInterface) { + return $name; + } + + $parts = explode('/', str_replace('\\', '/', $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 96e18eda8836..c50541fa27ee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php @@ -25,7 +25,7 @@ class TemplateNameParser extends BaseTemplateNameParser { protected $kernel; - protected $cache; + protected $cache = array(); /** * Constructor. @@ -35,7 +35,6 @@ class TemplateNameParser extends BaseTemplateNameParser public function __construct(KernelInterface $kernel) { $this->kernel = $kernel; - $this->cache = array(); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php new file mode 100644 index 000000000000..f179b25dbd2b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * KernelTestCase is the base class for tests needing a Kernel. + * + * @author Fabien Potencier + */ +abstract class KernelTestCase extends \PHPUnit_Framework_TestCase +{ + protected static $class; + + /** + * @var KernelInterface + */ + protected static $kernel; + + /** + * Finds the directory where the phpunit.xml(.dist) is stored. + * + * If you run tests with the PHPUnit CLI tool, everything will work as expected. + * If not, override this method in your test classes. + * + * @return string The directory where phpunit.xml(.dist) is stored + * + * @throws \RuntimeException + */ + protected static function getPhpUnitXmlDir() + { + if (!isset($_SERVER['argv']) || false === strpos($_SERVER['argv'][0], 'phpunit')) { + throw new \RuntimeException('You must override the KernelTestCase::createKernel() method.'); + } + + $dir = static::getPhpUnitCliConfigArgument(); + if (null === $dir && + (is_file(getcwd().DIRECTORY_SEPARATOR.'phpunit.xml') || + is_file(getcwd().DIRECTORY_SEPARATOR.'phpunit.xml.dist'))) { + $dir = getcwd(); + } + + // Can't continue + if (null === $dir) { + throw new \RuntimeException('Unable to guess the Kernel directory.'); + } + + if (!is_dir($dir)) { + $dir = dirname($dir); + } + + return $dir; + } + + /** + * Finds the value of the CLI configuration option. + * + * PHPUnit will use the last configuration argument on the command line, so this only returns + * the last configuration argument. + * + * @return string The value of the PHPUnit CLI configuration option + */ + private static function getPhpUnitCliConfigArgument() + { + $dir = null; + $reversedArgs = array_reverse($_SERVER['argv']); + foreach ($reversedArgs as $argIndex => $testArg) { + if (preg_match('/^-[^ \-]*c$/', $testArg) || $testArg === '--configuration') { + $dir = realpath($reversedArgs[$argIndex - 1]); + break; + } elseif (0 === strpos($testArg, '--configuration=')) { + $argPath = substr($testArg, strlen('--configuration=')); + $dir = realpath($argPath); + break; + } elseif (0 === strpos($testArg, '-c')) { + $argPath = substr($testArg, strlen('-c')); + $dir = realpath($argPath); + break; + } + } + + return $dir; + } + + /** + * Attempts to guess the kernel location. + * + * When the Kernel is located, the file is required. + * + * @return string The Kernel class name + * + * @throws \RuntimeException + */ + protected static function getKernelClass() + { + if (isset($_SERVER['KERNEL_DIR'])) { + $dir = $_SERVER['KERNEL_DIR']; + + if (!is_dir($dir)) { + $phpUnitDir = static::getPhpUnitXmlDir(); + if (is_dir("$phpUnitDir/$dir")) { + $dir = "$phpUnitDir/$dir"; + } + } + } else { + $dir = static::getPhpUnitXmlDir(); + } + + $finder = new Finder(); + $finder->name('*Kernel.php')->depth(0)->in($dir); + $results = iterator_to_array($finder); + if (!count($results)) { + throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to https://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.'); + } + + $file = current($results); + $class = $file->getBasename('.php'); + + require_once $file; + + return $class; + } + + /** + * Boots the Kernel for this test. + * + * @param array $options + */ + protected static function bootKernel(array $options = array()) + { + static::ensureKernelShutdown(); + + static::$kernel = static::createKernel($options); + static::$kernel->boot(); + } + + /** + * Creates a Kernel. + * + * Available options: + * + * * environment + * * debug + * + * @param array $options An array of options + * + * @return KernelInterface A KernelInterface instance + */ + protected static function createKernel(array $options = array()) + { + if (null === static::$class) { + static::$class = static::getKernelClass(); + } + + return new static::$class( + isset($options['environment']) ? $options['environment'] : 'test', + isset($options['debug']) ? $options['debug'] : true + ); + } + + /** + * Shuts the kernel down if it was used in the test. + */ + protected static function ensureKernelShutdown() + { + if (null !== static::$kernel) { + static::$kernel->shutdown(); + } + } + + /** + * Clean up Kernel usage in this test. + */ + protected function tearDown() + { + static::ensureKernelShutdown(); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php index 434aecf227c2..5708ef716df7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php @@ -12,23 +12,14 @@ namespace Symfony\Bundle\FrameworkBundle\Test; use Symfony\Bundle\FrameworkBundle\Client; -use Symfony\Component\Finder\Finder; -use Symfony\Component\HttpKernel\KernelInterface; /** * WebTestCase is the base class for functional tests. * * @author Fabien Potencier */ -abstract class WebTestCase extends \PHPUnit_Framework_TestCase +abstract class WebTestCase extends KernelTestCase { - protected static $class; - - /** - * @var KernelInterface - */ - protected static $kernel; - /** * Creates a Client. * @@ -39,143 +30,11 @@ abstract class WebTestCase extends \PHPUnit_Framework_TestCase */ protected static function createClient(array $options = array(), array $server = array()) { - if (null !== static::$kernel) { - static::$kernel->shutdown(); - } - - static::$kernel = static::createKernel($options); - static::$kernel->boot(); + static::bootKernel($options); $client = static::$kernel->getContainer()->get('test.client'); $client->setServerParameters($server); return $client; } - - /** - * Finds the directory where the phpunit.xml(.dist) is stored. - * - * If you run tests with the PHPUnit CLI tool, everything will work as expected. - * If not, override this method in your test classes. - * - * @return string The directory where phpunit.xml(.dist) is stored - * - * @throws \RuntimeException - */ - protected static function getPhpUnitXmlDir() - { - if (!isset($_SERVER['argv']) || false === strpos($_SERVER['argv'][0], 'phpunit')) { - throw new \RuntimeException('You must override the WebTestCase::createKernel() method.'); - } - - $dir = static::getPhpUnitCliConfigArgument(); - if (null === $dir && - (is_file(getcwd().DIRECTORY_SEPARATOR.'phpunit.xml') || - is_file(getcwd().DIRECTORY_SEPARATOR.'phpunit.xml.dist'))) { - $dir = getcwd(); - } - - // Can't continue - if (null === $dir) { - throw new \RuntimeException('Unable to guess the Kernel directory.'); - } - - if (!is_dir($dir)) { - $dir = dirname($dir); - } - - return $dir; - } - - /** - * Finds the value of the CLI configuration option. - * - * PHPUnit will use the last configuration argument on the command line, so this only returns - * the last configuration argument. - * - * @return string The value of the PHPUnit CLI configuration option - */ - private static function getPhpUnitCliConfigArgument() - { - $dir = null; - $reversedArgs = array_reverse($_SERVER['argv']); - foreach ($reversedArgs as $argIndex => $testArg) { - if (preg_match('/^-[^ \-]*c$/', $testArg) || $testArg === '--configuration') { - $dir = realpath($reversedArgs[$argIndex - 1]); - break; - } elseif (0 === strpos($testArg, '--configuration=')) { - $argPath = substr($testArg, strlen('--configuration=')); - $dir = realpath($argPath); - break; - } elseif (0 === strpos($testArg, '-c')) { - $argPath = substr($testArg, strlen('-c')); - $dir = realpath($argPath); - break; - } - } - - return $dir; - } - - /** - * Attempts to guess the kernel location. - * - * When the Kernel is located, the file is required. - * - * @return string The Kernel class name - * - * @throws \RuntimeException - */ - protected static function getKernelClass() - { - $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir(); - - $finder = new Finder(); - $finder->name('*Kernel.php')->depth(0)->in($dir); - $results = iterator_to_array($finder); - if (!count($results)) { - throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to https://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.'); - } - - $file = current($results); - $class = $file->getBasename('.php'); - - require_once $file; - - return $class; - } - - /** - * Creates a Kernel. - * - * Available options: - * - * * environment - * * debug - * - * @param array $options An array of options - * - * @return KernelInterface A KernelInterface instance - */ - protected static function createKernel(array $options = array()) - { - if (null === static::$class) { - static::$class = static::getKernelClass(); - } - - return new static::$class( - isset($options['environment']) ? $options['environment'] : 'test', - isset($options['debug']) ? $options['debug'] : true - ); - } - - /** - * Shuts the kernel down if it was used in the test. - */ - protected function tearDown() - { - if (null !== static::$kernel) { - static::$kernel->shutdown(); - } - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ClientTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/ClientTest.php new file mode 100644 index 000000000000..d021b0a29ac5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/ClientTest.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\Bundle\FrameworkBundle\Tests; + +use Symfony\Bundle\FrameworkBundle\Client; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\WebTestCase; +use Symfony\Component\HttpFoundation\Response; + +class ClientTest extends WebTestCase +{ + public function testRebootKernelBetweenRequests() + { + $mock = $this->getKernelMock(); + $mock->expects($this->once())->method('shutdown'); + + $client = new Client($mock); + $client->request('GET', '/'); + $client->request('GET', '/'); + } + + public function testDisabledRebootKernel() + { + $mock = $this->getKernelMock(); + $mock->expects($this->never())->method('shutdown'); + + $client = new Client($mock); + $client->disableReboot(); + $client->request('GET', '/'); + $client->request('GET', '/'); + } + + public function testEnableRebootKernel() + { + $mock = $this->getKernelMock(); + $mock->expects($this->once())->method('shutdown'); + + $client = new Client($mock); + $client->disableReboot(); + $client->request('GET', '/'); + $client->request('GET', '/'); + $client->enableReboot(); + $client->request('GET', '/'); + } + + private function getKernelMock() + { + $mock = $this->getMockBuilder($this->getKernelClass()) + ->setMethods(array('shutdown', 'boot', 'handle')) + ->disableOriginalConstructor() + ->getMock(); + + $mock->expects($this->any())->method('handle')->willReturn(new Response('foo')); + + return $mock; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php index 718999d20d46..c351a790ac07 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php @@ -33,7 +33,7 @@ protected function setUp() { $this->fs = new Filesystem(); $this->kernel = new TestAppKernel('test', true); - $this->rootDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.uniqid('sf2_cache_'); + $this->rootDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.uniqid('sf2_cache_', true); $this->kernel->setRootDir($this->rootDir); $this->fs->mkdir($this->rootDir); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php index da835dfd4d53..8a61d96d36c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Command\CacheClearCommand\Fixture; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php index 86c2c2a00350..25d9a72b961a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterDebugCommandTest.php @@ -56,7 +56,7 @@ private function createCommandTester() $command->setContainer($this->getContainer()); $application->add($command); - return new CommandTester($application->find('router:debug')); + return new CommandTester($application->find('debug:router')); } private function getContainer() @@ -65,11 +65,15 @@ private function getContainer() $routeCollection->add('foo', new Route('foo')); $router = $this->getMock('Symfony\Component\Routing\RouterInterface'); $router - ->expects($this->atLeastOnce()) + ->expects($this->any()) ->method('getRouteCollection') ->will($this->returnValue($routeCollection)) ; + $loader = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader') + ->disableOriginalConstructor() + ->getMock(); + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); $container ->expects($this->once()) @@ -77,12 +81,13 @@ private function getContainer() ->with('router') ->will($this->returnValue(true)) ; + $container - ->expects($this->atLeastOnce()) ->method('get') - ->with('router') - ->will($this->returnValue($router)) - ; + ->will($this->returnValueMap(array( + array('router', 1, $router), + array('controller_name_converter', 1, $loader), + ))); return $container; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php index 02442766969e..93c2468dc15a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php @@ -74,19 +74,22 @@ private function getContainer() ->will($this->returnValue($requestContext)) ; + $loader = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader') + ->disableOriginalConstructor() + ->getMock(); + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); $container ->expects($this->once()) ->method('has') ->with('router') - ->will($this->returnValue(true)) - ; - $container - ->expects($this->atLeastOnce()) - ->method('get') - ->with('router') - ->will($this->returnValue($router)) - ; + ->will($this->returnValue(true)); + $container->method('get') + ->will($this->returnValueMap(array( + array('router', 1, $router), + array('controller_name_converter', 1, $loader), + + ))); return $container; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php new file mode 100644 index 000000000000..c99c0ace2ac7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; +use Symfony\Component\Filesystem\Filesystem; + +class TranslationDebugCommandTest extends \PHPUnit_Framework_TestCase +{ + private $fs; + private $translationDir; + + public function testDebugMissingMessages() + { + $tester = $this->createCommandTester($this->getContainer(array('foo' => 'foo'))); + $tester->execute(array('locale' => 'en', 'bundle' => 'foo')); + + $this->assertRegExp('/missing/', $tester->getDisplay()); + } + + public function testDebugUnusedMessages() + { + $tester = $this->createCommandTester($this->getContainer(array(), array('foo' => 'foo'))); + $tester->execute(array('locale' => 'en', 'bundle' => 'foo')); + + $this->assertRegExp('/unused/', $tester->getDisplay()); + } + + public function testDebugFallbackMessages() + { + $tester = $this->createCommandTester($this->getContainer(array(), array('foo' => 'foo'))); + $tester->execute(array('locale' => 'fr', 'bundle' => 'foo')); + + $this->assertRegExp('/fallback/', $tester->getDisplay()); + } + + public function testNoDefinedMessages() + { + $tester = $this->createCommandTester($this->getContainer()); + $tester->execute(array('locale' => 'fr', 'bundle' => 'test')); + + $this->assertRegExp('/No defined or extracted messages for locale "fr"/', $tester->getDisplay()); + } + + public function testDebugDefaultDirectory() + { + $tester = $this->createCommandTester($this->getContainer(array('foo' => 'foo'), array('bar' => 'bar'))); + $tester->execute(array('locale' => 'en')); + + $this->assertRegExp('/missing/', $tester->getDisplay()); + $this->assertRegExp('/unused/', $tester->getDisplay()); + } + + public function testDebugCustomDirectory() + { + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + $kernel->expects($this->once()) + ->method('getBundle') + ->with($this->equalTo($this->translationDir)) + ->willThrowException(new \InvalidArgumentException()); + + $tester = $this->createCommandTester($this->getContainer(array('foo' => 'foo'), array('bar' => 'bar'), $kernel)); + $tester->execute(array('locale' => 'en', 'bundle' => $this->translationDir)); + + $this->assertRegExp('/missing/', $tester->getDisplay()); + $this->assertRegExp('/unused/', $tester->getDisplay()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testDebugInvalidDirectory() + { + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + $kernel->expects($this->once()) + ->method('getBundle') + ->with($this->equalTo('dir')) + ->will($this->throwException(new \InvalidArgumentException())); + + $tester = $this->createCommandTester($this->getContainer(array(), array(), $kernel)); + $tester->execute(array('locale' => 'en', 'bundle' => 'dir')); + } + + protected function setUp() + { + $this->fs = new Filesystem(); + $this->translationDir = sys_get_temp_dir().'/'.uniqid('sf2_translation', true); + $this->fs->mkdir($this->translationDir.'/Resources/translations'); + $this->fs->mkdir($this->translationDir.'/Resources/views'); + } + + protected function tearDown() + { + $this->fs->remove($this->translationDir); + } + + /** + * @return CommandTester + */ + private function createCommandTester($container) + { + $command = new TranslationDebugCommand(); + $command->setContainer($container); + + $application = new Application(); + $application->add($command); + + return new CommandTester($application->find('debug:translation')); + } + + private function getContainer($extractedMessages = array(), $loadedMessages = array(), $kernel = null) + { + $translator = $this->getMockBuilder('Symfony\Component\Translation\Translator') + ->disableOriginalConstructor() + ->getMock(); + + $translator + ->expects($this->any()) + ->method('getFallbackLocales') + ->will($this->returnValue(array('en'))); + + $extractor = $this->getMock('Symfony\Component\Translation\Extractor\ExtractorInterface'); + $extractor + ->expects($this->any()) + ->method('extract') + ->will( + $this->returnCallback(function ($path, $catalogue) use ($extractedMessages) { + $catalogue->add($extractedMessages); + }) + ); + + $loader = $this->getMock('Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader'); + $loader + ->expects($this->any()) + ->method('loadMessages') + ->will( + $this->returnCallback(function ($path, $catalogue) use ($loadedMessages) { + $catalogue->add($loadedMessages); + }) + ); + + if (null === $kernel) { + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + $kernel + ->expects($this->any()) + ->method('getBundle') + ->will($this->returnValueMap(array( + array('foo', true, $this->getBundle($this->translationDir)), + array('test', true, $this->getBundle('test')), + ))); + } + + $kernel + ->expects($this->any()) + ->method('getRootDir') + ->will($this->returnValue($this->translationDir)); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container + ->expects($this->any()) + ->method('get') + ->will($this->returnValueMap(array( + array('translation.extractor', 1, $extractor), + array('translation.loader', 1, $loader), + array('translator', 1, $translator), + array('kernel', 1, $kernel), + ))); + + return $container; + } + + private function getBundle($path) + { + $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\BundleInterface'); + $bundle + ->expects($this->any()) + ->method('getPath') + ->will($this->returnValue($path)) + ; + + return $bundle; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index dbc8fb868ba4..fc0e7654db2c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -24,7 +24,7 @@ public function testBundleInterfaceImplementation() { $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\BundleInterface'); - $kernel = $this->getKernel(array($bundle)); + $kernel = $this->getKernel(array($bundle), true); $application = new Application($kernel); $application->doRun(new ArrayInput(array('list')), new NullOutput()); @@ -35,7 +35,7 @@ public function testBundleCommandsAreRegistered() $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); $bundle->expects($this->once())->method('registerCommands'); - $kernel = $this->getKernel(array($bundle)); + $kernel = $this->getKernel(array($bundle), true); $application = new Application($kernel); $application->doRun(new ArrayInput(array('list')), new NullOutput()); @@ -49,12 +49,7 @@ public function testBundleCommandsAreRetrievable() $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); $bundle->expects($this->once())->method('registerCommands'); - $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); - $kernel - ->expects($this->any()) - ->method('getBundles') - ->will($this->returnValue(array($bundle))) - ; + $kernel = $this->getKernel(array($bundle)); $application = new Application($kernel); $application->all(); @@ -68,12 +63,7 @@ public function testBundleSingleCommandIsRetrievable() $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); $bundle->expects($this->once())->method('registerCommands'); - $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); - $kernel - ->expects($this->any()) - ->method('getBundles') - ->will($this->returnValue(array($bundle))) - ; + $kernel = $this->getKernel(array($bundle)); $application = new Application($kernel); @@ -88,12 +78,7 @@ public function testBundleCommandCanBeFound() $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); $bundle->expects($this->once())->method('registerCommands'); - $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); - $kernel - ->expects($this->any()) - ->method('getBundles') - ->will($this->returnValue(array($bundle))) - ; + $kernel = $this->getKernel(array($bundle)); $application = new Application($kernel); @@ -108,12 +93,7 @@ public function testBundleCommandCanBeFoundByAlias() $bundle = $this->getMock('Symfony\Component\HttpKernel\Bundle\Bundle'); $bundle->expects($this->once())->method('registerCommands'); - $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); - $kernel - ->expects($this->any()) - ->method('getBundles') - ->will($this->returnValue(array($bundle))) - ; + $kernel = $this->getKernel(array($bundle)); $application = new Application($kernel); @@ -130,7 +110,7 @@ public function testBundleCommandsHaveRightContainer() $command->setCode(function () {}); $command->expects($this->exactly(2))->method('setContainer'); - $application = new Application($this->getKernel(array())); + $application = new Application($this->getKernel(array(), true)); $application->setAutoExit(false); $application->setCatchExceptions(false); $application->add($command); @@ -143,20 +123,34 @@ public function testBundleCommandsHaveRightContainer() $tester->run(array('command' => 'foo')); } - private function getKernel(array $bundles) + private function getKernel(array $bundles, $useDispatcher = false) { - $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $dispatcher - ->expects($this->atLeastOnce()) - ->method('dispatch') - ; - $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + + if ($useDispatcher) { + $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $dispatcher + ->expects($this->atLeastOnce()) + ->method('dispatch') + ; + $container + ->expects($this->atLeastOnce()) + ->method('get') + ->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->atLeastOnce()) - ->method('get') - ->with($this->equalTo('event_dispatcher')) - ->will($this->returnValue($dispatcher)) + ->expects($this->once()) + ->method('getParameter') + ->with($this->equalTo('console.command.ids')) + ->will($this->returnValue(array())) ; $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); 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 000000000000..192ba44bf737 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php @@ -0,0 +1,207 @@ + + * + * 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\EventDispatcher\EventDispatcher; +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 provideLegacySynchronizedServiceDefinitionTestData + * @group legacy + */ + public function testLegacyDescribeSynchronizedServiceDefinition(Definition $definition, $expectedDescription) + { + $this->assertDescription($expectedDescription, $definition); + } + + public function provideLegacySynchronizedServiceDefinitionTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getLegacyContainerDefinitions()); + } + + /** @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()); + } + + /** @dataProvider getDescribeContainerParameterTestData */ + public function testDescribeContainerParameter($parameter, $expectedDescription, array $options) + { + $this->assertDescription($expectedDescription, $parameter, $options); + } + + public function getDescribeContainerParameterTestData() + { + $data = $this->getDescriptionTestData(ObjectsProvider::getContainerParameter()); + + $data[0][] = array('parameter' => 'database_name'); + $data[1][] = array('parameter' => 'twig.form.resources'); + + return $data; + } + + /** @dataProvider getDescribeEventDispatcherTestData */ + public function testDescribeEventDispatcher(EventDispatcher $eventDispatcher, $expectedDescription, array $options) + { + $this->assertDescription($expectedDescription, $eventDispatcher, $options); + } + + public function getDescribeEventDispatcherTestData() + { + return $this->getEventDispatcherDescriptionTestData(ObjectsProvider::getEventDispatchers()); + } + + /** @dataProvider getDescribeCallableTestData */ + public function testDescribeCallable($callable, $expectedDescription) + { + $this->assertDescription($expectedDescription, $callable); + } + + public function getDescribeCallableTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getCallables()); + } + + 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); + + if ('json' === $this->getFormat()) { + $this->assertEquals(json_decode($expectedDescription), json_decode($output->fetch())); + } else { + $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; + } + + private function getEventDispatcherDescriptionTestData(array $objects) + { + $variations = array( + 'events' => array(), + 'event1' => array('event' => 'event1'), + ); + + $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 000000000000..483fd65e2739 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/JsonDescriptorTest.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\Bundle\FrameworkBundle\Tests\Console\Descriptor; + +use Symfony\Bundle\FrameworkBundle\Console\Descriptor\JsonDescriptor; + +/** + * @requires PHP 5.4 + */ +class JsonDescriptorTest extends AbstractDescriptorTest +{ + 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 000000000000..fbb5aaa96268 --- /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 000000000000..52a666541692 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php @@ -0,0 +1,207 @@ + + * + * 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\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcher; +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 getContainerParameter() + { + $builder = new ContainerBuilder(); + $builder->setParameter('database_name', 'symfony'); + $builder->setParameter('twig.form.resources', array( + 'bootstrap_3_horizontal_layout.html.twig', + 'bootstrap_3_layout.html.twig', + 'form_div_layout.html.twig', + 'form_table_layout.html.twig', + )); + + return array( + 'parameter' => $builder, + 'array_parameter' => $builder, + ); + } + + 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) + ->setAbstract(true) + ->setFactory(array('Full\\Qualified\\FactoryClass', 'get')), + 'definition_2' => $definition2 + ->setPublic(false) + ->setSynthetic(true) + ->setFile('/path/to/file') + ->setLazy(false) + ->setAbstract(false) + ->addTag('tag1', array('attr1' => 'val1', 'attr2' => 'val2')) + ->addTag('tag1', array('attr3' => 'val3')) + ->addTag('tag2') + ->setFactory(array(new Reference('factory.service'), 'get')), + ); + } + + /** + * @deprecated since version 2.7, to be removed in 3.0 + * + * @internal + */ + public static function getLegacyContainerDefinitions() + { + $definition1 = new Definition('Full\\Qualified\\Class1'); + $definition2 = new Definition('Full\\Qualified\\Class2'); + + return array( + 'legacy_synchronized_service_definition_1' => $definition1 + ->setPublic(true) + ->setSynthetic(false) + ->setLazy(true) + ->setSynchronized(true) + ->setAbstract(true) + ->setFactoryClass('Full\\Qualified\\FactoryClass', 'get') + ->setFactoryMethod('get'), + 'legacy_synchronized_service_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), + ); + } + + public static function getEventDispatchers() + { + $eventDispatcher = new EventDispatcher(); + + $eventDispatcher->addListener('event1', 'global_function'); + $eventDispatcher->addListener('event1', function () { return 'Closure'; }); + $eventDispatcher->addListener('event2', new CallableClass()); + + return array('event_dispatcher_1' => $eventDispatcher); + } + + public static function getCallables() + { + return array( + 'callable_1' => 'array_key_exists', + 'callable_2' => array('Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass', 'staticMethod'), + 'callable_3' => array(new CallableClass(), 'method'), + 'callable_4' => 'Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass::staticMethod', + 'callable_5' => array('Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\ExtendedCallableClass', 'parent::staticMethod'), + 'callable_6' => function () { return 'Closure'; }, + 'callable_7' => new CallableClass(), + ); + } +} + +class CallableClass +{ + public function __invoke() + { + } + public static function staticMethod() + { + } + public function method() + { + } +} + +class ExtendedCallableClass extends CallableClass +{ + public static function staticMethod() + { + } +} 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 000000000000..ce4f377c508f --- /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 000000000000..8cb9a71697f6 --- /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/Controller/ControllerNameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php index af4357b43748..0fe47b0908d8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php @@ -59,8 +59,8 @@ public function testBuild() { $parser = $this->createParser(); - $this->assertEquals('FooBundle:Default:index', $parser->build('TestBundle\FooBundle\Controller\DefaultController::indexAction'), '->parse() converts a class::method string to a short a:b:c notation string'); - $this->assertEquals('FooBundle:Sub\Default:index', $parser->build('TestBundle\FooBundle\Controller\Sub\DefaultController::indexAction'), '->parse() converts a class::method string to a short a:b:c notation string'); + $this->assertEquals('FoooooBundle:Default:index', $parser->build('TestBundle\FooBundle\Controller\DefaultController::indexAction'), '->parse() converts a class::method string to a short a:b:c notation string'); + $this->assertEquals('FoooooBundle:Sub\Default:index', $parser->build('TestBundle\FooBundle\Controller\Sub\DefaultController::indexAction'), '->parse() converts a class::method string to a short a:b:c notation string'); try { $parser->build('TestBundle\FooBundle\Controller\DefaultController::index'); @@ -107,6 +107,37 @@ public function getMissingControllersTest() ); } + /** + * @expectedException + * @dataProvider getInvalidBundleNameTests + */ + public function testInvalidBundleName($bundleName, $suggestedBundleName) + { + $parser = $this->createParser(); + + try { + $parser->parse($bundleName); + } catch (\Exception $e) { + $this->assertInstanceOf('\InvalidArgumentException', $e, '->parse() throws a \InvalidArgumentException if the bundle does not exist'); + + if (false === $suggestedBundleName) { + // make sure we don't have a suggestion + $this->assertNotContains('Did you mean', $e->getMessage()); + } else { + $this->assertContains(sprintf('Did you mean "%s"', $suggestedBundleName), $e->getMessage()); + } + } + } + + public function getInvalidBundleNameTests() + { + return array( + 'Alternative will be found using levenshtein' => array('FoodBundle:Default:index', 'FooBundle:Default:index'), + 'Alternative will be found using partial match' => array('FabpotFooBund:Default:index', 'FabpotFooBundle:Default:index'), + 'Bundle does not exist at all' => array('CrazyBundle:Default:index', false), + ); + } + private function createParser() { $bundles = array( @@ -121,6 +152,10 @@ private function createParser() ->expects($this->any()) ->method('getBundle') ->will($this->returnCallback(function ($bundle) use ($bundles) { + if (!isset($bundles[$bundle])) { + throw new \InvalidArgumentException(sprintf('Invalid bundle name "%s"', $bundle)); + } + return $bundles[$bundle]; })) ; @@ -128,6 +163,7 @@ private function createParser() $bundles = array( 'SensioFooBundle' => $this->getBundle('TestBundle\Fabpot\FooBundle', 'FabpotFooBundle'), 'SensioCmsFooBundle' => $this->getBundle('TestBundle\Sensio\Cms\FooBundle', 'SensioCmsFooBundle'), + 'FoooooBundle' => $this->getBundle('TestBundle\FooBundle', 'FoooooBundle'), 'FooBundle' => $this->getBundle('TestBundle\FooBundle', 'FooBundle'), 'FabpotFooBundle' => $this->getBundle('TestBundle\Fabpot\FooBundle', 'FabpotFooBundle'), ); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php new file mode 100644 index 000000000000..00ffa4c1fd16 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; + +use Psr\Log\LoggerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest as BaseControllerResolverTest; + +class ControllerResolverTest extends BaseControllerResolverTest +{ + public function testGetControllerOnContainerAware() + { + $resolver = $this->createControllerResolver(); + $request = Request::create('/'); + $request->attributes->set('_controller', 'Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController::testAction'); + + $controller = $resolver->getController($request); + + $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller[0]->getContainer()); + $this->assertSame('testAction', $controller[1]); + } + + public function testGetControllerOnContainerAwareInvokable() + { + $resolver = $this->createControllerResolver(); + $request = Request::create('/'); + $request->attributes->set('_controller', 'Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController'); + + $controller = $resolver->getController($request); + + $this->assertInstanceOf('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController', $controller); + $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller->getContainer()); + } + + public function testGetControllerWithBundleNotation() + { + $shortName = 'FooBundle:Default:test'; + $parser = $this->createMockParser(); + $parser->expects($this->once()) + ->method('parse') + ->with($shortName) + ->will($this->returnValue('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController::testAction')) + ; + + $resolver = $this->createControllerResolver(null, $parser); + $request = Request::create('/'); + $request->attributes->set('_controller', $shortName); + + $controller = $resolver->getController($request); + + $this->assertInstanceOf('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController', $controller[0]); + $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller[0]->getContainer()); + $this->assertSame('testAction', $controller[1]); + } + + public function testGetControllerService() + { + $container = $this->createMockContainer(); + $container->expects($this->once()) + ->method('get') + ->with('foo') + ->will($this->returnValue($this)) + ; + + $resolver = $this->createControllerResolver(null, null, $container); + $request = Request::create('/'); + $request->attributes->set('_controller', 'foo:controllerMethod1'); + + $controller = $resolver->getController($request); + + $this->assertInstanceOf(get_class($this), $controller[0]); + $this->assertSame('controllerMethod1', $controller[1]); + } + + public function testGetControllerInvokableService() + { + $invokableController = new InvokableController('bar'); + + $container = $this->createMockContainer(); + $container->expects($this->once()) + ->method('has') + ->with('foo') + ->will($this->returnValue(true)) + ; + $container->expects($this->once()) + ->method('get') + ->with('foo') + ->will($this->returnValue($invokableController)) + ; + + $resolver = $this->createControllerResolver(null, null, $container); + $request = Request::create('/'); + $request->attributes->set('_controller', 'foo'); + + $controller = $resolver->getController($request); + + $this->assertEquals($invokableController, $controller); + } + + public function testGetControllerInvokableServiceWithClassNameAsName() + { + $invokableController = new InvokableController('bar'); + $className = __NAMESPACE__.'\InvokableController'; + + $container = $this->createMockContainer(); + $container->expects($this->once()) + ->method('has') + ->with($className) + ->will($this->returnValue(true)) + ; + $container->expects($this->once()) + ->method('get') + ->with($className) + ->will($this->returnValue($invokableController)) + ; + + $resolver = $this->createControllerResolver(null, null, $container); + $request = Request::create('/'); + $request->attributes->set('_controller', $className); + + $controller = $resolver->getController($request); + + $this->assertEquals($invokableController, $controller); + } + + /** + * @dataProvider getUndefinedControllers + */ + public function testGetControllerOnNonUndefinedFunction($controller, $exceptionName = null, $exceptionMessage = null) + { + $this->setExpectedException($exceptionName, $exceptionMessage); + + parent::testGetControllerOnNonUndefinedFunction($controller); + } + + public function getUndefinedControllers() + { + return array( + array('foo', '\LogicException', 'Unable to parse the controller name "foo".'), + array('oof::bar', '\InvalidArgumentException', 'Class "oof" does not exist.'), + array('stdClass', '\LogicException', 'Unable to parse the controller name "stdClass".'), + array( + 'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar', + '\InvalidArgumentException', + 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar" for URI "/" is not callable.', + ), + ); + } + + protected function createControllerResolver(LoggerInterface $logger = null, ControllerNameParser $parser = null, ContainerInterface $container = null) + { + if (!$parser) { + $parser = $this->createMockParser(); + } + + if (!$container) { + $container = $this->createMockContainer(); + } + + return new ControllerResolver($container, $parser, $logger); + } + + protected function createMockParser() + { + return $this->getMock('Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser', array(), array(), '', false); + } + + protected function createMockContainer() + { + return $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + } +} + +class ContainerAwareController implements ContainerAwareInterface +{ + private $container; + + public function setContainer(ContainerInterface $container = null) + { + $this->container = $container; + } + + public function getContainer() + { + return $this->container; + } + + public function testAction() + { + } + + public function __invoke() + { + } +} + +class InvokableController +{ + public function __construct($bar) // mandatory argument to prevent automatic instantiation + { + } + + public function __invoke() + { + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php index 89b143bbe217..307770316c6e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php @@ -13,8 +13,14 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\User; class ControllerTest extends TestCase { @@ -24,22 +30,158 @@ public function testForward() $request->setLocale('fr'); $request->setRequestFormat('xml'); + $requestStack = new RequestStack(); + $requestStack->push($request); + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); $kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { return new Response($request->getRequestFormat().'--'.$request->getLocale()); })); $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - $container->expects($this->at(0))->method('get')->will($this->returnValue($request)); + $container->expects($this->at(0))->method('get')->will($this->returnValue($requestStack)); $container->expects($this->at(1))->method('get')->will($this->returnValue($kernel)); - $controller = new Controller(); + $controller = new TestController(); $controller->setContainer($container); $response = $controller->forward('a_controller'); $this->assertEquals('xml--fr', $response->getContent()); } + public function testGetUser() + { + $user = new User('user', 'pass'); + $token = new UsernamePasswordToken($user, 'pass', 'default', array('ROLE_USER')); + + $controller = new TestController(); + $controller->setContainer($this->getContainerWithTokenStorage($token)); + + $this->assertSame($controller->getUser(), $user); + } + + public function testGetUserAnonymousUserConvertedToNull() + { + $token = new AnonymousToken('default', 'anon.'); + + $controller = new TestController(); + $controller->setContainer($this->getContainerWithTokenStorage($token)); + + $this->assertNull($controller->getUser()); + } + + public function testGetUserWithEmptyTokenStorage() + { + $controller = new TestController(); + $controller->setContainer($this->getContainerWithTokenStorage(null)); + + $this->assertNull($controller->getUser()); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage The SecurityBundle is not registered in your application. + */ + public function testGetUserWithEmptyContainer() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container + ->expects($this->once()) + ->method('has') + ->with('security.token_storage') + ->will($this->returnValue(false)); + + $controller = new TestController(); + $controller->setContainer($container); + + $controller->getUser(); + } + + /** + * @param $token + * + * @return ContainerInterface + */ + private function getContainerWithTokenStorage($token = null) + { + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage'); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container + ->expects($this->once()) + ->method('has') + ->with('security.token_storage') + ->will($this->returnValue(true)); + + $container + ->expects($this->once()) + ->method('get') + ->with('security.token_storage') + ->will($this->returnValue($tokenStorage)); + + return $container; + } + + public function testRedirectToRoute() + { + $router = $this->getMock('Symfony\Component\Routing\RouterInterface'); + $router->expects($this->once())->method('generate')->willReturn('/foo'); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container->expects($this->at(0))->method('get')->will($this->returnValue($router)); + + $controller = new TestController(); + $controller->setContainer($container); + $response = $controller->redirectToRoute('foo'); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); + $this->assertSame('/foo', $response->getTargetUrl()); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testAddFlash() + { + $flashBag = new FlashBag(); + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\Session'); + $session->expects($this->once())->method('getFlashBag')->willReturn($flashBag); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container->expects($this->at(0))->method('has')->will($this->returnValue(true)); + $container->expects($this->at(1))->method('get')->will($this->returnValue($session)); + + $controller = new TestController(); + $controller->setContainer($container); + $controller->addFlash('foo', 'bar'); + + $this->assertSame(array('bar'), $flashBag->get('foo')); + } + + public function testCreateAccessDeniedException() + { + $controller = new TestController(); + + $this->assertInstanceOf('Symfony\Component\Security\Core\Exception\AccessDeniedException', $controller->createAccessDeniedException()); + } + + public function testIsCsrfTokenValid() + { + $tokenManager = $this->getMock('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface'); + $tokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container->expects($this->at(0))->method('has')->will($this->returnValue(true)); + $container->expects($this->at(1))->method('get')->will($this->returnValue($tokenManager)); + + $controller = new TestController(); + $controller->setContainer($container); + + $this->assertTrue($controller->isCsrfTokenValid('foo', 'bar')); + } + public function testGenerateUrl() { $router = $this->getMock('Symfony\Component\Routing\RouterInterface'); @@ -158,3 +300,31 @@ public function testGetDoctrine() $this->assertEquals($doctrine, $controller->getDoctrine()); } } + +class TestController extends Controller +{ + public function forward($controller, array $path = array(), array $query = array()) + { + return parent::forward($controller, $path, $query); + } + + public function getUser() + { + return parent::getUser(); + } + + public function redirectToRoute($route, array $parameters = array(), $status = 302) + { + return parent::redirectToRoute($route, $parameters, $status); + } + + public function addFlash($type, $message) + { + parent::addFlash($type, $message); + } + + public function isCsrfTokenValid($id, $token) + { + return parent::isCsrfTokenValid($id, $token); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php index fa56a5ad0849..eaca18933059 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php @@ -19,7 +19,7 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; /** - * @author Marcin Sikon + * @author Marcin Sikon */ class RedirectControllerTest extends TestCase { 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 000000000000..975074485996 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddConsoleCommandPassTest.php @@ -0,0 +1,119 @@ + + * + * 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; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +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(); + } + + public function testHttpKernelRegisterCommandsIngoreCommandAsAService() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddConsoleCommandPass()); + $definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\MyCommand'); + $definition->addTag('console.command'); + $container->setDefinition('my-command', $definition); + $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); + } +} + +class MyCommand extends Command +{ +} + +class ExtensionPresentBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddExpressionLanguageProvidersPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddExpressionLanguageProvidersPassTest.php new file mode 100644 index 000000000000..9bc4acb9c156 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/AddExpressionLanguageProvidersPassTest.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\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass; + +class AddExpressionLanguageProvidersPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcessForRouter() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); + + $definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TestProvider'); + $definition->addTag('routing.expression_language_provider'); + $container->setDefinition('some_routing_provider', $definition); + + $container->register('router', '\stdClass'); + $container->compile(); + + $router = $container->getDefinition('router'); + $calls = $router->getMethodCalls(); + $this->assertCount(1, $calls); + $this->assertEquals('addExpressionLanguageProvider', $calls[0][0]); + $this->assertEquals(new Reference('some_routing_provider'), $calls[0][1][0]); + } + + public function testProcessForRouterAlias() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); + + $definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TestProvider'); + $definition->addTag('routing.expression_language_provider'); + $container->setDefinition('some_routing_provider', $definition); + + $container->register('my_router', '\stdClass'); + $container->setAlias('router', 'my_router'); + $container->compile(); + + $router = $container->getDefinition('my_router'); + $calls = $router->getMethodCalls(); + $this->assertCount(1, $calls); + $this->assertEquals('addExpressionLanguageProvider', $calls[0][0]); + $this->assertEquals(new Reference('some_routing_provider'), $calls[0][1][0]); + } + + public function testProcessForSecurity() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); + + $definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TestProvider'); + $definition->addTag('security.expression_language_provider'); + $container->setDefinition('some_security_provider', $definition); + + $container->register('security.access.expression_voter', '\stdClass'); + $container->compile(); + + $router = $container->getDefinition('security.access.expression_voter'); + $calls = $router->getMethodCalls(); + $this->assertCount(1, $calls); + $this->assertEquals('addExpressionLanguageProvider', $calls[0][0]); + $this->assertEquals(new Reference('some_security_provider'), $calls[0][1][0]); + } + + public function testProcessForSecurityAlias() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); + + $definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TestProvider'); + $definition->addTag('security.expression_language_provider'); + $container->setDefinition('some_security_provider', $definition); + + $container->register('my_security.access.expression_voter', '\stdClass'); + $container->setAlias('security.access.expression_voter', 'my_security.access.expression_voter'); + $container->compile(); + + $router = $container->getDefinition('my_security.access.expression_voter'); + $calls = $router->getMethodCalls(); + $this->assertCount(1, $calls); + $this->assertEquals('addExpressionLanguageProvider', $calls[0][0]); + $this->assertEquals(new Reference('some_security_provider'), $calls[0][1][0]); + } +} + +class TestProvider +{ +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/FragmentRendererPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LegacyFragmentRendererPassTest.php similarity index 97% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/FragmentRendererPassTest.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LegacyFragmentRendererPassTest.php index 00e5096ec494..97c98d5d8f1a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/FragmentRendererPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LegacyFragmentRendererPassTest.php @@ -15,7 +15,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FragmentRendererPass; -class FragmentRendererPassTest extends \PHPUnit_Framework_TestCase +/** + * @group legacy + */ +class LegacyFragmentRendererPassTest extends \PHPUnit_Framework_TestCase { /** * Tests that content rendering not implementing FragmentRendererInterface diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LegacyTemplatingAssetHelperPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LegacyTemplatingAssetHelperPassTest.php new file mode 100644 index 000000000000..5cc5c9c558bc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LegacyTemplatingAssetHelperPassTest.php @@ -0,0 +1,124 @@ + + * + * 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\TemplatingAssetHelperPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @group legacy + */ +class LegacyTemplatingAssetHelperPassTest extends \PHPUnit_Framework_TestCase +{ + public function getScopesTests() + { + return array( + array('container'), + array('request'), + ); + } + + /** @dataProvider getScopesTests */ + public function testFindLowestScopeInDefaultPackageWithReference($scope) + { + $container = new ContainerBuilder(); + + $defaultPackage = new Definition('stdClass'); + $defaultPackage->setScope($scope); + $container->setDefinition('default_package', $defaultPackage); + + $definition = new Definition('stdClass', array(new Reference('default_package'))); + $container->setDefinition('templating.helper.assets', $definition); + + $profilerPass = new TemplatingAssetHelperPass(); + $profilerPass->process($container); + + $this->assertSame($scope, $definition->getScope()); + } + + /** @dataProvider getScopesTests */ + public function testFindLowestScopeInDefaultPackageWithDefinition($scope) + { + $container = new ContainerBuilder(); + + $defaultPackage = new Definition('stdClass'); + $defaultPackage->setScope($scope); + + $definition = new Definition('stdClass', array($defaultPackage)); + $container->setDefinition('templating.helper.assets', $definition); + + $profilerPass = new TemplatingAssetHelperPass(); + $profilerPass->process($container); + + $this->assertSame($scope, $definition->getScope()); + } + + /** @dataProvider getScopesTests */ + public function testFindLowestScopeInNamedPackageWithReference($scope) + { + $container = new ContainerBuilder(); + + $defaultPackage = new Definition('stdClass'); + $container->setDefinition('default_package', $defaultPackage); + + $aPackage = new Definition('stdClass'); + $container->setDefinition('a_package', $aPackage); + + $bPackage = new Definition('stdClass'); + $bPackage->setScope($scope); + $container->setDefinition('b_package', $bPackage); + + $cPackage = new Definition('stdClass'); + $container->setDefinition('c_package', $cPackage); + + $definition = new Definition('stdClass', array(new Reference('default_package'), array( + new Reference('a_package'), + new Reference('b_package'), + new Reference('c_package'), + ))); + $container->setDefinition('templating.helper.assets', $definition); + + $profilerPass = new TemplatingAssetHelperPass(); + $profilerPass->process($container); + + $this->assertSame($scope, $definition->getScope()); + } + + /** @dataProvider getScopesTests */ + public function testFindLowestScopeInNamedPackageWithDefinition($scope) + { + $container = new ContainerBuilder(); + + $defaultPackage = new Definition('stdClass'); + + $aPackage = new Definition('stdClass'); + + $bPackage = new Definition('stdClass'); + $bPackage->setScope($scope); + + $cPackage = new Definition('stdClass'); + + $definition = new Definition('stdClass', array($defaultPackage, array( + $aPackage, + $bPackage, + $cPackage, + ))); + $container->setDefinition('templating.helper.assets', $definition); + + $profilerPass = new TemplatingAssetHelperPass(); + $profilerPass->process($container); + + $this->assertSame($scope, $definition->getScope()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php new file mode 100644 index 000000000000..ad0d65390d2f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/LoggingTranslatorPassTest.php @@ -0,0 +1,86 @@ + + * + * 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\LoggingTranslatorPass; + +class LoggingTranslatorPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcess() + { + $definition = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $parameterBag = $this->getMock('Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface'); + + $container->expects($this->exactly(2)) + ->method('hasAlias') + ->will($this->returnValue(true)); + + $container->expects($this->once()) + ->method('getParameter') + ->will($this->returnValue(true)); + + $container->expects($this->once()) + ->method('getAlias') + ->will($this->returnValue('translation.default')); + + $container->expects($this->exactly(3)) + ->method('getDefinition') + ->will($this->returnValue($definition)); + + $container->expects($this->once()) + ->method('hasParameter') + ->with('translator.logging') + ->will($this->returnValue(true)); + + $definition->expects($this->once()) + ->method('getClass') + ->will($this->returnValue('%translator.class%')); + + $parameterBag->expects($this->once()) + ->method('resolveValue') + ->will($this->returnValue("Symfony\Bundle\FrameworkBundle\Translation\Translator")); + + $container->expects($this->once()) + ->method('getParameterBag') + ->will($this->returnValue($parameterBag)); + + $pass = new LoggingTranslatorPass(); + $pass->process($container); + } + + public function testThatCompilerPassIsIgnoredIfThereIsNotLoggerDefinition() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $container->expects($this->once()) + ->method('hasAlias') + ->will($this->returnValue(false)); + + $pass = new LoggingTranslatorPass(); + $pass->process($container); + } + + public function testThatCompilerPassIsIgnoredIfThereIsNotTranslatorDefinition() + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + $container->expects($this->at(0)) + ->method('hasAlias') + ->will($this->returnValue(true)); + + $container->expects($this->at(0)) + ->method('hasAlias') + ->will($this->returnValue(false)); + + $pass = new LoggingTranslatorPass(); + $pass->process($container); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 61eea7a0d0fd..16980c76faea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -19,7 +19,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase public function testDefaultConfig() { $processor = new Processor(); - $config = $processor->processConfiguration(new Configuration(), array(array('secret' => 's3cr3t'))); + $config = $processor->processConfiguration(new Configuration(true), array(array('secret' => 's3cr3t'))); $this->assertEquals( array_merge(array('secret' => 's3cr3t', 'trusted_hosts' => array()), self::getBundleDefaultConfig()), @@ -35,7 +35,7 @@ public function testDoNoDuplicateDefaultFormResources() )); $processor = new Processor(); - $config = $processor->processConfiguration(new Configuration(), array($input)); + $config = $processor->processConfiguration(new Configuration(true), array($input)); $this->assertEquals(array('FrameworkBundle:Form'), $config['templating']['form']['resources']); } @@ -46,7 +46,7 @@ public function testDoNoDuplicateDefaultFormResources() public function testValidTrustedProxies($trustedProxies, $processedProxies) { $processor = new Processor(); - $configuration = new Configuration(); + $configuration = new Configuration(true); $config = $processor->processConfiguration($configuration, array(array( 'secret' => 's3cr3t', 'trusted_proxies' => $trustedProxies, @@ -76,7 +76,7 @@ public function getTestValidTrustedProxiesData() public function testInvalidTypeTrustedProxies() { $processor = new Processor(); - $configuration = new Configuration(); + $configuration = new Configuration(true); $processor->processConfiguration($configuration, array( array( 'secret' => 's3cr3t', @@ -91,7 +91,7 @@ public function testInvalidTypeTrustedProxies() public function testInvalidValueTrustedProxies() { $processor = new Processor(); - $configuration = new Configuration(); + $configuration = new Configuration(true); $processor->processConfiguration($configuration, array( array( 'secret' => 's3cr3t', @@ -100,6 +100,26 @@ public function testInvalidValueTrustedProxies() )); } + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + * @expectedExceptionMessage You cannot use assets settings under "framework.templating" and "assets" configurations in the same project. + * @group legacy + */ + public function testLegacyInvalidValueAssets() + { + $processor = new Processor(); + $configuration = new Configuration(true); + $processor->processConfiguration($configuration, array( + array( + 'templating' => array( + 'engines' => null, + 'assets_base_urls' => '//example.com', + ), + 'assets' => null, + ), + )); + } + protected static function getBundleDefaultConfig() { return array( @@ -107,12 +127,19 @@ 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), + 'ssi' => array('enabled' => false), 'fragments' => array( 'enabled' => false, 'path' => '/_fragment', @@ -130,19 +157,34 @@ protected static function getBundleDefaultConfig() 'translator' => array( 'enabled' => false, 'fallbacks' => array('en'), + 'logging' => true, ), 'validation' => array( 'enabled' => false, 'enable_annotations' => false, + 'static_method' => array('loadValidatorMetadata'), 'translation_domain' => 'validators', + 'strict_email' => false, ), 'annotations' => array( 'cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', - 'debug' => '%kernel.debug%', + 'debug' => true, ), 'serializer' => array( 'enabled' => false, + 'enable_annotations' => false, + ), + 'property_access' => array( + 'magic_call' => false, + 'throw_exception_on_invalid_index' => false, + ), + 'assets' => array( + 'version' => null, + 'version_format' => '%%s?%%s', + 'base_path' => '', + 'base_urls' => array(), + 'packages' => array(), ), ); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php new file mode 100644 index 000000000000..1055251004bd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php @@ -0,0 +1,29 @@ +loadFromExtension('framework', array( + 'assets' => array( + 'version' => 'SomeVersionScheme', + 'base_urls' => 'http://cdn.example.com', + 'version_format' => '%%s?version=%%s', + 'packages' => array( + 'images_path' => array( + 'base_path' => '/foo', + ), + 'images' => array( + 'version' => '1.0.0', + 'base_urls' => array('http://images1.example.com', 'http://images2.example.com'), + ), + 'foo' => array( + 'version' => '1.0.0', + 'version_format' => '%%s-%%s', + ), + 'bar' => array( + 'base_urls' => array('https://bar2.example.com'), + ), + 'bar_null_version' => array( + 'version' => null, + 'base_urls' => array('https://bar3.example.com'), + ), + ), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets_disabled.php new file mode 100644 index 000000000000..bf12a8bc47e5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets_disabled.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', array( + 'templating' => array( + 'engines' => array('php', 'twig'), + ), +)); 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 000000000000..0c34ad10892d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php @@ -0,0 +1,12 @@ +loadFromExtension('framework', array( + 'csrf_protection' => true, + 'form' => array( + 'enabled' => true, + 'csrf_protection' => 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 000000000000..b7bbd8bf3c88 --- /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 000000000000..d1df1c596233 --- /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/default_config.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/default_config.php new file mode 100644 index 000000000000..cd2e56bddf14 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/default_config.php @@ -0,0 +1,3 @@ +loadFromExtension('framework', array()); 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 000000000000..3022ffc98665 --- /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 000000000000..e3d13b4f9223 --- /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 000000000000..db7a6b93dcaa --- /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/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index 95f94e50958b..5022aeaf9f20 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -3,12 +3,15 @@ $container->loadFromExtension('framework', array( 'secret' => 's3cr3t', 'default_locale' => 'fr', - 'form' => null, + 'form' => array( + 'csrf_protection' => array( + 'field_name' => '_csrf', + ), + ), 'http_method_override' => false, 'trusted_proxies' => array('127.0.0.1', '10.0.0.1'), 'csrf_protection' => array( 'enabled' => true, - 'field_name' => '_csrf', ), 'esi' => array( 'enabled' => true, @@ -36,23 +39,9 @@ 'save_path' => '/path/to/sessions', ), 'templating' => array( - 'assets_version' => 'SomeVersionScheme', - 'assets_base_urls' => 'http://cdn.example.com', 'cache' => '/path/to/cache', 'engines' => array('php', 'twig'), 'loader' => array('loader.foo', 'loader.bar'), - 'packages' => array( - 'images' => array( - 'version' => '1.0.0', - 'base_urls' => array('http://images1.example.com', 'http://images2.example.com'), - ), - 'foo' => array( - 'version' => '1.0.0', - ), - 'bar' => array( - 'base_urls' => array('http://bar1.example.com', 'http://bar2.example.com'), - ), - ), 'form' => array( 'resources' => array('theme1', 'theme2'), ), @@ -71,5 +60,15 @@ 'debug' => true, 'file_cache_dir' => '%kernel.cache_dir%/annotations', ), + 'serializer' => array('enabled' => true), 'ide' => 'file%%link%%format', + 'request' => array( + 'formats' => array( + 'csv' => array( + 'text/csv', + 'text/plain', + ), + 'pdf' => 'application/pdf', + ), + ), )); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_templating_assets.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_templating_assets.php new file mode 100644 index 000000000000..15df1015be15 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_templating_assets.php @@ -0,0 +1,27 @@ +loadFromExtension('framework', array( + 'templating' => array( + 'engines' => array('php'), + 'assets_version' => 'SomeVersionScheme', + 'assets_base_urls' => 'http://cdn.example.com', + 'assets_version_format' => '%%s?version=%%s', + 'packages' => array( + 'images' => array( + 'version' => '1.0.0', + 'base_urls' => array('http://images1.example.com', 'http://images2.example.com'), + ), + 'foo' => array( + 'version' => '1.0.0', + 'version_format' => '%%s-%%s', + ), + 'bar' => array( + 'base_urls' => array('https://bar2.example.com'), + ), + 'bar_null_version' => array( + 'version' => null, + 'base_urls' => array('https://bar3.example.com'), + ), + ), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_url_package.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_templating_url_package.php similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_url_package.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_templating_url_package.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php new file mode 100644 index 000000000000..4340e61fc096 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', array( + 'property_access' => array( + 'magic_call' => true, + 'throw_exception_on_invalid_index' => true, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/request.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/request.php new file mode 100644 index 000000000000..1e7cb2921c4c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/request.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', array( + 'request' => array( + 'formats' => array(), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_multiple_static_methods.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_multiple_static_methods.php new file mode 100644 index 000000000000..476da7948e16 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_multiple_static_methods.php @@ -0,0 +1,9 @@ +loadFromExtension('framework', array( + 'secret' => 's3cr3t', + 'validation' => array( + 'enabled' => true, + 'static_method' => array('loadFoo', 'loadBar'), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_no_static_method.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_no_static_method.php new file mode 100644 index 000000000000..b428e06f5c3c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_no_static_method.php @@ -0,0 +1,9 @@ +loadFromExtension('framework', array( + 'secret' => 's3cr3t', + 'validation' => array( + 'enabled' => true, + 'static_method' => false, + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml new file mode 100644 index 000000000000..3e057d5b9025 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml @@ -0,0 +1,26 @@ + + + + + + + http://cdn.example.com + + + http://images1.example.com + http://images2.example.com + + + + https://bar2.example.com + + + https://bar3.example.com + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets_disabled.xml new file mode 100644 index 000000000000..d579ed4c0a18 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets_disabled.xml @@ -0,0 +1,14 @@ + + + + + + php + twig + + + 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 000000000000..4484c353ede2 --- /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 000000000000..635557f938ac --- /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 000000000000..0a08d2351aaf --- /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/default_config.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/default_config.xml new file mode 100644 index 000000000000..af727abdcb8d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/default_config.xml @@ -0,0 +1,9 @@ + + + + + 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 000000000000..8e1803a6c721 --- /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 000000000000..d08ac9e54278 --- /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 000000000000..fdeb60acf993 --- /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/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index bfdaabc71f31..5b16a5979609 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -7,34 +7,36 @@ http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - + + + + - + + + text/csv + text/plain + + + application/pdf + + + loader.foo loader.bar php twig - http://cdn.example.com - - http://images1.example.com - http://images2.example.com - - - - http://bar1.example.com - http://bar2.example.com - theme1 theme2 - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_templating_assets.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_templating_assets.xml new file mode 100644 index 000000000000..3b21e2cb4ed4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_templating_assets.xml @@ -0,0 +1,26 @@ + + + + + + + php + http://cdn.example.com + + http://images1.example.com + http://images2.example.com + + + + https://bar2.example.com + + + https://bar3.example.com + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_url_package.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_templating_url_package.xml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_url_package.xml rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_templating_url_package.xml diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml new file mode 100644 index 000000000000..d7db78a6e2e3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/request.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/request.xml new file mode 100644 index 000000000000..320d1b187525 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/request.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_multiple_static_methods.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_multiple_static_methods.xml new file mode 100644 index 000000000000..053f574bafe9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_multiple_static_methods.xml @@ -0,0 +1,15 @@ + + + + + + + loadFoo + loadBar + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_no_static_method.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_no_static_method.xml new file mode 100644 index 000000000000..d26c7a2be50e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_no_static_method.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml new file mode 100644 index 000000000000..ba87d98b42ba --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml @@ -0,0 +1,19 @@ +framework: + assets: + version: SomeVersionScheme + version_format: '%%s?version=%%s' + base_urls: http://cdn.example.com + packages: + images_path: + base_path: '/foo' + images: + version: 1.0.0 + base_urls: ["http://images1.example.com", "http://images2.example.com"] + foo: + version: 1.0.0 + version_format: '%%s-%%s' + bar: + base_urls: ["https://bar2.example.com"] + bar_null_version: + version: null + base_urls: "https://bar3.example.com" diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets_disabled.yml new file mode 100644 index 000000000000..66ffdf7adc79 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets_disabled.yml @@ -0,0 +1,3 @@ +framework: + templating: + engines: [php, twig] 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 ce5fc591edbf..c08db90f5b7a 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 000000000000..b094cc481fea --- /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 000000000000..b8065b6fb678 --- /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/default_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/default_config.yml new file mode 100644 index 000000000000..00874fbe8d11 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/default_config.yml @@ -0,0 +1 @@ +framework: ~ 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 000000000000..3347cbfaf1bf --- /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 000000000000..a4bb34259f58 --- /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 000000000000..e3ac7e8daf42 --- /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/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index 1e607e9bb6cd..47dcb575317e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -1,12 +1,13 @@ framework: secret: s3cr3t default_locale: fr - form: ~ + form: + csrf_protection: + field_name: _csrf http_method_override: false trusted_proxies: ['127.0.0.1', '10.0.0.1'] csrf_protection: enabled: true - field_name: _csrf esi: enabled: true profiler: @@ -29,19 +30,9 @@ framework: gc_maxlifetime: 90000 save_path: /path/to/sessions templating: - assets_version: SomeVersionScheme - assets_base_urls: http://cdn.example.com engines: [php, twig] loader: [loader.foo, loader.bar] cache: /path/to/cache - packages: - images: - version: 1.0.0 - base_urls: ["http://images1.example.com", "http://images2.example.com"] - foo: - version: 1.0.0 - bar: - base_urls: ["http://images1.example.com", "http://images2.example.com"] form: resources: [theme1, theme2] hinclude_default_template: global_hinclude_template @@ -55,4 +46,9 @@ framework: cache: file debug: true file_cache_dir: '%kernel.cache_dir%/annotations' + serializer: { enabled: true } ide: file%%link%%format + request: + formats: + csv: ['text/csv', 'text/plain'] + pdf: 'application/pdf' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_templating_assets.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_templating_assets.yml new file mode 100644 index 000000000000..f2c8ecfec670 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_templating_assets.yml @@ -0,0 +1,18 @@ +framework: + templating: + engines: [php] + assets_version: SomeVersionScheme + assets_version_format: %%s?version=%%s + assets_base_urls: http://cdn.example.com + packages: + images: + version: 1.0.0 + base_urls: ["http://images1.example.com", "http://images2.example.com"] + foo: + version: 1.0.0 + version_format: %%s-%%s + bar: + base_urls: "https://bar2.example.com" + bar_null_version: + version: null + base_urls: "https://bar3.example.com" diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_url_package.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_templating_url_package.yml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_url_package.yml rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/legacy_templating_url_package.yml diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml new file mode 100644 index 000000000000..b5fd2718ab11 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml @@ -0,0 +1,4 @@ +framework: + property_access: + magic_call: true + throw_exception_on_invalid_index: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/request.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/request.yml new file mode 100644 index 000000000000..9beae1dc5975 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/request.yml @@ -0,0 +1,3 @@ +framework: + request: + formats: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_multiple_static_methods.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_multiple_static_methods.yml new file mode 100644 index 000000000000..6ca343351328 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_multiple_static_methods.yml @@ -0,0 +1,5 @@ +framework: + secret: s3cr3t + validation: + enabled: true + static_method: [loadFoo, loadBar] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_no_static_method.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_no_static_method.yml new file mode 100644 index 000000000000..ca5214964259 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_no_static_method.yml @@ -0,0 +1,5 @@ +framework: + secret: s3cr3t + validation: + enabled: true + static_method: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index caed2c2916a1..3c1a2335141e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -14,7 +14,11 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Validator\Validation; abstract class FrameworkExtensionTest extends TestCase { @@ -32,7 +36,46 @@ 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))); + } + + public function testPropertyAccessWithDefaultValue() + { + $container = $this->createContainerFromFile('full'); + + $def = $container->getDefinition('property_accessor'); + $this->assertFalse($def->getArgument(0)); + $this->assertFalse($def->getArgument(1)); + } + + public function testPropertyAccessWithOverriddenValues() + { + $container = $this->createContainerFromFile('property_accessor'); + $def = $container->getDefinition('property_accessor'); + $this->assertTrue($def->getArgument(0)); + $this->assertTrue($def->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() @@ -125,28 +168,27 @@ public function testNullSessionHandler() $this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0)); } - public function testTemplating() + public function testRequest() { $container = $this->createContainerFromFile('full'); - $this->assertTrue($container->hasDefinition('templating.name_parser'), '->registerTemplatingConfiguration() loads templating.xml'); + $this->assertTrue($container->hasDefinition('request.add_request_formats_listener'), '->registerRequestConfiguration() loads request.xml'); + $listenerDef = $container->getDefinition('request.add_request_formats_listener'); + $this->assertEquals(array('csv' => array('text/csv', 'text/plain'), 'pdf' => array('application/pdf')), $listenerDef->getArgument(0)); + } + + public function testEmptyRequestFormats() + { + $container = $this->createContainerFromFile('request'); - $this->assertEquals('request', $container->getDefinition('templating.helper.assets')->getScope(), '->registerTemplatingConfiguration() sets request scope on assets helper if one or more packages are request-scoped'); + $this->assertFalse($container->hasDefinition('request.add_request_formats_listener'), '->registerRequestConfiguration() does not load request.xml when no request formats are defined'); + } - // default package should have one HTTP base URL and path package SSL URL - $this->assertTrue($container->hasDefinition('templating.asset.default_package.http')); - $package = $container->getDefinition('templating.asset.default_package.http'); - $this->assertInstanceOf('Symfony\\Component\\DependencyInjection\\DefinitionDecorator', $package); - $this->assertEquals('templating.asset.url_package', $package->getParent()); - $arguments = array_values($package->getArguments()); - $this->assertEquals(array('http://cdn.example.com'), $arguments[0]); - $this->assertEquals('SomeVersionScheme', $arguments[1]); - $this->assertEquals('%%s?%%s', $arguments[2]); + public function testTemplating() + { + $container = $this->createContainerFromFile('full'); - $this->assertTrue($container->hasDefinition('templating.asset.default_package.ssl')); - $package = $container->getDefinition('templating.asset.default_package.ssl'); - $this->assertInstanceOf('Symfony\\Component\\DependencyInjection\\DefinitionDecorator', $package); - $this->assertEquals('templating.asset.path_package', $package->getParent()); + $this->assertTrue($container->hasDefinition('templating.name_parser'), '->registerTemplatingConfiguration() loads templating.xml'); $this->assertEquals('templating.engine.delegating', (string) $container->getAlias('templating'), '->registerTemplatingConfiguration() configures delegating loader if multiple engines are provided'); @@ -163,29 +205,28 @@ public function testTemplating() $this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template'), '->registerTemplatingConfiguration() registers the global hinclude.js template'); } - public function testTemplatingAssetsHelperScopeDependsOnPackageArgumentScopes() + /** + * @group legacy + */ + public function testLegacyTemplatingAssets() { - $container = $this->createContainerFromFile('templating_url_package'); + $this->checkAssetsPackages($this->createContainerFromFile('legacy_templating_assets'), true); + } - $this->assertNotEquals('request', $container->getDefinition('templating.helper.assets')->getScope(), '->registerTemplatingConfiguration() does not set request scope on assets helper if no packages are request-scoped'); + public function testAssets() + { + $this->checkAssetsPackages($this->createContainerFromFile('assets')); } public function testTranslator() { $container = $this->createContainerFromFile('full'); - $this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.xml'); $this->assertEquals('translator.default', (string) $container->getAlias('translator'), '->registerTranslatorConfiguration() redefines translator service from identity to real translator'); + $options = $container->getDefinition('translator.default')->getArgument(3); - $resources = array(); - foreach ($container->getDefinition('translator.default')->getMethodCalls() as $call) { - if ('addResource' == $call[0]) { - $resources[] = $call[1]; - } - } - - $files = array_map(function ($resource) { return realpath($resource[1]); }, $resources); - $ref = new \ReflectionClass('Symfony\Component\Validator\Validator'); + $files = array_map(function ($resource) { return realpath($resource); }, $options['resource_files']['en']); + $ref = new \ReflectionClass('Symfony\Component\Validator\Validation'); $this->assertContains( strtr(dirname($ref->getFileName()).'/Resources/translations/validators.en.xlf', '/', DIRECTORY_SEPARATOR), $files, @@ -197,13 +238,9 @@ public function testTranslator() $files, '->registerTranslatorConfiguration() finds Form translation resources' ); - $ref = new \ReflectionClass('Symfony\Component\Security\Core\SecurityContext'); - $ref = dirname($ref->getFileName()); - if (!file_exists($ref.'/composer.json')) { - $ref = dirname($ref); - } + $ref = new \ReflectionClass('Symfony\Component\Security\Core\Security'); $this->assertContains( - strtr($ref.'/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,17 +271,42 @@ public function testValidation() { $container = $this->createContainerFromFile('full'); - $this->assertTrue($container->hasDefinition('validator'), '->registerValidationConfiguration() loads validator.xml'); - $this->assertTrue($container->hasDefinition('validator.mapping.loader.xml_files_loader'), '->registerValidationConfiguration() defines the XML loader'); - $this->assertTrue($container->hasDefinition('validator.mapping.loader.yaml_files_loader'), '->registerValidationConfiguration() defines the YAML loader'); - - $xmlFiles = $container->getParameter('validator.mapping.loader.xml_files_loader.mapping_files'); $ref = new \ReflectionClass('Symfony\Component\Form\Form'); - $this->assertContains( - realpath(dirname($ref->getFileName()).'/Resources/config/validation.xml'), - array_map('realpath', $xmlFiles), - '->registerValidationConfiguration() adds Form validation.xml to XML loader' - ); + $xmlMappings = array(dirname($ref->getFileName()).'/Resources/config/validation.xml'); + + $calls = $container->getDefinition('validator.builder')->getMethodCalls(); + + $this->assertCount(6, $calls); + $this->assertSame('setConstraintValidatorFactory', $calls[0][0]); + $this->assertEquals(array(new Reference('validator.validator_factory')), $calls[0][1]); + $this->assertSame('setTranslator', $calls[1][0]); + $this->assertEquals(array(new Reference('translator')), $calls[1][1]); + $this->assertSame('setTranslationDomain', $calls[2][0]); + $this->assertSame(array('%validator.translation_domain%'), $calls[2][1]); + $this->assertSame('addXmlMappings', $calls[3][0]); + $this->assertSame(array($xmlMappings), $calls[3][1]); + $this->assertSame('addMethodMapping', $calls[4][0]); + $this->assertSame(array('loadValidatorMetadata'), $calls[4][1]); + $this->assertSame('setMetadataCache', $calls[5][0]); + $this->assertEquals(array(new Reference('validator.mapping.cache.apc')), $calls[5][1]); + } + + /** + * @group legacy + * @requires extension apc + */ + public function testLegacyFullyConfiguredValidationService() + { + $container = $this->createContainerFromFile('full'); + + $this->assertInstanceOf('Symfony\Component\Validator\Validator\ValidatorInterface', $container->get('validator')); + } + + public function testValidationService() + { + $container = $this->createContainerFromFile('validation_annotations'); + + $this->assertInstanceOf('Symfony\Component\Validator\Validator\ValidatorInterface', $container->get('validator')); } public function testAnnotations() @@ -266,15 +328,14 @@ public function testValidationAnnotations() { $container = $this->createContainerFromFile('validation_annotations'); - $this->assertTrue($container->hasDefinition('validator.mapping.loader.annotation_loader'), '->registerValidationConfiguration() defines the annotation loader'); - $loaders = $container->getDefinition('validator.mapping.loader.loader_chain')->getArgument(0); - $found = false; - foreach ($loaders as $loader) { - if ('validator.mapping.loader.annotation_loader' === (string) $loader) { - $found = true; - } - } - $this->assertTrue($found, 'validator.mapping.loader.annotation_loader is added to the loader chain.'); + $calls = $container->getDefinition('validator.builder')->getMethodCalls(); + + $this->assertCount(6, $calls); + $this->assertSame('enableAnnotationMapping', $calls[4][0]); + $this->assertEquals(array(new Reference('annotation_reader')), $calls[4][1]); + $this->assertSame('addMethodMapping', $calls[5][0]); + $this->assertSame(array('loadValidatorMetadata'), $calls[5][1]); + // no cache this time } public function testValidationPaths() @@ -285,14 +346,127 @@ public function testValidationPaths() 'kernel.bundles' => array('TestBundle' => 'Symfony\Bundle\FrameworkBundle\Tests\TestBundle'), )); - $yamlArgs = $container->getParameter('validator.mapping.loader.yaml_files_loader.mapping_files'); - $this->assertCount(1, $yamlArgs); - $this->assertStringEndsWith('TestBundle'.DIRECTORY_SEPARATOR.'Resources'.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'validation.yml', $yamlArgs[0]); + $calls = $container->getDefinition('validator.builder')->getMethodCalls(); + + $this->assertCount(7, $calls); + $this->assertSame('addXmlMappings', $calls[3][0]); + $this->assertSame('addYamlMappings', $calls[4][0]); + $this->assertSame('enableAnnotationMapping', $calls[5][0]); + $this->assertSame('addMethodMapping', $calls[6][0]); + $this->assertSame(array('loadValidatorMetadata'), $calls[6][1]); + + $xmlMappings = $calls[3][1][0]; + $this->assertCount(2, $xmlMappings); + try { + // Testing symfony/symfony + $this->assertStringEndsWith('Component'.DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]); + } catch (\Exception $e) { + // Testing symfony/framework-bundle with deps=high + $this->assertStringEndsWith('symfony'.DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]); + } + $this->assertStringEndsWith('TestBundle'.DIRECTORY_SEPARATOR.'Resources'.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'validation.xml', $xmlMappings[1]); + + $yamlMappings = $calls[4][1][0]; + $this->assertCount(1, $yamlMappings); + $this->assertStringEndsWith('TestBundle'.DIRECTORY_SEPARATOR.'Resources'.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'validation.yml', $yamlMappings[0]); + } + + public function testValidationNoStaticMethod() + { + $container = $this->createContainerFromFile('validation_no_static_method'); + + $calls = $container->getDefinition('validator.builder')->getMethodCalls(); + + $this->assertCount(4, $calls); + $this->assertSame('addXmlMappings', $calls[3][0]); + // no cache, no annotations, no static methods + } + + public function testFormsCanBeEnabledWithoutCsrfProtection() + { + $container = $this->createContainerFromFile('form_no_csrf'); - $xmlArgs = $container->getParameter('validator.mapping.loader.xml_files_loader.mapping_files'); - $this->assertCount(2, $xmlArgs); - $this->assertStringEndsWith('Component'.DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlArgs[0]); - $this->assertStringEndsWith('TestBundle'.DIRECTORY_SEPARATOR.'Resources'.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'validation.xml', $xmlArgs[1]); + $this->assertFalse($container->getParameter('form.type_extension.csrf.enabled')); + } + + /** + * @group legacy + */ + public function testLegacyFormCsrfFieldNameCanBeSetUnderCsrfSettings() + { + $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')); + } + + /** + * @group legacy + */ + public function testLegacyFormCsrfFieldNameUnderFormSettingsTakesPrecedence() + { + $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')); + } + + public function testStopwatchEnabledWithDebugModeEnabled() + { + $container = $this->createContainerFromFile('default_config', array( + 'kernel.container_class' => 'foo', + 'kernel.debug' => true, + )); + + $this->assertTrue($container->has('debug.stopwatch')); + } + + public function testStopwatchEnabledWithDebugModeDisabled() + { + $container = $this->createContainerFromFile('default_config', array( + 'kernel.container_class' => 'foo', + )); + + $this->assertTrue($container->has('debug.stopwatch')); + } + + public function testSerializerDisabled() + { + $container = $this->createContainerFromFile('default_config'); + $this->assertFalse($container->has('serializer')); + } + + public function testSerializerEnabled() + { + $container = $this->createContainerFromFile('full'); + $this->assertTrue($container->has('serializer')); + } + + public function testObjectNormalizerRegistered() + { + $container = $this->createContainerFromFile('full'); + + $definition = $container->getDefinition('serializer.normalizer.object'); + $tag = $definition->getTag('serializer.normalizer'); + + $this->assertEquals('Symfony\Component\Serializer\Normalizer\ObjectNormalizer', $definition->getClass()); + $this->assertEquals(-1000, $tag[0]['priority']); + } + + public function testAssetHelperWhenAssetsAreEnabled() + { + $container = $this->createContainerFromFile('full'); + $packages = $container->getDefinition('templating.helper.assets')->getArgument(0); + + $this->assertSame('assets.packages', (string) $packages); + } + + public function testAssetHelperWhenTemplatesAreEnabledAndAssetsAreDisabled() + { + $container = $this->createContainerFromFile('assets_disabled'); + $packages = $container->getDefinition('templating.helper.assets')->getArgument(0); + + $this->assertSame('assets.packages', (string) $packages); } protected function createContainer(array $data = array()) @@ -323,4 +497,74 @@ protected function createContainerFromFile($file, $data = array()) return self::$containerCache[$cacheKey] = $container; } + + protected function createContainerFromClosure($closure, $data = array()) + { + $container = $this->createContainer($data); + $container->registerExtension(new FrameworkExtension()); + $loader = new ClosureLoader($container); + $loader->load($closure); + + $container->getCompilerPassConfig()->setOptimizationPasses(array()); + $container->getCompilerPassConfig()->setRemovingPasses(array()); + $container->compile(); + + return $container; + } + + private function checkAssetsPackages(ContainerBuilder $container, $legacy = false) + { + $packages = $container->getDefinition('assets.packages'); + + // default package + $defaultPackage = $container->getDefinition($packages->getArgument(0)); + $this->assertUrlPackage($container, $defaultPackage, array('http://cdn.example.com'), 'SomeVersionScheme', '%%s?version=%%s'); + + // packages + $packages = $packages->getArgument(1); + $this->assertCount($legacy ? 4 : 5, $packages); + + if (!$legacy) { + $package = $container->getDefinition($packages['images_path']); + $this->assertPathPackage($container, $package, '/foo', 'SomeVersionScheme', '%%s?version=%%s'); + } + + $package = $container->getDefinition($packages['images']); + $this->assertUrlPackage($container, $package, array('http://images1.example.com', 'http://images2.example.com'), '1.0.0', $legacy ? '%%s?%%s' : '%%s?version=%%s'); + + $package = $container->getDefinition($packages['foo']); + $this->assertPathPackage($container, $package, '', '1.0.0', '%%s-%%s'); + + $package = $container->getDefinition($packages['bar']); + $this->assertUrlPackage($container, $package, array('https://bar2.example.com'), $legacy ? null : 'SomeVersionScheme', $legacy ? '%%s?%%s' : '%%s?version=%%s'); + + $this->assertEquals($legacy ? 'assets.empty_version_strategy' : 'assets._version__default', (string) $container->getDefinition('assets._package_bar')->getArgument(1)); + $this->assertEquals('assets.empty_version_strategy', (string) $container->getDefinition('assets._package_bar_null_version')->getArgument(1)); + } + + private function assertPathPackage(ContainerBuilder $container, DefinitionDecorator $package, $basePath, $version, $format) + { + $this->assertEquals('assets.path_package', $package->getParent()); + $this->assertEquals($basePath, $package->getArgument(0)); + $this->assertVersionStrategy($container, $package->getArgument(1), $version, $format); + } + + private function assertUrlPackage(ContainerBuilder $container, DefinitionDecorator $package, $baseUrls, $version, $format) + { + $this->assertEquals('assets.url_package', $package->getParent()); + $this->assertEquals($baseUrls, $package->getArgument(0)); + $this->assertVersionStrategy($container, $package->getArgument(1), $version, $format); + } + + private function assertVersionStrategy(ContainerBuilder $container, Reference $reference, $version, $format) + { + $versionStrategy = $container->getDefinition($reference); + if (null === $version) { + $this->assertEquals('assets.empty_version_strategy', (string) $reference); + } else { + $this->assertEquals('assets.static_version_strategy', $versionStrategy->getParent()); + $this->assertEquals($version, $versionStrategy->getArgument(0)); + $this->assertEquals($format, $versionStrategy->getArgument(1)); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index fad373e47160..b077e80dc43e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -22,4 +22,38 @@ protected function loadFromFile(ContainerBuilder $container, $file) $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/Fixtures/php')); $loader->load($file.'.php'); } + + /** + * @expectedException \LogicException + */ + public function testAssetsCannotHavePathAndUrl() + { + $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', array( + 'assets' => array( + 'base_urls' => 'http://cdn.example.com', + 'base_path' => '/foo', + ), + )); + }); + } + + /** + * @expectedException \LogicException + */ + public function testAssetPackageCannotHavePathAndUrl() + { + $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', array( + 'assets' => array( + 'packages' => array( + 'impossible' => array( + 'base_urls' => 'http://cdn.example.com', + 'base_path' => '/foo', + ), + ), + ), + )); + }); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/YamlFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/YamlFrameworkExtensionTest.php index b8dcefc55802..43070c00c983 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 000000000000..283ed13fa330 --- /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 000000000000..3220d5f1393f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_1.md @@ -0,0 +1,2 @@ +- Service: `service_1` +- Public: yes \ No newline at end of file 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 000000000000..3d40ba008434 --- /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 000000000000..759f8a22aa1d --- /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 000000000000..6998dae2828a --- /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 000000000000..73a4101d8bce --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/alias_2.md @@ -0,0 +1,2 @@ +- Service: `service_2` +- Public: no \ No newline at end of file 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 000000000000..df99e817c96b --- /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 000000000000..847050b33a42 --- /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/array_parameter.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.json new file mode 100644 index 000000000000..cb6809159ad2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.json @@ -0,0 +1,3 @@ +{ + "twig.form.resources": ["bootstrap_3_horizontal_layout.html.twig", "bootstrap_3_layout.html.twig", "form_div_layout.html.twig", "form_table_layout.html.twig"] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.md new file mode 100644 index 000000000000..593be0cab77f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.md @@ -0,0 +1,4 @@ +twig.form.resources +=================== + +["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo... diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.txt new file mode 100644 index 000000000000..182037f2469d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.txt @@ -0,0 +1 @@ +["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo... diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.xml new file mode 100644 index 000000000000..0e16f57fc948 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/array_parameter.xml @@ -0,0 +1,2 @@ + +["bootstrap_3_horizontal_layout.html.twig","bootstrap_3_layo... 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 000000000000..047f4e8c16a4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.json @@ -0,0 +1,32 @@ +{ + "definitions": { + "definition_1": { + "class": "Full\\Qualified\\Class1", + "scope": "container", + "public": true, + "synthetic": false, + "lazy": true, + "synchronized": false, + "abstract": true, + "file": null, + "factory_class": "Full\\Qualified\\FactoryClass", + "factory_method": "get", + "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 000000000000..1c3b958bd92c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md @@ -0,0 +1,40 @@ +Public services +=============== + +Definitions +----------- + +definition_1 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class1` +- Scope: `container` +- Public: yes +- Synthetic: no +- Lazy: yes +- Synchronized: no +- Abstract: yes +- Factory Class: `Full\Qualified\FactoryClass` +- Factory Method: `get` + + +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 000000000000..5fdbdfd439f2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.txt @@ -0,0 +1,6 @@ +[container] Public services + Service ID  Class name  + alias_1 alias for "service_1" + alias_2 alias for "service_2" + definition_1 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 000000000000..b21190dc7983 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.xml @@ -0,0 +1,9 @@ + + + + + + + + + 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 000000000000..3397fd67acd6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.json @@ -0,0 +1,65 @@ +{ + "definitions": { + "definition_1": { + "class": "Full\\Qualified\\Class1", + "scope": "container", + "public": true, + "synthetic": false, + "lazy": true, + "synchronized": false, + "abstract": true, + "file": null, + "factory_class": "Full\\Qualified\\FactoryClass", + "factory_method": "get", + "tags": [ + + ] + }, + "definition_2": { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "lazy": false, + "synchronized": false, + "abstract": false, + "file": "\/path\/to\/file", + "factory_service": "factory.service", + "factory_method": "get", + "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 000000000000..b3018b80b7f3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md @@ -0,0 +1,60 @@ +Public and private services +=========================== + +Definitions +----------- + +definition_1 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class1` +- Scope: `container` +- Public: yes +- Synthetic: no +- Lazy: yes +- Synchronized: no +- Abstract: yes +- Factory Class: `Full\Qualified\FactoryClass` +- Factory Method: `get` + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- Lazy: no +- Synchronized: no +- Abstract: no +- File: `/path/to/file` +- Factory Service: `factory.service` +- Factory Method: `get` +- 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 000000000000..662664fc97f3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt @@ -0,0 +1,7 @@ +[container] Public and private services + Service ID  Class name  + alias_1 alias for "service_1" + alias_2 alias for "service_2" + definition_1 Full\Qualified\Class1 + definition_2 Full\Qualified\Class2 + service_container Symfony\Component\DependencyInjection\ContainerBuilder 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 000000000000..7aecc4f629e7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + 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 000000000000..53bf114e81e0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.json @@ -0,0 +1,43 @@ +{ + "definitions": { + "definition_2": { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "lazy": false, + "synchronized": false, + "abstract": false, + "file": "\/path\/to\/file", + "factory_service": "factory.service", + "factory_method": "get", + "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 000000000000..56a2c390779a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.md @@ -0,0 +1,25 @@ +Public and private services with tag `tag1` +=========================================== + +Definitions +----------- + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- Lazy: no +- Synchronized: no +- Abstract: no +- File: `/path/to/file` +- Factory Service: `factory.service` +- Factory Method: `get` +- 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 000000000000..b506c5c92284 --- /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 Class name  + definition_2 val1 val2 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 000000000000..d6ac0b750b16 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.xml @@ -0,0 +1,16 @@ + + + + + + + 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 000000000000..3837b95cb89e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.json @@ -0,0 +1,30 @@ +{ + "tag1": [ + { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "lazy": false, + "synchronized": false, + "abstract": false, + "file": "\/path\/to\/file", + "factory_service": "factory.service", + "factory_method": "get" + } + ], + "tag2": [ + { + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "lazy": false, + "synchronized": false, + "abstract": false, + "file": "\/path\/to\/file", + "factory_service": "factory.service", + "factory_method": "get" + } + ] +} 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 000000000000..6577037f9c00 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.md @@ -0,0 +1,37 @@ +Container tags +============== + +tag1 +---- + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- Lazy: no +- Synchronized: no +- Abstract: no +- File: `/path/to/file` +- Factory Service: `factory.service` +- Factory Method: `get` + + +tag2 +---- + +definition_2 +~~~~~~~~~~~~ + +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- Lazy: no +- Synchronized: no +- Abstract: no +- File: `/path/to/file` +- Factory Service: `factory.service` +- Factory Method: `get` 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 000000000000..791c1d95c3c9 --- /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 000000000000..be9d2f015bd2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tags.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.json new file mode 100644 index 000000000000..10eccf38fd03 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.json @@ -0,0 +1,4 @@ +{ + "type": "function", + "name": "array_key_exists" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.md new file mode 100644 index 000000000000..9cf31352e3cf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.md @@ -0,0 +1,3 @@ + +- Type: `function` +- Name: `array_key_exists` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.txt new file mode 100644 index 000000000000..09901c3c403c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.txt @@ -0,0 +1 @@ +array_key_exists() \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.xml new file mode 100644 index 000000000000..28d100dfd68c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.json new file mode 100644 index 000000000000..63972540d5a1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.json @@ -0,0 +1,6 @@ +{ + "type": "function", + "name": "staticMethod", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass", + "static": true +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.md new file mode 100644 index 000000000000..c041ee8ea6fc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.md @@ -0,0 +1,5 @@ + +- Type: `function` +- Name: `staticMethod` +- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass` +- Static: yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.txt new file mode 100644 index 000000000000..5e3101e1bc2c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.txt @@ -0,0 +1 @@ +Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::staticMethod() \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.xml new file mode 100644 index 000000000000..df493100520f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.json new file mode 100644 index 000000000000..f77f13ad2705 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.json @@ -0,0 +1,5 @@ +{ + "type": "function", + "name": "method", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.md new file mode 100644 index 000000000000..3b61c0da2ee7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.md @@ -0,0 +1,4 @@ + +- Type: `function` +- Name: `method` +- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.txt new file mode 100644 index 000000000000..dde0dbba2e3d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.txt @@ -0,0 +1 @@ +Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::method() \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.xml new file mode 100644 index 000000000000..367e8b21a1ba --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.json new file mode 100644 index 000000000000..63972540d5a1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.json @@ -0,0 +1,6 @@ +{ + "type": "function", + "name": "staticMethod", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass", + "static": true +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.md new file mode 100644 index 000000000000..c041ee8ea6fc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.md @@ -0,0 +1,5 @@ + +- Type: `function` +- Name: `staticMethod` +- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass` +- Static: yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.txt new file mode 100644 index 000000000000..5e3101e1bc2c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.txt @@ -0,0 +1 @@ +Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::staticMethod() \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.xml new file mode 100644 index 000000000000..df493100520f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.json new file mode 100644 index 000000000000..249b59f3c712 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.json @@ -0,0 +1,7 @@ +{ + "type": "function", + "name": "staticMethod", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\ExtendedCallableClass", + "static": true, + "parent": true +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.md new file mode 100644 index 000000000000..fc69e9bafc01 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.md @@ -0,0 +1,6 @@ + +- Type: `function` +- Name: `staticMethod` +- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\ExtendedCallableClass` +- Static: yes +- Parent: yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.txt new file mode 100644 index 000000000000..1c06a7e9a5cf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.txt @@ -0,0 +1 @@ +Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\ExtendedCallableClass::parent::staticMethod() \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.xml new file mode 100644 index 000000000000..05fbb3de7287 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.json new file mode 100644 index 000000000000..1798fddc4b39 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.json @@ -0,0 +1,3 @@ +{ + "type": "closure" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.md new file mode 100644 index 000000000000..474a9ddc24cc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.md @@ -0,0 +1,2 @@ + +- Type: `closure` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.txt new file mode 100644 index 000000000000..9b030ab7913a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.txt @@ -0,0 +1 @@ +\Closure() \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.xml new file mode 100644 index 000000000000..b0f2ab56bc2b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.json new file mode 100644 index 000000000000..0d4d1e861cf6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "name": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.md new file mode 100644 index 000000000000..c9ad7af95d8c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.md @@ -0,0 +1,3 @@ + +- Type: `object` +- Name: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.txt new file mode 100644 index 000000000000..78ef6a6527ca --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.txt @@ -0,0 +1 @@ +Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::__invoke() \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.xml new file mode 100644 index 000000000000..7a5d812c0764 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.xml @@ -0,0 +1,2 @@ + + 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 000000000000..8de781dfc45a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.json @@ -0,0 +1,15 @@ +{ + "class": "Full\\Qualified\\Class1", + "scope": "container", + "public": true, + "synthetic": false, + "lazy": true, + "synchronized": false, + "abstract": true, + "file": null, + "factory_class": "Full\\Qualified\\FactoryClass", + "factory_method": "get", + "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 000000000000..68d3569732c6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.md @@ -0,0 +1,9 @@ +- Class: `Full\Qualified\Class1` +- Scope: `container` +- Public: yes +- Synthetic: no +- Lazy: yes +- Synchronized: no +- Abstract: yes +- Factory Class: `Full\Qualified\FactoryClass` +- Factory Method: `get` 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 000000000000..af495497dd35 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.txt @@ -0,0 +1,11 @@ +Service Id - +Class Full\Qualified\Class1 +Tags - +Scope container +Public yes +Synthetic no +Lazy yes +Synchronized no +Abstract yes +Factory Class Full\Qualified\FactoryClass +Factory Method get 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 000000000000..92a9bbd70bd3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_1.xml @@ -0,0 +1,4 @@ + + + + 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 000000000000..9d58434c17e1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.json @@ -0,0 +1,33 @@ +{ + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "lazy": false, + "synchronized": false, + "abstract": false, + "file": "\/path\/to\/file", + "factory_service": "factory.service", + "factory_method": "get", + "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 000000000000..6b2f14651d30 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.md @@ -0,0 +1,16 @@ +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- Lazy: no +- Synchronized: no +- Abstract: no +- File: `/path/to/file` +- Factory Service: `factory.service` +- Factory Method: `get` +- Tag: `tag1` + - Attr1: val1 + - Attr2: val2 +- Tag: `tag1` + - Attr3: val3 +- Tag: `tag2` \ No newline at end of file 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 000000000000..28a00d583b09 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt @@ -0,0 +1,15 @@ +Service Id - +Class Full\Qualified\Class2 +Tags + - tag1 (attr1: val1, attr2: val2) + - tag1 (attr3: val3) + - tag2 () +Scope container +Public no +Synthetic yes +Lazy no +Synchronized no +Abstract no +Required File /path/to/file +Factory Service factory.service +Factory Method get 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 000000000000..f128e522e599 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.xml @@ -0,0 +1,14 @@ + + + + + + val1 + val2 + + + val3 + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json new file mode 100644 index 000000000000..e40e130d453c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json @@ -0,0 +1,9 @@ +[ + { + "type": "function", + "name": "global_function" + }, + { + "type": "closure" + } +] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md new file mode 100644 index 000000000000..206c44f71752 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md @@ -0,0 +1,10 @@ +# Registered listeners for event `event1` ordered by descending priority + +## Listener 1 + +- Type: `function` +- Name: `global_function` + +## Listener 2 + +- Type: `closure` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt new file mode 100644 index 000000000000..22b17a19cfb9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt @@ -0,0 +1,8 @@ +[event_dispatcher] Registered listeners for event event1 + ++-------+-------------------+ +| Order | Callable | ++-------+-------------------+ +| #1 | global_function() | +| #2 | \Closure() | ++-------+-------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml new file mode 100644 index 000000000000..4806f1f1280c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json new file mode 100644 index 000000000000..56fc7a4f1e54 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json @@ -0,0 +1,17 @@ +{ + "event1": [ + { + "type": "function", + "name": "global_function" + }, + { + "type": "closure" + } + ], + "event2": [ + { + "type": "object", + "name": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass" + } + ] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md new file mode 100644 index 000000000000..ad4b79e3117f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md @@ -0,0 +1,19 @@ +# Registered listeners + +## event1 + +### Listener 1 + +- Type: `function` +- Name: `global_function` + +### Listener 2 + +- Type: `closure` + +## event2 + +### Listener 1 + +- Type: `object` +- Name: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt new file mode 100644 index 000000000000..95a5b4648e93 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt @@ -0,0 +1,16 @@ +[event_dispatcher] Registered listeners by event + +[Event] event1 ++-------+-------------------+ +| Order | Callable | ++-------+-------------------+ +| #1 | global_function() | +| #2 | \Closure() | ++-------+-------------------+ + +[Event] event2 ++-------+-----------------------------------------------------------------------------------+ +| Order | Callable | ++-------+-----------------------------------------------------------------------------------+ +| #1 | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::__invoke() | ++-------+-----------------------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml new file mode 100644 index 000000000000..3e4b20d82379 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.json new file mode 100644 index 000000000000..6372d9e5b56d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.json @@ -0,0 +1,15 @@ +{ + "class": "Full\\Qualified\\Class1", + "scope": "container", + "public": true, + "synthetic": false, + "lazy": true, + "synchronized": true, + "abstract": true, + "file": null, + "factory_class": "Full\\Qualified\\FactoryClass", + "factory_method": "get", + "tags": [ + + ] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.md new file mode 100644 index 000000000000..d9832a1511ab --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.md @@ -0,0 +1,9 @@ +- Class: `Full\Qualified\Class1` +- Scope: `container` +- Public: yes +- Synthetic: no +- Lazy: yes +- Synchronized: yes +- Abstract: yes +- Factory Class: `Full\Qualified\FactoryClass` +- Factory Method: `get` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.txt new file mode 100644 index 000000000000..3d9cbb2077c3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.txt @@ -0,0 +1,11 @@ +Service Id - +Class Full\Qualified\Class1 +Tags - +Scope container +Public yes +Synthetic no +Lazy yes +Synchronized yes +Abstract yes +Factory Class Full\Qualified\FactoryClass +Factory Method get diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.xml new file mode 100644 index 000000000000..75d082024457 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_1.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.json new file mode 100644 index 000000000000..278a5bfed413 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.json @@ -0,0 +1,33 @@ +{ + "class": "Full\\Qualified\\Class2", + "scope": "container", + "public": false, + "synthetic": true, + "lazy": false, + "synchronized": false, + "abstract": false, + "file": "\/path\/to\/file", + "factory_service": "factory.service", + "factory_method": "get", + "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/legacy_synchronized_service_definition_2.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.md new file mode 100644 index 000000000000..f552debbf18b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.md @@ -0,0 +1,16 @@ +- Class: `Full\Qualified\Class2` +- Scope: `container` +- Public: no +- Synthetic: yes +- Lazy: no +- Synchronized: no +- Abstract: no +- File: `/path/to/file` +- Factory Service: `factory.service` +- Factory Method: `get` +- Tag: `tag1` + - Attr1: val1 + - Attr2: val2 +- Tag: `tag1` + - Attr3: val3 +- Tag: `tag2` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.txt new file mode 100644 index 000000000000..28a00d583b09 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.txt @@ -0,0 +1,15 @@ +Service Id - +Class Full\Qualified\Class2 +Tags + - tag1 (attr1: val1, attr2: val2) + - tag1 (attr3: val3) + - tag2 () +Scope container +Public no +Synthetic yes +Lazy no +Synchronized no +Abstract no +Required File /path/to/file +Factory Service factory.service +Factory Method get diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.xml new file mode 100644 index 000000000000..dd3e2e06d717 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/legacy_synchronized_service_definition_2.xml @@ -0,0 +1,13 @@ + + + + + val1 + val2 + + + val3 + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.json new file mode 100644 index 000000000000..069fdbc5d396 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.json @@ -0,0 +1,3 @@ +{ + "database_name": "symfony" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.md new file mode 100644 index 000000000000..239e98d6aac7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.md @@ -0,0 +1,4 @@ +database_name +============= + +symfony \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.txt new file mode 100644 index 000000000000..6bc5995f62e3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.txt @@ -0,0 +1 @@ +symfony \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.xml new file mode 100644 index 000000000000..8465522c1bc2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameter.xml @@ -0,0 +1,2 @@ + +symfony 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 000000000000..686cbc9e5220 --- /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 000000000000..c1eb4dc90fc4 --- /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!` \ No newline at end of file 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 000000000000..8a1b02c17876 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_1.txt @@ -0,0 +1,6 @@ +[container] List of parameters + Parameter Value  + array [12,"Hello world!",true] + boolean true + integer 12 + string Hello world! 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 000000000000..e97f89573013 --- /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 000000000000..beac79f1f875 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.json @@ -0,0 +1,20 @@ +{ + "path": "\/hello\/{name}", + "pathRegex": "#^\/hello(?:\/(?P[a-z]+))?$#s", + "host": "localhost", + "hostRegex": "#^localhost$#si", + "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" + } +} 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 000000000000..5bfba18b2c81 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.md @@ -0,0 +1,15 @@ +- Path: /hello/{name} +- Path Regex: #^/hello(?:/(?P[a-z]+))?$#s +- Host: localhost +- Host Regex: #^localhost$#si +- 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 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 000000000000..1c07ccde5011 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt @@ -0,0 +1,12 @@ +Path /hello/{name} +Path Regex #^/hello(?:/(?P[a-z]+))?$#s +Host localhost +Host Regex #^localhost$#si +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 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 000000000000..b6040bdad160 --- /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 000000000000..d34b3a77aec6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.json @@ -0,0 +1,18 @@ +{ + "path": "\/name\/add", + "pathRegex": "#^\/name\/add$#s", + "host": "localhost", + "hostRegex": "#^localhost$#si", + "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" + } +} 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 000000000000..0a3f84be17c7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.md @@ -0,0 +1,13 @@ +- Path: /name/add +- Path Regex: #^/name/add$#s +- Host: localhost +- Host Regex: #^localhost$#si +- Scheme: http|https +- Method: PUT|POST +- Class: Symfony\Component\Routing\Route +- Defaults: NONE +- Requirements: NO CUSTOM +- Options: + - `compiler_class`: Symfony\Component\Routing\RouteCompiler + - `opt1`: val1 + - `opt2`: val2 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 000000000000..560c474ae95c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt @@ -0,0 +1,12 @@ +Path /name/add +Path Regex #^/name/add$#s +Host localhost +Host Regex #^localhost$#si +Scheme http|https +Method PUT|POST +Class Symfony\Component\Routing\Route +Defaults NONE +Requirements NO CUSTOM +Options compiler_class: Symfony\Component\Routing\RouteCompiler + opt1: val1 + opt2: val2 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 000000000000..0f94cf7c4176 --- /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 000000000000..7170953f5fee --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.json @@ -0,0 +1,40 @@ +{ + "route_1": { + "path": "\/hello\/{name}", + "pathRegex": "#^\/hello(?:\/(?P[a-z]+))?$#s", + "host": "localhost", + "hostRegex": "#^localhost$#si", + "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" + } + }, + "route_2": { + "path": "\/name\/add", + "pathRegex": "#^\/name\/add$#s", + "host": "localhost", + "hostRegex": "#^localhost$#si", + "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" + } + } +} 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 000000000000..87efcc22e471 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.md @@ -0,0 +1,36 @@ +route_1 +------- + +- Path: /hello/{name} +- Path Regex: #^/hello(?:/(?P[a-z]+))?$#s +- Host: localhost +- Host Regex: #^localhost$#si +- 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 + + +route_2 +------- + +- Path: /name/add +- Path Regex: #^/name/add$#s +- Host: localhost +- Host Regex: #^localhost$#si +- Scheme: http|https +- Method: PUT|POST +- Class: Symfony\Component\Routing\Route +- Defaults: NONE +- Requirements: NO CUSTOM +- Options: + - `compiler_class`: Symfony\Component\Routing\RouteCompiler + - `opt1`: val1 + - `opt2`: val2 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 000000000000..31c796685bbf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.txt @@ -0,0 +1,4 @@ +[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 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 000000000000..6d17820c3143 --- /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/Fixtures/Resources/translations/messages.fr.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/translations/messages.fr.yml new file mode 100644 index 000000000000..767141de628f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/translations/messages.fr.yml @@ -0,0 +1 @@ +folder: répertoire diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fragment/ContainerAwareHIncludeFragmentRendererTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fragment/LegacyContainerAwareHIncludeFragmentRendererTest.php similarity index 90% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Fragment/ContainerAwareHIncludeFragmentRendererTest.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/Fragment/LegacyContainerAwareHIncludeFragmentRendererTest.php index c831d2b53cac..1757cfee9618 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fragment/ContainerAwareHIncludeFragmentRendererTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fragment/LegacyContainerAwareHIncludeFragmentRendererTest.php @@ -15,7 +15,10 @@ use Symfony\Bundle\FrameworkBundle\Fragment\ContainerAwareHIncludeFragmentRenderer; use Symfony\Component\HttpFoundation\Request; -class ContainerAwareHIncludeFragmentRendererTest extends TestCase +/** + * @group legacy + */ +class LegacyContainerAwareHIncludeFragmentRendererTest extends TestCase { public function testRender() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php index cefa7de7c126..783014ee9e1a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php @@ -19,23 +19,7 @@ class FragmentController extends ContainerAware { public function indexAction(Request $request) { - $actions = $this->container->get('templating')->get('actions'); - - $html1 = $actions->render($actions->controller('TestBundle:Fragment:inlined', array( - 'options' => array( - 'bar' => new Bar(), - 'eleven' => 11, - ), - ))); - - $html2 = $actions->render($actions->controller('TestBundle:Fragment:customformat', array('_format' => 'html'))); - - $html3 = $actions->render($actions->controller('TestBundle:Fragment:customlocale', array('_locale' => 'es'))); - - $request->setLocale('fr'); - $html4 = $actions->render($actions->controller('TestBundle:Fragment:forwardlocale')); - - return new Response($html1.'--'.$html2.'--'.$html3.'--'.$html4); + return $this->container->get('templating')->renderResponse('fragment.html.php', array('bar' => new Bar())); } public function inlinedAction($options, $_format) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php index 96bc067fe5a8..bf22e6337013 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php @@ -11,15 +11,15 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\DependencyInjection\ContainerAware; class SessionController extends ContainerAware { - public function welcomeAction($name = null) + public function welcomeAction(Request $request, $name = null) { - $request = $this->container->get('request'); $session = $request->getSession(); // new session case @@ -40,25 +40,23 @@ public function welcomeAction($name = null) return new Response(sprintf('Welcome back %s, nice to meet you.', $name)); } - public function logoutAction() + public function logoutAction(Request $request) { - $request = $this->container->get('request')->getSession('session')->invalidate(); + $request->getSession('session')->invalidate(); return new Response('Session cleared.'); } - public function setFlashAction($message) + public function setFlashAction(Request $request, $message) { - $request = $this->container->get('request'); $session = $request->getSession(); $session->getFlashBag()->set('notice', $message); return new RedirectResponse($this->container->get('router')->generate('session_showflash')); } - public function showFlashAction() + public function showFlashAction(Request $request) { - $request = $this->container->get('request'); $session = $request->getSession(); if ($session->getFlashBag()->has('notice')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Config/CustomConfig.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Config/CustomConfig.php new file mode 100644 index 000000000000..8b4abb7a08d6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Config/CustomConfig.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\Config; + +class CustomConfig +{ + public function addConfiguration($rootNode) + { + $rootNode + ->children() + ->scalarNode('custom')->end() + ->end() + ; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Configuration.php new file mode 100644 index 000000000000..2f45cd98534e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Configuration.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\Tests\Functional\Bundle\TestBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +class Configuration implements ConfigurationInterface +{ + private $customConfig; + + public function __construct($customConfig = null) + { + $this->customConfig = $customConfig; + } + + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('test'); + + if ($this->customConfig) { + $this->customConfig->addConfiguration($rootNode); + } + + return $treeBuilder; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TestExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TestExtension.php new file mode 100644 index 000000000000..38ce8d399051 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TestExtension.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; + +class TestExtension extends Extension implements PrependExtensionInterface +{ + private $customConfig; + + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = $this->getConfiguration($configs, $container); + $config = $this->processConfiguration($configuration, $configs); + } + + /** + * {@inheritdoc} + */ + public function prepend(ContainerBuilder $container) + { + $container->prependExtensionConfig('test', array('custom' => 'foo')); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration(array $config, ContainerBuilder $container) + { + return new Configuration($this->customConfig); + } + + public function setCustomConfig($customConfig) + { + $this->customConfig = $customConfig; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php index d17347cef2be..e7982cfaab7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php @@ -12,7 +12,18 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\Config\CustomConfig; class TestBundle extends Bundle { + public function build(ContainerBuilder $container) + { + parent::build($container); + + /** @var $extension DependencyInjection\TestExtension */ + $extension = $container->getExtension('test'); + + $extension->setCustomConfig(new CustomConfig()); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php new file mode 100644 index 000000000000..dcffe78d9755 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * @group functional + */ +class ConfigDebugCommandTest extends WebTestCase +{ + private $application; + + protected function setUp() + { + $kernel = static::createKernel(array('test_case' => 'ConfigDump', 'root_config' => 'config.yml')); + $this->application = new Application($kernel); + $this->application->doRun(new ArrayInput(array()), new NullOutput()); + } + + public function testDumpBundleName() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(array('name' => 'TestBundle')); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertContains('custom: foo', $tester->getDisplay()); + } + + /** + * @return CommandTester + */ + private function createCommandTester() + { + $command = $this->application->find('debug:config'); + + return new CommandTester($command); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php new file mode 100644 index 000000000000..b0ac90c31f5f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.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\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * @group functional + */ +class ConfigDumpReferenceCommandTest extends WebTestCase +{ + private $application; + + protected function setUp() + { + $kernel = static::createKernel(array('test_case' => 'ConfigDump', 'root_config' => 'config.yml')); + $this->application = new Application($kernel); + $this->application->doRun(new ArrayInput(array()), new NullOutput()); + } + + public function testDumpBundleName() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(array('name' => 'TestBundle')); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertContains('test:', $tester->getDisplay()); + $this->assertContains(' custom:', $tester->getDisplay()); + } + + /** + * @return CommandTester + */ + private function createCommandTester() + { + $command = $this->application->find('config:dump-reference'); + + return new CommandTester($command); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php new file mode 100644 index 000000000000..a73987bcc986 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; + +return array( + new FrameworkBundle(), + new TestBundle(), +); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml new file mode 100644 index 000000000000..377d3e785206 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml @@ -0,0 +1,2 @@ +imports: + - { resource: ../config/default.yml } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Resources/views/fragment.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Resources/views/fragment.html.php new file mode 100644 index 000000000000..8ea3c36f8a4d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Resources/views/fragment.html.php @@ -0,0 +1,14 @@ +get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:inlined', array( + 'options' => array( + 'bar' => $bar, + 'eleven' => 11, + ), + ))); +?>--get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:customformat', array('_format' => 'html'))); +?>--get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:customlocale', array('_locale' => 'es'))); +?>--getRequest()->setLocale('fr'); + echo $this->get('actions')->render($this->get('actions')->controller('TestBundle:Fragment:forwardlocale')); +?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php index 6b31fb2e4921..057aa6c8091a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php @@ -41,7 +41,7 @@ public function testRedirectWhenNoSlash() public function testSchemeRedirect() { $coll = new RouteCollection(); - $coll->add('foo', new Route('/foo', array(), array('_scheme' => 'https'))); + $coll->add('foo', new Route('/foo', array(), array(), array(), '', array('https'))); $matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php index 0d3dab84889a..330e0659ca14 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php @@ -28,16 +28,18 @@ public function testGenerateWithServiceParam() ), array( '_locale' => 'en|es', - ) + ), array(), '', array(), array(), '"%foo%" == "bar"' )); $sc = $this->getServiceContainer($routes); $sc->setParameter('locale', 'es'); + $sc->setParameter('foo', 'bar'); $router = new Router($sc, 'foo'); $this->assertSame('/en', $router->generate('foo', array('_locale' => 'en'))); $this->assertSame('/', $router->generate('foo', array('_locale' => 'es'))); + $this->assertSame('"bar" == "bar"', $router->getRouteCollection()->get('foo')->getCondition()); } public function testDefaultsPlaceholders() 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 000000000000..c5d2d8bbebed --- /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/GlobalVariablesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php new file mode 100644 index 000000000000..48a7737dd808 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.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\Bundle\FrameworkBundle\Tests\Templating; + +use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables; +use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\DependencyInjection\Container; + +class GlobalVariablesTest extends TestCase +{ + private $container; + private $globals; + + protected function setUp() + { + $this->container = new Container(); + $this->globals = new GlobalVariables($this->container); + } + + /** + * @group legacy + */ + public function testLegacyGetSecurity() + { + $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + + $this->assertNull($this->globals->getSecurity()); + $this->container->set('security.context', $securityContext); + $this->assertSame($securityContext, $this->globals->getSecurity()); + } + + public function testGetUserNoTokenStorage() + { + $this->assertNull($this->globals->getUser()); + } + + public function testGetUserNoToken() + { + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $this->container->set('security.token_storage', $tokenStorage); + $this->assertNull($this->globals->getUser()); + } + + /** + * @dataProvider getUserProvider + */ + public function testGetUser($user, $expectedUser) + { + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + $this->container->set('security.token_storage', $tokenStorage); + + $token + ->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + + $this->assertSame($expectedUser, $this->globals->getUser()); + } + + public function getUserProvider() + { + $user = $this->getMock('Symfony\Component\Security\Core\User\UserInterface'); + $std = new \stdClass(); + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + return array( + array($user, $user), + array($std, $std), + array($token, $token), + array('Anon.', null), + array(null, null), + array(10, null), + array(true, null), + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php new file mode 100644 index 000000000000..9460799684bd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.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\Bundle\FrameworkBundle\Tests\Templating\Helper; + +use Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper; +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +class AssetsHelperTest extends \PHPUnit_Framework_TestCase +{ + /** + * @group legacy + */ + public function testLegacyGetUrl() + { + $versionStrategy = new StaticVersionStrategy('22', '%s?version=%s'); + $package = new Package($versionStrategy); + $imagePackage = new PathPackage('images', $versionStrategy); + $packages = new Packages($package, array('images' => $imagePackage)); + $helper = new AssetsHelper($packages); + + $this->assertEquals('me.png?version=42', $helper->getUrl('me.png', null, '42')); + $this->assertEquals('/images/me.png?version=42', $helper->getUrl('me.png', 'images', '42')); + } + + /** + * @group legacy + */ + public function testLegacyGetVersion() + { + $package = new Package(new StaticVersionStrategy('22')); + $imagePackage = new Package(new StaticVersionStrategy('42')); + $packages = new Packages($package, array('images' => $imagePackage)); + $helper = new AssetsHelper($packages); + + $this->assertEquals('22', $helper->getVersion()); + $this->assertEquals('22', $helper->getVersion('/foo')); + $this->assertEquals('42', $helper->getVersion('images')); + $this->assertEquals('42', $helper->getVersion('/foo', 'images')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php index 7d71a1e1c3d7..217fd5e50e23 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php @@ -27,6 +27,10 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest */ protected $engine; + protected $testableFeatures = array( + 'choice_attr', + ); + protected function getExtensions() { // should be moved to the Form component once absolute file paths are supported @@ -44,7 +48,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', )), )); @@ -124,4 +128,39 @@ public static function themeInheritanceProvider() array(array('TestBundle:Parent'), array('TestBundle:Child')), ); } + + public function testRange() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testRangeWithMinMaxValues() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testLabelWithoutTranslationOnButton() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testButtonlabelWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testAttributesNotTranslatedWhenTranslationDomainIsFalse() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php index 9d5ee6a8b959..99bfba0c4e7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php @@ -27,6 +27,10 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest */ protected $engine; + protected $testableFeatures = array( + 'choice_attr', + ); + protected function getExtensions() { // should be moved to the Form component once absolute file paths are supported @@ -44,7 +48,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', )), @@ -111,4 +115,39 @@ protected function setTheme(FormView $view, array $themes) { $this->engine->get('form')->setTheme($view, $themes); } + + public function testRange() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testRangeWithMinMaxValues() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testLabelWithoutTranslationOnButton() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testButtonlabelWithoutTranslation() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } + + public function testAttributesNotTranslatedWhenTranslationDomainIsFalse() + { + // No-op for forward compatibility with AbstractLayoutTest 2.8 + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php index d3d4f22385c2..d626e63e41cf 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/Resources/Custom/_name_c_entry_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_c_entry_label.html.php new file mode 100644 index 000000000000..05240035c023 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_c_entry_label.html.php @@ -0,0 +1,2 @@ +humanize($name); } ?> + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_names_entry_label.html.php similarity index 63% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_names_entry_label.html.php index 0efecf0c721e..e165a429a5ff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_names_entry_label.html.php @@ -1,2 +1,4 @@ -humanize($name); } ?> +humanize($name); +} ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php index 4bd043ed5948..5517afdb732f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php @@ -12,33 +12,37 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; 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\Bundle\FrameworkBundle\Templating\Helper\SessionHelper; class SessionHelperTest extends \PHPUnit_Framework_TestCase { - protected $request; + protected $requestStack; protected function setUp() { - $this->request = new Request(); + $request = new Request(); $session = new Session(new MockArraySessionStorage()); $session->set('foobar', 'bar'); $session->getFlashBag()->set('notice', 'bar'); - $this->request->setSession($session); + $request->setSession($session); + + $this->requestStack = new RequestStack(); + $this->requestStack->push($request); } protected function tearDown() { - $this->request = null; + $this->requestStack = null; } public function testFlash() { - $helper = new SessionHelper($this->request); + $helper = new SessionHelper($this->requestStack); $this->assertTrue($helper->hasFlash('notice')); @@ -47,13 +51,13 @@ public function testFlash() public function testGetFlashes() { - $helper = new SessionHelper($this->request); + $helper = new SessionHelper($this->requestStack); $this->assertEquals(array('notice' => array('bar')), $helper->getFlashes()); } public function testGet() { - $helper = new SessionHelper($this->request); + $helper = new SessionHelper($this->requestStack); $this->assertEquals('bar', $helper->get('foobar')); $this->assertEquals('foo', $helper->get('bar', 'foo')); @@ -63,7 +67,7 @@ public function testGet() public function testGetName() { - $helper = new SessionHelper($this->request); + $helper = new SessionHelper($this->requestStack); $this->assertEquals('session', $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 000000000000..1a0ff5f548ce --- /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 476b398e61b1..b5eeb901342d 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()); + $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 ed0e6c6bdce9..9411c1cb4718 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; use Symfony\Bundle\FrameworkBundle\Templating\TimedPhpEngine; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; class TimedPhpEngineTest extends TestCase @@ -38,7 +40,7 @@ public function testThatRenderLogsTime() } /** - * @return \Symfony\Component\DependencyInjection\Container + * @return Container */ private function getContainer() { @@ -60,7 +62,7 @@ private function getTemplateNameParser() } /** - * @return \Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables + * @return GlobalVariables */ private function getGlobalVariables() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php index 135c79635411..420d2f2d71dd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/PhpExtractorTest.php @@ -17,7 +17,12 @@ class PhpExtractorTest extends TestCase { - public function testExtraction() + /** + * @dataProvider resourcesProvider + * + * @param array|string $resource + */ + public function testExtraction($resource) { // Arrange $extractor = new PhpExtractor(); @@ -25,7 +30,7 @@ public function testExtraction() $catalogue = new MessageCatalogue('en'); // Act - $extractor->extract(__DIR__.'/../Fixtures/Resources/views/', $catalogue); + $extractor->extract($resource, $catalogue); $expectedHeredoc = <<assertEquals($expectedCatalogue, $actualCatalogue); } + + public function resourcesProvider() + { + $directory = __DIR__.'/../Fixtures/Resources/views/'; + $splFiles = array(); + foreach (new \DirectoryIterator($directory) as $fileInfo) { + if ($fileInfo->isDot()) { + continue; + } + if ('translation.html.php' === $fileInfo->getBasename()) { + $phpFile = $fileInfo->getPathname(); + } + $splFiles[] = $fileInfo->getFileInfo(); + } + + return array( + array($directory), + array($phpFile), + array(glob($directory.'*')), + array($splFiles), + array(new \ArrayObject(glob($directory.'*'))), + array(new \ArrayObject($splFiles)), + ); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index 3617d3858a85..e09b0d3f33a8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Translation; use Symfony\Bundle\FrameworkBundle\Translation\Translator; -use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Translation\MessageSelector; @@ -95,144 +94,44 @@ public function testTransWithCaching() $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); } - public function testRefreshCacheWhenResourcesAreNoLongerFresh() - { - $resource = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); - $loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface'); - $resource->method('isFresh')->will($this->returnValue(false)); - $loader - ->expects($this->exactly(2)) - ->method('load') - ->will($this->returnValue($this->getCatalogue('fr', array(), array($resource)))); - - // prime the cache - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'debug' => true)); - $translator->setLocale('fr'); - $translator->trans('foo'); - - // prime the cache second time - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'debug' => true)); - $translator->setLocale('fr'); - $translator->trans('foo'); - } - public function testTransWithCachingWithInvalidLocale() { $loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface'); - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale'); + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'loader', '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale'); $translator->setLocale('invalid locale'); $this->setExpectedException('\InvalidArgumentException'); $translator->trans('foo'); } - public function testGetLocale() + public function testLoadResourcesWithoutCaching() { - $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); - - $request - ->expects($this->once()) - ->method('getLocale') - ->will($this->returnValue('en')) - ; - - $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - - $container - ->expects($this->exactly(2)) - ->method('isScopeActive') - ->with('request') - ->will($this->onConsecutiveCalls(false, true)) - ; - - $container - ->expects($this->once()) - ->method('has') - ->with('request') - ->will($this->returnValue(true)) - ; - - $container - ->expects($this->once()) - ->method('get') - ->with('request') - ->will($this->returnValue($request)) - ; + $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $resourceFiles = array( + 'fr' => array( + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ), + ); - $translator = new Translator($container, new MessageSelector()); + $translator = $this->getTranslator($loader, array('resource_files' => $resourceFiles), 'yml'); + $translator->setLocale('fr'); - $this->assertNull($translator->getLocale()); - $this->assertSame('en', $translator->getLocale()); + $this->assertEquals('répertoire', $translator->trans('folder')); } - public function testGetLocaleWithInvalidLocale() + public function testGetDefaultLocale() { - $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); - - $request - ->expects($this->once()) - ->method('getLocale') - ->will($this->returnValue('foo bar')) - ; - $request - ->expects($this->once()) - ->method('getDefaultLocale') - ->will($this->returnValue('en-US')) - ; - $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - - $container - ->expects($this->once()) - ->method('isScopeActive') - ->with('request') - ->will($this->returnValue(true)) - ; - $container ->expects($this->once()) - ->method('has') - ->with('request') - ->will($this->returnValue(true)) - ; - - $container - ->expects($this->any()) - ->method('get') - ->with('request') - ->will($this->returnValue($request)) + ->method('getParameter') + ->with('kernel.default_locale') + ->will($this->returnValue('en')) ; $translator = new Translator($container, new MessageSelector()); - $this->assertSame('en-US', $translator->getLocale()); - } - public function testDifferentCacheFilesAreUsedForDifferentSetsOfFallbackLocales() - { - /* - * Because the cache file contains a catalogue including all of its fallback - * catalogues, we must take the active set of fallback locales into - * consideration when loading a catalogue from the cache. - */ - $translator = $this->createTranslator(new ArrayLoader(), array('cache_dir' => $this->tmpDir)); - $translator->setLocale('a'); - $translator->setFallbackLocales(array('b')); - $translator->addResource('loader', array('foo' => 'foo (a)'), 'a'); - $translator->addResource('loader', array('bar' => 'bar (b)'), 'b'); - - $this->assertEquals('bar (b)', $translator->trans('bar')); - - // Remove fallback locale - $translator->setFallbackLocales(array()); - $this->assertEquals('bar', $translator->trans('bar')); - - // Use a fresh translator with no fallback locales, result should be the same - $translator = $this->createTranslator(new ArrayLoader(), array('cache_dir' => $this->tmpDir)); - $translator->setLocale('a'); - $translator->addResource('loader', array('foo' => 'foo (a)'), 'a'); - $translator->addResource('loader', array('bar' => 'bar (b)'), 'b'); - - $this->assertEquals('bar', $translator->trans('bar')); + $this->assertSame('en', $translator->getLocale()); } protected function getCatalogue($locale, $messages, $resources = array()) @@ -318,31 +217,56 @@ protected function getContainer($loader) return $container; } - public function getTranslator($loader, $options = array(), $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator') + public function getTranslator($loader, $options = array(), $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator') { - $translator = $this->createTranslator($loader, $options, $translatorClass); - - $translator->addResource('loader', 'foo', 'fr'); - $translator->addResource('loader', 'foo', 'en'); - $translator->addResource('loader', 'foo', 'es'); - $translator->addResource('loader', 'foo', 'pt-PT'); // European Portuguese - $translator->addResource('loader', 'foo', 'pt_BR'); // Brazilian Portuguese - $translator->addResource('loader', 'foo', 'fr.UTF-8'); - $translator->addResource('loader', 'foo', 'sr@latin'); // Latin Serbian + $translator = $this->createTranslator($loader, $options, $translatorClass, $loaderFomat); + + if ('loader' === $loaderFomat) { + $translator->addResource('loader', 'foo', 'fr'); + $translator->addResource('loader', 'foo', 'en'); + $translator->addResource('loader', 'foo', 'es'); + $translator->addResource('loader', 'foo', 'pt-PT'); // European Portuguese + $translator->addResource('loader', 'foo', 'pt_BR'); // Brazilian Portuguese + $translator->addResource('loader', 'foo', 'fr.UTF-8'); + $translator->addResource('loader', 'foo', 'sr@latin'); // Latin Serbian + } return $translator; } - private function createTranslator($loader, $options, $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator') + public function testWarmup() { - $translator = new $translatorClass( + $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $resourceFiles = array( + 'fr' => array( + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ), + ); + + // prime the cache + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml'); + $translator->setFallbackLocales(array('fr')); + $translator->warmup($this->tmpDir); + + $loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface'); + $loader + ->expects($this->never()) + ->method('load'); + + $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml'); + $translator->setLocale('fr'); + $translator->setFallbackLocales(array('fr')); + $this->assertEquals('répertoire', $translator->trans('folder')); + } + + private function createTranslator($loader, $options, $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator', $loaderFomat = 'loader') + { + return new $translatorClass( $this->getContainer($loader), new MessageSelector(), - array('loader' => array('loader')), + array($loaderFomat => array($loaderFomat)), $options ); - - return $translator; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php index f68f8a7979ae..cf7f3c5769bf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/PhpExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Translation; use Symfony\Component\Finder\Finder; +use Symfony\Component\Translation\Extractor\AbstractFileExtractor; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Extractor\ExtractorInterface; @@ -20,7 +21,7 @@ * * @author Michel Salib */ -class PhpExtractor implements ExtractorInterface +class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface { const MESSAGE_TOKEN = 300; @@ -54,11 +55,9 @@ class PhpExtractor implements ExtractorInterface /** * {@inheritdoc} */ - public function extract($directory, MessageCatalogue $catalog) + public function extract($resource, MessageCatalogue $catalog) { - // load any existing translation files - $finder = new Finder(); - $files = $finder->files()->name('*.php')->in($directory); + $files = $this->extractFiles($resource); foreach ($files as $file) { $this->parseTokens(token_get_all(file_get_contents($file)), $catalog); @@ -179,4 +178,28 @@ protected function parseTokens($tokens, MessageCatalogue $catalog) } } } + + /** + * @param string $file + * + * @return bool + * + * @throws \InvalidArgumentException + */ + protected function canBeExtracted($file) + { + return $this->isFile($file) && 'php' === pathinfo($file, PATHINFO_EXTENSION); + } + + /** + * @param string|array $directory + * + * @return array + */ + protected function extractFromDirectory($directory) + { + $finder = new Finder(); + + return $finder->files()->name('*.php')->in($directory); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index 75b995dacbc5..fba6d70d6978 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -11,22 +11,32 @@ namespace Symfony\Bundle\FrameworkBundle\Translation; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Translator as BaseTranslator; use Symfony\Component\Translation\MessageSelector; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\Config\ConfigCache; /** * Translator. * * @author Fabien Potencier */ -class Translator extends BaseTranslator +class Translator extends BaseTranslator implements WarmableInterface { protected $container; - protected $options; protected $loaderIds; + protected $options = array( + 'cache_dir' => null, + 'debug' => false, + 'resource_files' => array(), + ); + + /** + * @var array + */ + private $resourceLocales; + /** * Constructor. * @@ -34,6 +44,7 @@ class Translator extends BaseTranslator * * * cache_dir: The cache directory (or null to disable caching) * * debug: Whether to enable debugging or not (false by default) + * * resource_files: List of translation resources available grouped by locale. * * @param ContainerInterface $container A ContainerInterface instance * @param MessageSelector $selector The message selector for pluralization @@ -47,110 +58,53 @@ 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))); } $this->options = array_merge($this->options, $options); - - parent::__construct(null, $selector); - } - - /** - * {@inheritdoc} - */ - public function getLocale() - { - if (null === $this->locale && $this->container->isScopeActive('request') && $this->container->has('request')) { - try { - $this->setLocale($this->container->get('request')->getLocale()); - } catch (\InvalidArgumentException $e) { - $this->setLocale($this->container->get('request')->getDefaultLocale()); - } + $this->resourceLocales = array_keys($this->options['resource_files']); + if (null !== $this->options['cache_dir'] && $this->options['debug']) { + $this->loadResources(); } - return $this->locale; + parent::__construct($container->getParameter('kernel.default_locale'), $selector, $this->options['cache_dir'], $this->options['debug']); } /** * {@inheritdoc} */ - protected function loadCatalogue($locale) + public function warmUp($cacheDir) { - if (isset($this->catalogues[$locale])) { - return; - } - + // skip warmUp when translator doesn't use cache if (null === $this->options['cache_dir']) { - $this->initialize(); - - return parent::loadCatalogue($locale); + return; } - $this->assertValidLocale($locale); - - $cache = new ConfigCache($this->getCatalogueCachePath($locale), $this->options['debug']); - if (!$cache->isFresh()) { - $this->initialize(); - - parent::loadCatalogue($locale); - - $fallbackContent = ''; - $current = ''; - $replacementPattern = '/[^a-z0-9_]/i'; - foreach ($this->computeFallbackLocales($locale) as $fallback) { - $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); - $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); - - $fallbackContent .= sprintf(<<<'EOF' -$catalogue%s = new MessageCatalogue('%s', %s); -$catalogue%s->addFallbackCatalogue($catalogue%s); - - -EOF - , - $fallbackSuffix, - $fallback, - var_export($this->catalogues[$fallback]->all(), true), - $currentSuffix, - $fallbackSuffix - ); - $current = $fallback; + $locales = array_merge($this->getFallbackLocales(), array($this->getLocale()), $this->resourceLocales); + foreach (array_unique($locales) as $locale) { + // reset catalogue in case it's already loaded during the dump of the other locales. + if (isset($this->catalogues[$locale])) { + unset($this->catalogues[$locale]); } - $content = sprintf(<<catalogues[$locale]->all(), true), - $fallbackContent - ); - - $cache->write($content, $this->catalogues[$locale]->getResources()); - - return; + $this->loadCatalogue($locale); } + } - $this->catalogues[$locale] = include $cache; + /** + * {@inheritdoc} + */ + protected function initializeCatalogue($locale) + { + $this->initialize(); + parent::initializeCatalogue($locale); } protected function initialize() { + $this->loadResources(); foreach ($this->loaderIds as $id => $aliases) { foreach ($aliases as $alias) { $this->addLoader($alias, $this->container->get($id)); @@ -158,8 +112,15 @@ protected function initialize() } } - private function getCatalogueCachePath($locale) + private function loadResources() { - return $this->options['cache_dir'].'/catalogue.'.$locale.'.'.sha1(serialize($this->getFallbackLocales())).'.php'; + foreach ($this->options['resource_files'] as $locale => $files) { + foreach ($files as $key => $file) { + // filename is domain.locale.format + list($domain, $locale, $format) = explode('.', basename($file), 3); + $this->addResource($format, $file, $locale, $domain); + unset($this->options['resource_files'][$locale][$key]); + } + } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index bbb16f953d35..bcfce7b70700 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -16,51 +16,56 @@ } ], "require": { - "php": ">=5.3.3", - "symfony/dependency-injection": "~2.3", - "symfony/config": "~2.3,>=2.3.12", - "symfony/event-dispatcher": "~2.1", + "php": ">=5.3.9", + "symfony/asset": "~2.7", + "symfony/dependency-injection": "~2.6,>=2.6.2", + "symfony/config": "~2.4", + "symfony/event-dispatcher": "~2.5", "symfony/finder": "~2.0,>=2.0.5", - "symfony/http-foundation": "~2.3,>=2.3.19", - "symfony/http-kernel": "~2.3,>=2.3.22", + "symfony/http-foundation": "~2.4.9|~2.5,>=2.5.4", + "symfony/http-kernel": "~2.7", "symfony/filesystem": "~2.3", - "symfony/routing": "~2.3", + "symfony/routing": "~2.6,>2.6.4", + "symfony/security-core": "~2.6.13|~2.7.9|~2.8", + "symfony/security-csrf": "~2.6", "symfony/stopwatch": "~2.3", "symfony/templating": "~2.1", - "symfony/translation": "~2.3,>=2.3.19", - "doctrine/common": "~2.2" + "symfony/translation": "~2.7", + "doctrine/annotations": "~1.0" }, "require-dev": { - "symfony/browser-kit": "~2.3", - "symfony/console": "~2.3", + "symfony/browser-kit": "~2.4", + "symfony/console": "~2.7", "symfony/css-selector": "~2.0,>=2.0.5", "symfony/dom-crawler": "~2.0,>=2.0.5", "symfony/intl": "~2.3", - "symfony/security": "~2.3", - "symfony/form": "~2.3.31", + "symfony/security": "~2.6", + "symfony/form": "~2.7,>=2.7.2", "symfony/class-loader": "~2.1", + "symfony/expression-language": "~2.6", "symfony/process": "~2.0,>=2.0.5", - "symfony/validator": "~2.1", + "symfony/validator": "~2.5", "symfony/yaml": "~2.0,>=2.0.5" }, "suggest": { "symfony/console": "For using the console commands", "symfony/form": "For using forms", - "symfony/validator": "For using validation", "symfony/serializer": "For using the serializer service", - "symfony/process": "For using the server:run command" + "symfony/validator": "For using validation", + "symfony/yaml": "For using the debug:config and lint:yaml commands", + "symfony/process": "For using the server:run, server:start, server:stop, and server:status commands", + "doctrine/cache": "For using alternative cache drivers" }, "autoload": { - "psr-0": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, + "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bundle/FrameworkBundle", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 724254de78b4..657777f02000 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,23 @@ CHANGELOG ========= +2.6.0 +----- + + * Added the possibility to override the default success/failure handler + to get the provider key and the options injected + * Deprecated the `security.context` service for the `security.token_storage` and + `security.authorization_checker` services. + +2.4.0 +----- + + * Added 'host' option to firewall configuration + * Added 'csrf_token_generator' and 'csrf_token_id' options to firewall logout + listener configuration to supersede/alias 'csrf_provider' and 'intention' + respectively + * Moved 'security.secure_random' service configuration to FrameworkBundle + 2.3.0 ----- @@ -74,9 +91,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/Command/SetAclCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/SetAclCommand.php new file mode 100644 index 000000000000..ba3478234627 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/SetAclCommand.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +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\Security\Acl\Domain\ObjectIdentity; +use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; +use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; +use Symfony\Component\Security\Acl\Exception\AclAlreadyExistsException; +use Symfony\Component\Security\Acl\Permission\MaskBuilder; +use Symfony\Component\Security\Acl\Model\MutableAclProviderInterface; + +/** + * Sets ACL for objects. + * + * @author Kévin Dunglas + */ +class SetAclCommand extends ContainerAwareCommand +{ + /** + * {@inheritdoc} + */ + public function isEnabled() + { + if (!$this->getContainer()->has('security.acl.provider')) { + return false; + } + + $provider = $this->getContainer()->get('security.acl.provider'); + if (!$provider instanceof MutableAclProviderInterface) { + return false; + } + + return parent::isEnabled(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('acl:set') + ->setDescription('Sets ACL for objects') + ->setHelp(<<%command.name% command sets ACL. +The ACL system must have been initialized with the init:acl command. + +To set VIEW and EDIT permissions for the user kevin on the instance of +Acme\MyClass having the identifier 42: + + php %command.full_name% --user=Symfony/Component/Security/Core/User/User:kevin VIEW EDIT Acme/MyClass:42 + +Note that you can use / instead of \\ for the namespace delimiter to avoid any +problem. + +To set permissions for a role, use the --role option: + + php %command.full_name% --role=ROLE_USER VIEW Acme/MyClass:1936 + +To set permissions at the class scope, use the --class-scope option: + + php %command.full_name% --class-scope --user=Symfony/Component/Security/Core/User/User:anne OWNER Acme/MyClass:42 + +EOF + ) + ->addArgument('arguments', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'A list of permissions and object identities (class name and ID separated by a column)') + ->addOption('user', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A list of security identities') + ->addOption('role', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'A list of roles') + ->addOption('class-scope', null, InputOption::VALUE_NONE, 'Use class-scope entries') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + // Parse arguments + $objectIdentities = array(); + $maskBuilder = $this->getMaskBuilder(); + foreach ($input->getArgument('arguments') as $argument) { + $data = explode(':', $argument, 2); + + if (count($data) > 1) { + $objectIdentities[] = new ObjectIdentity($data[1], strtr($data[0], '/', '\\')); + } else { + $maskBuilder->add($data[0]); + } + } + + // Build permissions mask + $mask = $maskBuilder->get(); + + $userOption = $input->getOption('user'); + $roleOption = $input->getOption('role'); + $classScopeOption = $input->getOption('class-scope'); + + if (empty($userOption) && empty($roleOption)) { + throw new \InvalidArgumentException('A Role or a User must be specified.'); + } + + // Create security identities + $securityIdentities = array(); + + if ($userOption) { + foreach ($userOption as $user) { + $data = explode(':', $user, 2); + + if (count($data) === 1) { + throw new \InvalidArgumentException('The user must follow the format "Acme/MyUser:username".'); + } + + $securityIdentities[] = new UserSecurityIdentity($data[1], strtr($data[0], '/', '\\')); + } + } + + if ($roleOption) { + foreach ($roleOption as $role) { + $securityIdentities[] = new RoleSecurityIdentity($role); + } + } + + /** @var $container \Symfony\Component\DependencyInjection\ContainerInterface */ + $container = $this->getContainer(); + /** @var $aclProvider MutableAclProviderInterface */ + $aclProvider = $container->get('security.acl.provider'); + + // Sets ACL + foreach ($objectIdentities as $objectIdentity) { + // Creates a new ACL if it does not already exist + try { + $aclProvider->createAcl($objectIdentity); + } catch (AclAlreadyExistsException $e) { + } + + $acl = $aclProvider->findAcl($objectIdentity, $securityIdentities); + + foreach ($securityIdentities as $securityIdentity) { + if ($classScopeOption) { + $acl->insertClassAce($securityIdentity, $mask); + } else { + $acl->insertObjectAce($securityIdentity, $mask); + } + } + + $aclProvider->updateAcl($acl); + } + } + + /** + * Gets the mask builder. + * + * @return MaskBuilder + */ + protected function getMaskBuilder() + { + return new MaskBuilder(); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php new file mode 100644 index 000000000000..5dbd830a39da --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +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\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; + +/** + * Encode a user's password. + * + * @author Sarah Khalil + */ +class UserPasswordEncoderCommand extends ContainerAwareCommand +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('security:encode-password') + ->setDescription('Encodes a password.') + ->addArgument('password', InputArgument::OPTIONAL, 'The plain password to encode.') + ->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.', 'Symfony\Component\Security\Core\User\User') + ->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.') + ->setHelp(<<%command.name% command encodes passwords according to your +security configuration. This command is mainly used to generate passwords for +the in_memory user provider type and for changing passwords +in the database while developing the application. + +Suppose that you have the following security configuration in your application: + + +# app/config/security.yml +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + AppBundle\Entity\User: bcrypt + + +If you execute the command non-interactively, the default Symfony User class +is used and a random salt is generated to encode the password: + + php %command.full_name% --no-interaction [password] + +Pass the full user class path as the second argument to encode passwords for +your own entities: + + php %command.full_name% --no-interaction [password] AppBundle\Entity\User + +Executing the command interactively allows you to generate a random salt for +encoding the password: + + php %command.full_name% [password] AppBundle\Entity\User + +In case your encoder doesn't require a salt, add the empty-salt option: + + php %command.full_name% --empty-salt [password] AppBundle\Entity\User + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output = new SymfonyStyle($input, $output); + + $input->isInteractive() ? $output->title('Symfony Password Encoder Utility') : $output->newLine(); + + $password = $input->getArgument('password'); + $userClass = $input->getArgument('user-class'); + $emptySalt = $input->getOption('empty-salt'); + + $encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); + $bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder; + + if ($bcryptWithoutEmptySalt) { + $emptySalt = true; + } + + if (!$password) { + if (!$input->isInteractive()) { + $output->error('The password must not be empty.'); + + return 1; + } + $passwordQuestion = $this->createPasswordQuestion($input, $output); + $password = $output->askQuestion($passwordQuestion); + } + + $salt = null; + + if ($input->isInteractive() && !$emptySalt) { + $emptySalt = true; + + $output->note('The command will take care of generating a salt for you. Be aware that some encoders advise to let them generate their own salt. If you\'re using one of those encoders, please answer \'no\' to the question below. '.PHP_EOL.'Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'); + + if ($output->confirm('Confirm salt generation ?')) { + $salt = $this->generateSalt(); + $emptySalt = false; + } + } elseif (!$emptySalt) { + $salt = $this->generateSalt(); + } + + $encodedPassword = $encoder->encodePassword($password, $salt); + + $rows = array( + array('Encoder used', get_class($encoder)), + array('Encoded password', $encodedPassword), + ); + if (!$emptySalt) { + $rows[] = array('Generated salt', $salt); + } + $output->table(array('Key', 'Value'), $rows); + + if (!$emptySalt) { + $output->note(sprintf('Make sure that your salt storage field fits the salt length: %s chars', strlen($salt))); + } elseif ($bcryptWithoutEmptySalt) { + $output->note('Bcrypt encoder used: the encoder generated its own built-in salt.'); + } + + $output->success('Password encoding succeeded'); + } + + /** + * Create the password question to ask the user for the password to be encoded. + * + * @return Question + */ + private function createPasswordQuestion() + { + $passwordQuestion = new Question('Type in your password to be encoded'); + + return $passwordQuestion->setValidator(function ($value) { + if ('' === trim($value)) { + throw new \Exception('The password must not be empty.'); + } + + return $value; + })->setHidden(true)->setMaxAttempts(20); + } + + private function generateSalt() + { + return base64_encode($this->getContainer()->get('security.secure_random')->nextBytes(30)); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 737ee2139ebc..a6ea333d95b8 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -11,11 +11,12 @@ namespace Symfony\Bundle\SecurityBundle\DataCollector; -use Symfony\Component\Security\Core\Role\RoleInterface; -use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\Security\Core\Role\RoleInterface; /** * SecurityDataCollector. @@ -24,16 +25,19 @@ */ class SecurityDataCollector extends DataCollector { - private $context; + private $tokenStorage; + private $roleHierarchy; /** * Constructor. * - * @param SecurityContextInterface|null $context + * @param TokenStorageInterface|null $tokenStorage + * @param RoleHierarchyInterface|null $roleHierarchy */ - public function __construct(SecurityContextInterface $context = null) + public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null) { - $this->context = $context; + $this->tokenStorage = $tokenStorage; + $this->roleHierarchy = $roleHierarchy; } /** @@ -41,29 +45,45 @@ public function __construct(SecurityContextInterface $context = null) */ public function collect(Request $request, Response $response, \Exception $exception = null) { - if (null === $this->context) { + if (null === $this->tokenStorage) { $this->data = array( 'enabled' => false, 'authenticated' => false, 'token_class' => null, 'user' => '', 'roles' => array(), + 'inherited_roles' => array(), + 'supports_role_hierarchy' => null !== $this->roleHierarchy, ); - } elseif (null === $token = $this->context->getToken()) { + } elseif (null === $token = $this->tokenStorage->getToken()) { $this->data = array( 'enabled' => true, 'authenticated' => false, 'token_class' => null, 'user' => '', 'roles' => array(), + 'inherited_roles' => array(), + 'supports_role_hierarchy' => null !== $this->roleHierarchy, ); } else { + $inheritedRoles = array(); + $assignedRoles = $token->getRoles(); + if (null !== $this->roleHierarchy) { + $allRoles = $this->roleHierarchy->getReachableRoles($assignedRoles); + foreach ($allRoles as $role) { + if (!in_array($role, $assignedRoles, true)) { + $inheritedRoles[] = $role; + } + } + } $this->data = array( 'enabled' => true, 'authenticated' => $token->isAuthenticated(), 'token_class' => get_class($token), 'user' => $token->getUsername(), - 'roles' => array_map(function (RoleInterface $role) { return $role->getRole(); }, $token->getRoles()), + 'roles' => array_map(function (RoleInterface $role) { return $role->getRole();}, $assignedRoles), + 'inherited_roles' => array_map(function (RoleInterface $role) { return $role->getRole(); }, $inheritedRoles), + 'supports_role_hierarchy' => null !== $this->roleHierarchy, ); } } @@ -98,6 +118,27 @@ public function getRoles() return $this->data['roles']; } + /** + * Gets the inherited roles of the user. + * + * @return array The inherited roles + */ + public function getInheritedRoles() + { + return $this->data['inherited_roles']; + } + + /** + * Checks if the data contains information about inherited roles. Still the inherited + * roles can be an empty array. + * + * @return bool true if the profile was contains inherited role information. + */ + public function supportsRoleHierarchy() + { + return $this->data['supports_role_hierarchy']; + } + /** * Checks if the user is authenticated or not. * diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 439ce6104647..36dbcdf89a1b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -70,7 +71,10 @@ public function getConfigTreeBuilder() ->arrayNode('access_decision_manager') ->addDefaultsIfNotSet() ->children() - ->enumNode('strategy')->values(array('affirmative', 'consensus', 'unanimous'))->defaultValue('affirmative')->end() + ->enumNode('strategy') + ->values(array(AccessDecisionManager::STRATEGY_AFFIRMATIVE, AccessDecisionManager::STRATEGY_CONSENSUS, AccessDecisionManager::STRATEGY_UNANIMOUS)) + ->defaultValue(AccessDecisionManager::STRATEGY_AFFIRMATIVE) + ->end() ->booleanNode('allow_if_all_abstain')->defaultFalse()->end() ->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end() ->end() @@ -175,6 +179,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() @@ -205,6 +210,11 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto $firewallNodeBuilder ->scalarNode('pattern')->end() + ->scalarNode('host')->end() + ->arrayNode('methods') + ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end() + ->prototype('scalar')->end() + ->end() ->booleanNode('security')->defaultTrue()->end() ->scalarNode('request_matcher')->end() ->scalarNode('access_denied_url')->end() @@ -216,10 +226,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() @@ -251,7 +287,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->arrayNode('anonymous') ->canBeUnset() ->children() - ->scalarNode('key')->defaultValue(uniqid())->end() + ->scalarNode('key')->defaultValue(uniqid('', true))->end() ->end() ->end() ->arrayNode('switch_user') diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index a797bfc75ab8..cdb0a96fe417 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -20,6 +20,7 @@ * AbstractFactory is the base class for all classes inheriting from * AbstractAuthenticationListener. * + * @author Fabien Potencier * @author Lukas Kahwe Smith * @author Johannes M. Schmitt */ @@ -169,30 +170,47 @@ protected function createListener($container, $id, $config, $userProvider) protected function createAuthenticationSuccessHandler($container, $id, $config) { + $successHandlerId = $this->getSuccessHandlerId($id); + $options = array_intersect_key($config, $this->defaultSuccessHandlerOptions); + if (isset($config['success_handler'])) { - return $config['success_handler']; + $successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.custom_success_handler')); + $successHandler->replaceArgument(0, new Reference($config['success_handler'])); + $successHandler->replaceArgument(1, $options); + $successHandler->replaceArgument(2, $id); + } else { + $successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler')); + $successHandler->addMethodCall('setOptions', array($options)); + $successHandler->addMethodCall('setProviderKey', array($id)); } - $successHandlerId = 'security.authentication.success_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); - - $successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler')); - $successHandler->replaceArgument(1, array_intersect_key($config, $this->defaultSuccessHandlerOptions)); - $successHandler->addMethodCall('setProviderKey', array($id)); - return $successHandlerId; } protected function createAuthenticationFailureHandler($container, $id, $config) { + $id = $this->getFailureHandlerId($id); + $options = array_intersect_key($config, $this->defaultFailureHandlerOptions); + if (isset($config['failure_handler'])) { - return $config['failure_handler']; + $failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.custom_failure_handler')); + $failureHandler->replaceArgument(0, new Reference($config['failure_handler'])); + $failureHandler->replaceArgument(1, $options); + } else { + $failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler')); + $failureHandler->addMethodCall('setOptions', array($options)); } - $id = 'security.authentication.failure_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); + return $id; + } - $failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler')); - $failureHandler->replaceArgument(2, array_intersect_key($config, $this->defaultFailureHandlerOptions)); + protected function getSuccessHandlerId($id) + { + return 'security.authentication.success_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); + } - return $id; + 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 ce06bba7d281..b674c47e15bf 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/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index e7fb75773ca4..7aa4f5baa03e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -102,6 +102,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, $listenerId = 'security.authentication.listener.rememberme.'.$id; $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.rememberme')); $listener->replaceArgument(1, new Reference($rememberMeServicesId)); + $listener->replaceArgument(5, $config['catch_exceptions']); return array($authProviderId, $listenerId, $defaultEntryPoint); } @@ -130,6 +131,7 @@ public function addConfiguration(NodeDefinition $node) ->end() ->prototype('scalar')->end() ->end() + ->scalarNode('catch_exceptions')->defaultTrue()->end() ; foreach ($this->options as $name => $value) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php new file mode 100644 index 000000000000..01ac91ce2ce9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\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; + +/** + * RemoteUserFactory creates services for REMOTE_USER based authentication. + * + * @author Fabien Potencier + * @author Maxime Douailin + */ +class RemoteUserFactory implements SecurityFactoryInterface +{ + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + { + $providerId = 'security.authentication.provider.pre_authenticated.'.$id; + $container + ->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.pre_authenticated')) + ->replaceArgument(0, new Reference($userProvider)) + ->addArgument($id) + ; + + $listenerId = 'security.authentication.listener.remote_user.'.$id; + $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.remote_user')); + $listener->replaceArgument(2, $id); + $listener->replaceArgument(3, $config['user']); + + return array($providerId, $listenerId, $defaultEntryPoint); + } + + public function getPosition() + { + return 'pre_auth'; + } + + public function getKey() + { + return 'remote-user'; + } + + public function addConfiguration(NodeDefinition $node) + { + $node + ->children() + ->scalarNode('provider')->end() + ->scalarNode('user')->defaultValue('REMOTE_USER')->end() + ->end() + ; + } +} 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 000000000000..9da6601ff547 --- /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 000000000000..27d8c5f050ec --- /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/Security/UserProvider/InMemoryFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/InMemoryFactory.php index 5debce9adbdd..b184385f221f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/InMemoryFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/InMemoryFactory.php @@ -54,7 +54,7 @@ public function addConfiguration(NodeDefinition $node) ->useAttributeAsKey('name') ->prototype('array') ->children() - ->scalarNode('password')->defaultValue(uniqid())->end() + ->scalarNode('password')->defaultValue(uniqid('', true))->end() ->arrayNode('roles') ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end() ->prototype('scalar')->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 972b4f8f90c9..36c16e0dbc7d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -22,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. @@ -32,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() { @@ -63,6 +66,11 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('templating_twig.xml'); $loader->load('collectors.xml'); + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + $container->removeDefinition('security.expression_language'); + $container->removeDefinition('security.access.expression_voter'); + } + // set some global scalars $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); @@ -92,10 +100,11 @@ public function load(array $configs, ContainerBuilder $container) // add some required classes for compilation $this->addClassesToCompile(array( 'Symfony\\Component\\Security\\Http\\Firewall', - 'Symfony\\Component\\Security\\Core\\SecurityContext', 'Symfony\\Component\\Security\\Core\\User\\UserProviderInterface', 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationProviderManager', + 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorage', 'Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager', + 'Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker', 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface', 'Symfony\\Bundle\\SecurityBundle\\Security\\FirewallMap', 'Symfony\\Bundle\\SecurityBundle\\Security\\FirewallContext', @@ -186,8 +195,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'])); } } @@ -242,8 +256,11 @@ 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; + $methods = isset($firewall['methods']) ? $firewall['methods'] : array(); + $matcher = $this->createRequestMatcher($container, $pattern, $host, $methods); } // Security disabled? @@ -280,7 +297,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); @@ -296,8 +313,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 @@ -319,15 +336,15 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listener->addMethodCall('addHandler', array(new Reference($handlerId))); } - // register with LogoutUrlHelper + // register with LogoutUrlGenerator $container - ->getDefinition('templating.helper.logout_url') + ->getDefinition('security.logout_url_generator') ->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, )) ; } @@ -566,6 +583,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', 'trust_resolver'))->getNodes())) + ; + + return $this->expressions[$id] = new Reference($id); + } + private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array()) { if ($methods) { @@ -624,4 +657,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 53758abb761b..8faa9ac366fd 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 f6106f7f7051..8f6a608c6de8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -10,8 +10,9 @@ - - + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 956bf89aa4ee..b7c1407c1cc5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -32,26 +32,38 @@ 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 + + + + + + %security.access.always_authenticate_before_granting% + + @@ -76,8 +88,19 @@ + + + + + + + + + + + @@ -104,6 +127,13 @@ + + + + + + + @@ -122,6 +152,12 @@ + + + + + + @@ -136,11 +172,8 @@ - + - - - 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 b33709031a1f..9464f00a27b8 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 543ca38b7869..917e90f79281 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,20 +39,23 @@ 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 - + + @@ -75,7 +82,7 @@ - + @@ -83,7 +90,7 @@ - + @@ -104,7 +111,7 @@ - + @@ -116,11 +123,22 @@ + + + + + + + + + + + @@ -134,9 +152,33 @@ parent="security.authentication.listener.abstract" abstract="true" /> + + + + + + + + + + + + + + + + + + + + + - + @@ -145,9 +187,19 @@ + + + + + + + + + + - + @@ -156,7 +208,7 @@ - + @@ -171,6 +223,12 @@ %security.authentication.hide_user_not_found% + + + + + + @@ -178,7 +236,7 @@ - + @@ -191,7 +249,7 @@ - + @@ -204,7 +262,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml index f430e04cd5d8..d9fb0d261e44 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml @@ -19,11 +19,13 @@ - + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml index c91bf7970c3e..b10d060d8ae1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml @@ -12,13 +12,12 @@ - - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml index d457cf1485cd..9fe6c4adde80 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml @@ -5,19 +5,19 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Bundle\SecurityBundle\Twig\Extension\LogoutUrlExtension + Symfony\Bridge\Twig\Extension\LogoutUrlExtension Symfony\Bridge\Twig\Extension\SecurityExtension - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index f15836509c14..923be8381064 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -31,7 +31,7 @@ {% endif %} {% endset %} {% set icon %} - Security + {% if collector.user %}
{{ collector.user }}
{% endif %} {% endset %} @@ -40,7 +40,7 @@ {% block menu %} - + Security {% endblock %} @@ -67,6 +67,12 @@ Roles {{ collector.roles|yaml_encode }} + {% if collector.supportsRoleHierarchy %} + + Inherited Roles + {{ collector.inheritedRoles|yaml_encode }} + + {% endif %} {% if collector.tokenClass != null %} Token class diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 7d810fde389f..72f7b68de959 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -19,6 +19,9 @@ 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\RemoteUserFactory; +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 +41,9 @@ public function build(ContainerBuilder $container) $extension->addSecurityListenerFactory(new HttpDigestFactory()); $extension->addSecurityListenerFactory(new RememberMeFactory()); $extension->addSecurityListenerFactory(new X509Factory()); + $extension->addSecurityListenerFactory(new RemoteUserFactory()); + $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 248984ec54bc..6819c3760987 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php @@ -12,8 +12,9 @@ namespace Symfony\Bundle\SecurityBundle\Templating\Helper; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Templating\Helper\Helper; /** @@ -23,94 +24,55 @@ */ class LogoutUrlHelper extends Helper { - private $container; - private $listeners; - private $router; + private $generator; /** * Constructor. * - * @param ContainerInterface $container A ContainerInterface instance - * @param UrlGeneratorInterface $router A Router instance - */ - public function __construct(ContainerInterface $container, UrlGeneratorInterface $router) - { - $this->container = $container; - $this->router = $router; - $this->listeners = array(); - } - - /** - * Registers a firewall's LogoutListener, allowing its URL to be generated. + * @param ContainerInterface|LogoutUrlGenerator $generator A ContainerInterface or LogoutUrlGenerator instance + * @param UrlGeneratorInterface|null $router The router service + * @param TokenStorageInterface|null $tokenStorage The token storage service * - * @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 + * @deprecated Passing a ContainerInterface as a first argument is deprecated since 2.7 and will be removed in 3.0. + * @deprecated Passing a second and third argument is deprecated since 2.7 and will be removed in 3.0. */ - public function registerListener($key, $logoutPath, $intention, $csrfParameter, CsrfProviderInterface $csrfProvider = null) + public function __construct($generator, UrlGeneratorInterface $router = null, TokenStorageInterface $tokenStorage = null) { - $this->listeners[$key] = array($logoutPath, $intention, $csrfParameter, $csrfProvider); + if ($generator instanceof ContainerInterface) { + @trigger_error('The '.__CLASS__.' constructor will require a LogoutUrlGenerator instead of a ContainerInterface instance in 3.0.', E_USER_DEPRECATED); + + if ($generator->has('security.logout_url_generator')) { + $this->generator = $generator->get('security.logout_url_generator'); + } else { + $this->generator = new LogoutUrlGenerator($generator->get('request_stack'), $router, $tokenStorage); + } + } else { + $this->generator = $generator; + } } /** * Generates the absolute logout path for the firewall. * - * @param string $key The firewall key + * @param string|null $key The firewall key or null to use the current firewall key * * @return string The logout path */ public function getLogoutPath($key) { - return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_PATH); + return $this->generator->getLogoutPath($key, UrlGeneratorInterface::ABSOLUTE_PATH); } /** * Generates the absolute logout URL for the firewall. * - * @param string $key The firewall key + * @param string|null $key The firewall key or null to use the current firewall key * * @return string The logout URL */ public function getLogoutUrl($key) { - return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_URL); - } - - /** - * Generates the logout URL for the firewall. - * - * @param string $key The firewall key - * @param bool|string $referenceType The type of reference (one of the constants in UrlGeneratorInterface) - * - * @return string The logout URL - * - * @throws \InvalidArgumentException if no LogoutListener is registered for the key - */ - private function generateLogoutUrl($key, $referenceType) - { - if (!array_key_exists($key, $this->listeners)) { - throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key)); - } - - list($logoutPath, $intention, $csrfParameter, $csrfProvider) = $this->listeners[$key]; - - $parameters = null !== $csrfProvider ? array($csrfParameter => $csrfProvider->generateCsrfToken($intention)) : array(); - - if ('/' === $logoutPath[0]) { - $request = $this->container->get('request'); - - $url = UrlGeneratorInterface::ABSOLUTE_URL === $referenceType ? $request->getUriForPath($logoutPath) : $request->getBaseUrl().$logoutPath; - - if (!empty($parameters)) { - $url .= '?'.http_build_query($parameters); - } - } else { - $url = $this->router->generate($logoutPath, $parameters, $referenceType); - } - - return $url; + return $this->generator->getLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_URL); } /** diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php index a43f2b446d6a..7df8511f8a05 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php @@ -13,30 +13,25 @@ use Symfony\Component\Security\Acl\Voter\FieldVote; use Symfony\Component\Templating\Helper\Helper; -use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; /** - * SecurityHelper provides read-only access to the security context. + * SecurityHelper provides read-only access to the security checker. * * @author Fabien Potencier */ class SecurityHelper extends Helper { - private $context; + private $securityChecker; - /** - * Constructor. - * - * @param SecurityContextInterface $context A SecurityContext instance - */ - public function __construct(SecurityContextInterface $context = null) + public function __construct(AuthorizationCheckerInterface $securityChecker = null) { - $this->context = $context; + $this->securityChecker = $securityChecker; } public function isGranted($role, $object = null, $field = null) { - if (null === $this->context) { + if (null === $this->securityChecker) { return false; } @@ -44,7 +39,7 @@ public function isGranted($role, $object = null, $field = null) $object = new FieldVote($object, $field); } - return $this->context->isGranted($role, $object); + return $this->securityChecker->isGranted($role, $object); } /** diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 8127233aafb4..acf4788adc7e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -12,8 +12,10 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DataCollector; use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Security\Core\Role\RoleHierarchy; class SecurityDataCollectorTest extends \PHPUnit_Framework_TestCase { @@ -26,58 +28,98 @@ public function testCollectWhenSecurityIsDisabled() $this->assertFalse($collector->isEnabled()); $this->assertFalse($collector->isAuthenticated()); $this->assertNull($collector->getTokenClass()); + $this->assertFalse($collector->supportsRoleHierarchy()); $this->assertCount(0, $collector->getRoles()); + $this->assertCount(0, $collector->getInheritedRoles()); $this->assertEmpty($collector->getUser()); } public function testCollectWhenAuthenticationTokenIsNull() { - $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); - $securityContext->expects($this->once())->method('getToken')->willReturn(null); + $tokenStorage = new TokenStorage(); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector->collect($this->getRequest(), $this->getResponse()); + + $this->assertTrue($collector->isEnabled()); + $this->assertFalse($collector->isAuthenticated()); + $this->assertNull($collector->getTokenClass()); + $this->assertTrue($collector->supportsRoleHierarchy()); + $this->assertCount(0, $collector->getRoles()); + $this->assertCount(0, $collector->getInheritedRoles()); + $this->assertEmpty($collector->getUser()); + } - $collector = new SecurityDataCollector($securityContext); + /** + * @group legacy + */ + public function testLegacyCollectWhenAuthenticationTokenIsNull() + { + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); $collector->collect($this->getRequest(), $this->getResponse()); $this->assertTrue($collector->isEnabled()); $this->assertFalse($collector->isAuthenticated()); $this->assertNull($collector->getTokenClass()); + $this->assertTrue($collector->supportsRoleHierarchy()); $this->assertCount(0, $collector->getRoles()); + $this->assertCount(0, $collector->getInheritedRoles()); $this->assertEmpty($collector->getUser()); } /** @dataProvider provideRoles */ - public function testCollectAuthenticationTokenAndRoles(array $roles, array $normalizedRoles) + public function testCollectAuthenticationTokenAndRoles(array $roles, array $normalizedRoles, array $inheritedRoles) { - $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); - $securityContext - ->expects($this->once()) - ->method('getToken') - ->willReturn(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles)); + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles)); - $collector = new SecurityDataCollector($securityContext); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); $collector->collect($this->getRequest(), $this->getResponse()); $this->assertTrue($collector->isEnabled()); $this->assertTrue($collector->isAuthenticated()); $this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()); + $this->assertTrue($collector->supportsRoleHierarchy()); $this->assertSame($normalizedRoles, $collector->getRoles()); + $this->assertSame($inheritedRoles, $collector->getInheritedRoles()); $this->assertSame('hhamon', $collector->getUser()); } public function provideRoles() { return array( + // Basic roles array( array('ROLE_USER'), array('ROLE_USER'), + array(), ), array( array(new Role('ROLE_USER')), array('ROLE_USER'), + array(), + ), + // Inherited roles + array( + array('ROLE_ADMIN'), + array('ROLE_ADMIN'), + array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + ), + array( + array(new Role('ROLE_ADMIN')), + array('ROLE_ADMIN'), + array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), ), ); } + private function getRoleHierarchy() + { + return new RoleHierarchy(array( + 'ROLE_ADMIN' => array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'), + )); + } + private function getRequest() { return $this diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 0e7736aa2b95..8a0a14ddb32a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -79,16 +79,51 @@ public function testFirewalls() 'security.channel_listener', 'security.logout_listener.secure', 'security.authentication.listener.x509.secure', + 'security.authentication.listener.remote_user.secure', 'security.authentication.listener.form.secure', 'security.authentication.listener.basic.secure', 'security.authentication.listener.digest.secure', + 'security.authentication.listener.rememberme.secure', 'security.authentication.listener.anonymous.secure', 'security.authentication.switchuser_listener.secure', 'security.access_listener', ), + 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', + array('GET', 'POST'), + ), + ), $matchers); + } + public function testAccess() { $container = $this->getContainer('container1'); @@ -102,7 +137,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])); @@ -110,19 +145,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); } } } @@ -182,6 +221,20 @@ public function testCustomAclProvider() $this->assertEquals('foo', (string) $container->getAlias('security.acl.provider')); } + public function testRememberMeThrowExceptionsDefault() + { + $container = $this->getContainer('container1'); + $this->assertTrue($container->getDefinition('security.authentication.listener.rememberme.secure')->getArgument(5)); + } + + public function testRememberMeThrowExceptions() + { + $container = $this->getContainer('remember_me_options'); + $service = $container->getDefinition('security.authentication.listener.rememberme.main'); + $this->assertEquals('security.authentication.rememberme.services.persistent.main', $service->getArgument(1)); + $this->assertFalse($service->getArgument(5)); + } + protected function getContainer($file) { if (isset(self::$containerCache[$file])) { 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 bb79da3fb05a..b16a46ff0345 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php @@ -69,13 +69,23 @@ 'anonymous' => true, 'switch_user' => true, 'x509' => true, + 'remote_user' => true, 'logout' => true, + 'remember_me' => array('key' => 'TheKey'), + ), + 'host' => array( + 'pattern' => '/test', + 'host' => 'foo\\.example\\.org', + 'methods' => array('GET', 'POST'), + '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/php/remember_me_options.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/remember_me_options.php new file mode 100644 index 000000000000..93a30444139e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/remember_me_options.php @@ -0,0 +1,18 @@ +loadFromExtension('security', array( + 'providers' => array( + 'default' => array('id' => 'foo'), + ), + + 'firewalls' => array( + 'main' => array( + 'form_login' => true, + 'remember_me' => array( + 'key' => 'TheyKey', + 'catch_exceptions' => false, + 'token_provider' => 'token_provider_id', + ), + ), + ), +)); 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 cb452e931669..1a56aa88fda0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml @@ -54,7 +54,14 @@ + + + + + + + ROLE_USER @@ -63,5 +70,6 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml new file mode 100644 index 000000000000..167475689157 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + 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 169f7fa43126..93c231ea235f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml @@ -52,7 +52,16 @@ security: anonymous: true switch_user: true x509: true + remote_user: true logout: true + remember_me: + key: TheKey + host: + pattern: /test + host: foo\.example\.org + methods: [GET,POST] + anonymous: true + http_basic: true role_hierarchy: ROLE_ADMIN: ROLE_USER @@ -64,3 +73,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/Fixtures/yml/remember_me_options.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/remember_me_options.yml new file mode 100644 index 000000000000..3a38b33c521c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/remember_me_options.yml @@ -0,0 +1,12 @@ +security: + providers: + default: + id: foo + + firewalls: + main: + form_login: true + remember_me: + key: TheKey + catch_exceptions: false + token_provider: token_provider_id diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index 047821cfdb37..8d9224673cd7 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/Security/Factory/AbstractFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php index e27811fea03f..39daf998cb4e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php @@ -18,11 +18,13 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase { public function testCreate() { - list($container, - $authProviderId, - $listenerId, - $entryPointId - ) = $this->callFactory('foo', array('use_forward' => true, 'failure_path' => '/foo', 'success_handler' => 'qux', 'failure_handler' => 'bar', 'remember_me' => true), 'user_provider', 'entry_point'); + list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', array( + 'use_forward' => true, + 'failure_path' => '/foo', + 'success_handler' => 'custom_success_handler', + 'failure_handler' => 'custom_failure_handler', + 'remember_me' => true, + ), 'user_provider', 'entry_point'); // auth provider $this->assertEquals('auth_provider', $authProviderId); @@ -33,8 +35,8 @@ public function testCreate() $definition = $container->getDefinition('abstract_listener.foo'); $this->assertEquals(array( 'index_4' => 'foo', - 'index_5' => new Reference('qux'), - 'index_6' => new Reference('bar'), + 'index_5' => new Reference('security.authentication.success_handler.foo.abstract_factory'), + 'index_6' => new Reference('security.authentication.failure_handler.foo.abstract_factory'), 'index_7' => array( 'use_forward' => true, ), @@ -44,30 +46,82 @@ public function testCreate() $this->assertEquals('entry_point', $entryPointId, '->create() does not change the default entry point.'); } - public function testDefaultFailureHandler() + /** + * @dataProvider getFailureHandlers + */ + public function testDefaultFailureHandler($serviceId, $defaultHandlerInjection) { - list($container, - $authProviderId, - $listenerId, - $entryPointId - ) = $this->callFactory('foo', array('remember_me' => true), 'user_provider', 'entry_point'); + $options = array( + 'remember_me' => true, + 'login_path' => '/bar', + ); + + if ($serviceId) { + $options['failure_handler'] = $serviceId; + } + + list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); $definition = $container->getDefinition('abstract_listener.foo'); $arguments = $definition->getArguments(); $this->assertEquals(new Reference('security.authentication.failure_handler.foo.abstract_factory'), $arguments['index_6']); + $failureHandler = $container->findDefinition((string) $arguments['index_6']); + + $methodCalls = $failureHandler->getMethodCalls(); + if ($defaultHandlerInjection) { + $this->assertEquals('setOptions', $methodCalls[0][0]); + $this->assertEquals(array('login_path' => '/bar'), $methodCalls[0][1][0]); + } else { + $this->assertCount(0, $methodCalls); + } + } + + public function getFailureHandlers() + { + return array( + array(null, true), + array('custom_failure_handler', false), + ); } - public function testDefaultSuccessHandler() + /** + * @dataProvider getSuccessHandlers + */ + public function testDefaultSuccessHandler($serviceId, $defaultHandlerInjection) { - list($container, - $authProviderId, - $listenerId, - $entryPointId - ) = $this->callFactory('foo', array('remember_me' => true), 'user_provider', 'entry_point'); + $options = array( + 'remember_me' => true, + 'default_target_path' => '/bar', + ); + + if ($serviceId) { + $options['success_handler'] = $serviceId; + } + + list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); $definition = $container->getDefinition('abstract_listener.foo'); $arguments = $definition->getArguments(); $this->assertEquals(new Reference('security.authentication.success_handler.foo.abstract_factory'), $arguments['index_5']); + $successHandler = $container->findDefinition((string) $arguments['index_5']); + $methodCalls = $successHandler->getMethodCalls(); + + if ($defaultHandlerInjection) { + $this->assertEquals('setOptions', $methodCalls[0][0]); + $this->assertEquals(array('default_target_path' => '/bar'), $methodCalls[0][1][0]); + $this->assertEquals('setProviderKey', $methodCalls[1][0]); + $this->assertEquals(array('foo'), $methodCalls[1][1]); + } else { + $this->assertCount(0, $methodCalls); + } + } + + public function getSuccessHandlers() + { + return array( + array(null, true), + array('custom_success_handler', false), + ); } protected function callFactory($id, $config, $userProviderId, $defaultEntryPointId) @@ -92,11 +146,10 @@ protected function callFactory($id, $config, $userProviderId, $defaultEntryPoint $container = new ContainerBuilder(); $container->register('auth_provider'); + $container->register('custom_success_handler'); + $container->register('custom_failure_handler'); - list($authProviderId, - $listenerId, - $entryPointId - ) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); + list($authProviderId, $listenerId, $entryPointId) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); return array($container, $authProviderId, $listenerId, $entryPointId); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 52db39200129..4dd33020c572 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -111,9 +111,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/AclBundle/AclBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/AclBundle.php new file mode 100644 index 000000000000..1208003bcc2c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/AclBundle.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\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * @author Kévin Dunglas + */ +class AclBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/Entity/Car.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/Entity/Car.php new file mode 100644 index 000000000000..c85a589578ec --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AclBundle/Entity/Car.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\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity; + +/** + * Car. + * + * @author Kévin Dunglas + */ +class Car +{ + public $id; +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php index 11753cb1267d..d76d8fd629bb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Form/UserLoginFormType.php @@ -17,8 +17,8 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvent; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\SecurityContextInterface; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Security\Core\Security; /** * Form type for use with the Security component's form-based authentication @@ -58,10 +58,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) * session for an authentication error and last username. */ $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($request) { - if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) { - $error = $request->attributes->get(SecurityContextInterface::AUTHENTICATION_ERROR); + if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) { + $error = $request->attributes->get(Security::AUTHENTICATION_ERROR); } else { - $error = $request->getSession()->get(SecurityContextInterface::AUTHENTICATION_ERROR); + $error = $request->getSession()->get(Security::AUTHENTICATION_ERROR); } if ($error) { @@ -69,7 +69,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) } $event->setData(array_replace((array) $event->getData(), array( - 'username' => $request->getSession()->get(SecurityContextInterface::LAST_USERNAME), + 'username' => $request->getSession()->get(Security::LAST_USERNAME), ))); }); } @@ -77,7 +77,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) /** * {@inheritdoc} */ - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function configureOptions(OptionsResolver $resolver) { /* Note: the form's intention must correspond to that for the form login * listener in order for the CSRF token to validate successfully. diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php index 9a33781a7ed3..b9004d26a3e6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php @@ -11,24 +11,25 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller; -use Symfony\Component\Security\Core\SecurityContext; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\DependencyInjection\ContainerAware; class LocalizedController extends ContainerAware { - public function loginAction() + public function loginAction(Request $request) { // get the login error if there is one - if ($this->container->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { - $error = $this->container->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR); + if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) { + $error = $request->attributes->get(Security::AUTHENTICATION_ERROR); } else { - $error = $this->container->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR); + $error = $request->getSession()->get(Security::AUTHENTICATION_ERROR); } return $this->container->get('templating')->renderResponse('FormLoginBundle:Localized:login.html.twig', array( // last username entered by the user - 'last_username' => $this->container->get('request')->getSession()->get(SecurityContext::LAST_USERNAME), + 'last_username' => $request->getSession()->get(Security::LAST_USERNAME), 'error' => $error, )); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php index eabb5683e667..3d0794a9e112 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php @@ -12,24 +12,25 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\SecurityContext; +use Symfony\Component\Security\Core\Security; use Symfony\Component\DependencyInjection\ContainerAware; class LoginController extends ContainerAware { - public function loginAction() + public function loginAction(Request $request) { // get the login error if there is one - if ($this->container->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { - $error = $this->container->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR); + if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) { + $error = $request->attributes->get(Security::AUTHENTICATION_ERROR); } else { - $error = $this->container->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR); + $error = $request->getSession()->get(Security::AUTHENTICATION_ERROR); } return $this->container->get('templating')->renderResponse('FormLoginBundle:Login:login.html.twig', array( // last username entered by the user - 'last_username' => $this->container->get('request')->getSession()->get(SecurityContext::LAST_USERNAME), + 'last_username' => $request->getSession()->get(Security::LAST_USERNAME), 'error' => $error, )); } 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 535df3576c2f..6992f80a0a12 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/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig index 9972b48ae730..3ef1f9c7bd18 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig @@ -3,4 +3,14 @@ {% block body %} Hello {{ app.user.username }}!

You're browsing to path "{{ app.request.pathInfo }}". + + Log out. + Log out. + + Log out. + Log out. + + Log out. + Log out. + {% endblock %} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php index 520eee648998..2f19f3f8a1a9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php @@ -32,6 +32,39 @@ public function testFormLogin($config) $this->assertContains('You\'re browsing to path "/profile".', $text); } + /** + * @dataProvider getConfigs + */ + public function testFormLogout($config) + { + $client = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config)); + + $form = $client->request('GET', '/login')->selectButton('login')->form(); + $form['_username'] = 'johannes'; + $form['_password'] = 'test'; + $client->submit($form); + + $this->assertRedirect($client->getResponse(), '/profile'); + + $crawler = $client->followRedirect(); + $text = $crawler->text(); + + $this->assertContains('Hello johannes!', $text); + $this->assertContains('You\'re browsing to path "/profile".', $text); + + $logoutLinks = $crawler->selectLink('Log out')->links(); + $this->assertCount(6, $logoutLinks); + $this->assertSame($logoutLinks[0]->getUri(), $logoutLinks[1]->getUri()); + $this->assertSame($logoutLinks[2]->getUri(), $logoutLinks[3]->getUri()); + $this->assertSame($logoutLinks[4]->getUri(), $logoutLinks[5]->getUri()); + + $this->assertNotSame($logoutLinks[0]->getUri(), $logoutLinks[2]->getUri()); + $this->assertNotSame($logoutLinks[1]->getUri(), $logoutLinks[3]->getUri()); + + $this->assertSame($logoutLinks[0]->getUri(), $logoutLinks[4]->getUri()); + $this->assertSame($logoutLinks[1]->getUri(), $logoutLinks[5]->getUri()); + } + /** * @dataProvider getConfigs */ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php index 52a31f539786..c7db437819a1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php @@ -29,10 +29,6 @@ public function testRoutingErrorIsNotExposedForProtectedResourceWhenAnonymous($c */ public function testRoutingErrorIsExposedWhenNotProtected($config) { - if ('\\' === DIRECTORY_SEPARATOR && PHP_VERSION_ID < 50309) { - $this->markTestSkipped('Test hangs on Windows & PHP due to https://bugs.php.net/bug.php?id=60120 fixed in http://svn.php.net/viewvc?view=revision&revision=318366'); - } - $client = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config)); $client->request('GET', '/unprotected_resource'); @@ -44,10 +40,6 @@ public function testRoutingErrorIsExposedWhenNotProtected($config) */ public function testRoutingErrorIsNotExposedForProtectedResourceWhenLoggedInWithInsufficientRights($config) { - if ('\\' === DIRECTORY_SEPARATOR && PHP_VERSION_ID < 50309) { - $this->markTestSkipped('Test hangs on Windows & PHP due to https://bugs.php.net/bug.php?id=60120 fixed in http://svn.php.net/viewvc?view=revision&revision=318366'); - } - $client = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config)); $form = $client->request('GET', '/login')->selectButton('login')->form(); @@ -86,6 +78,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/SetAclCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SetAclCommandTest.php new file mode 100644 index 000000000000..db4c51c5f064 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SetAclCommandTest.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\Bundle\SecurityBundle\Tests\Functional; + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\SecurityBundle\Command\InitAclCommand; +use Symfony\Bundle\SecurityBundle\Command\SetAclCommand; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Security\Acl\Domain\ObjectIdentity; +use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity; +use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity; +use Symfony\Component\Security\Acl\Exception\NoAceFoundException; +use Symfony\Component\Security\Acl\Permission\BasicPermissionMap; + +/** + * Tests SetAclCommand. + * + * @author Kévin Dunglas + * @requires extension pdo_sqlite + */ +class SetAclCommandTest extends WebTestCase +{ + const OBJECT_CLASS = 'Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\Entity\Car'; + const SECURITY_CLASS = 'Symfony\Component\Security\Core\User\User'; + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('Acl'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('Acl'); + } + + public function testSetAclUser() + { + $objectId = 1; + $securityUsername1 = 'kevin'; + $securityUsername2 = 'anne'; + $grantedPermission1 = 'VIEW'; + $grantedPermission2 = 'EDIT'; + + $application = $this->getApplication(); + $application->add(new SetAclCommand()); + + $setAclCommand = $application->find('acl:set'); + $setAclCommandTester = new CommandTester($setAclCommand); + $setAclCommandTester->execute(array( + 'command' => 'acl:set', + 'arguments' => array($grantedPermission1, $grantedPermission2, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)), + '--user' => array(sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername1), sprintf('%s:%s', self::SECURITY_CLASS, $securityUsername2)), + )); + + $objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS); + $securityIdentity1 = new UserSecurityIdentity($securityUsername1, self::SECURITY_CLASS); + $securityIdentity2 = new UserSecurityIdentity($securityUsername2, self::SECURITY_CLASS); + $permissionMap = new BasicPermissionMap(); + + /** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */ + $aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider'); + $acl = $aclProvider->findAcl($objectIdentity, array($securityIdentity1)); + + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity1))); + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission1, null), array($securityIdentity2))); + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission2, null), array($securityIdentity2))); + + try { + $acl->isGranted($permissionMap->getMasks('OWNER', null), array($securityIdentity1)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + + try { + $acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($securityIdentity2)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + } + + public function testSetAclRole() + { + $objectId = 1; + $securityUsername = 'kevin'; + $grantedPermission = 'VIEW'; + $role = 'ROLE_ADMIN'; + + $application = $this->getApplication(); + $application->add(new SetAclCommand()); + + $setAclCommand = $application->find('acl:set'); + $setAclCommandTester = new CommandTester($setAclCommand); + $setAclCommandTester->execute(array( + 'command' => 'acl:set', + 'arguments' => array($grantedPermission, sprintf('%s:%s', str_replace('\\', '/', self::OBJECT_CLASS), $objectId)), + '--role' => array($role), + )); + + $objectIdentity = new ObjectIdentity($objectId, self::OBJECT_CLASS); + $userSecurityIdentity = new UserSecurityIdentity($securityUsername, self::SECURITY_CLASS); + $roleSecurityIdentity = new RoleSecurityIdentity($role); + $permissionMap = new BasicPermissionMap(); + + /** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */ + $aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider'); + $acl = $aclProvider->findAcl($objectIdentity, array($roleSecurityIdentity, $userSecurityIdentity)); + + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + $this->assertTrue($acl->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + + try { + $acl->isGranted($permissionMap->getMasks('VIEW', null), array($userSecurityIdentity)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + + try { + $acl->isGranted($permissionMap->getMasks('OPERATOR', null), array($userSecurityIdentity)); + $this->fail('NoAceFoundException not throwed'); + } catch (NoAceFoundException $e) { + } + } + + public function testSetAclClassScope() + { + $objectId = 1; + $grantedPermission = 'VIEW'; + $role = 'ROLE_USER'; + + $application = $this->getApplication(); + $application->add(new SetAclCommand()); + + $setAclCommand = $application->find('acl:set'); + $setAclCommandTester = new CommandTester($setAclCommand); + $setAclCommandTester->execute(array( + 'command' => 'acl:set', + 'arguments' => array($grantedPermission, sprintf('%s:%s', self::OBJECT_CLASS, $objectId)), + '--class-scope' => true, + '--role' => array($role), + )); + + $objectIdentity1 = new ObjectIdentity($objectId, self::OBJECT_CLASS); + $objectIdentity2 = new ObjectIdentity(2, self::OBJECT_CLASS); + $roleSecurityIdentity = new RoleSecurityIdentity($role); + $permissionMap = new BasicPermissionMap(); + + /** @var \Symfony\Component\Security\Acl\Model\AclProviderInterface $aclProvider */ + $aclProvider = $application->getKernel()->getContainer()->get('security.acl.provider'); + + $acl1 = $aclProvider->findAcl($objectIdentity1, array($roleSecurityIdentity)); + $this->assertTrue($acl1->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + + $acl2 = $aclProvider->createAcl($objectIdentity2); + $this->assertTrue($acl2->isGranted($permissionMap->getMasks($grantedPermission, null), array($roleSecurityIdentity))); + } + + private function getApplication() + { + $kernel = $this->createKernel(array('test_case' => 'Acl')); + $kernel->boot(); + + $application = new Application($kernel); + $application->add(new InitAclCommand()); + + $initAclCommand = $application->find('init:acl'); + $initAclCommandTester = new CommandTester($initAclCommand); + $initAclCommandTester->execute(array('command' => 'init:acl')); + + return $application; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php new file mode 100644 index 000000000000..86a69fdb7624 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; + +/** + * Tests UserPasswordEncoderCommand. + * + * @author Sarah Khalil + */ +class UserPasswordEncoderCommandTest extends WebTestCase +{ + private $passwordEncoderCommandTester; + + public function testEncodePasswordEmptySalt() + { + $this->passwordEncoderCommandTester->execute(array( + 'command' => 'security:encode-password', + 'password' => 'password', + 'user-class' => 'Symfony\Component\Security\Core\User\User', + '--empty-salt' => true, + ), array('decorated' => false)); + $expected = str_replace("\n", PHP_EOL, file_get_contents(__DIR__.'/app/PasswordEncode/emptysalt.txt')); + + $this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); + } + + public function testEncodeNoPasswordNoInteraction() + { + $statusCode = $this->passwordEncoderCommandTester->execute(array( + 'command' => 'security:encode-password', + ), array('interactive' => false)); + + $this->assertContains('[ERROR] The password must not be empty.', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertEquals($statusCode, 1); + } + + public function testEncodePasswordBcrypt() + { + $this->passwordEncoderCommandTester->execute(array( + 'command' => 'security:encode-password', + 'password' => 'password', + 'user-class' => 'Custom\Class\Bcrypt\User', + ), array('interactive' => false)); + + $output = $this->passwordEncoderCommandTester->getDisplay(); + $this->assertContains('Password encoding succeeded', $output); + + $encoder = new BCryptPasswordEncoder(17); + preg_match('# Encoded password\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches); + $hash = $matches[1]; + $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); + } + + public function testEncodePasswordPbkdf2() + { + $this->passwordEncoderCommandTester->execute(array( + 'command' => 'security:encode-password', + 'password' => 'password', + 'user-class' => 'Custom\Class\Pbkdf2\User', + ), array('interactive' => false)); + + $output = $this->passwordEncoderCommandTester->getDisplay(); + $this->assertContains('Password encoding succeeded', $output); + + $encoder = new Pbkdf2PasswordEncoder('sha512', true, 1000); + preg_match('# Encoded password\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); + $hash = $matches[1]; + preg_match('# Generated salt\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); + $salt = $matches[1]; + $this->assertTrue($encoder->isPasswordValid($hash, 'password', $salt)); + } + + public function testEncodePasswordOutput() + { + $this->passwordEncoderCommandTester->execute( + array( + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + ), array('interactive' => false) + ); + + $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + } + + public function testEncodePasswordEmptySaltOutput() + { + $this->passwordEncoderCommandTester->execute( + array( + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + '--empty-salt' => true, + ) + ); + + $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + } + + public function testEncodePasswordBcryptOutput() + { + $this->passwordEncoderCommandTester->execute( + array( + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + 'user-class' => 'Custom\Class\Bcrypt\User', + ) + ); + + $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + } + + public function testEncodePasswordNoConfigForGivenUserClass() + { + $this->setExpectedException('\RuntimeException', 'No encoder has been configured for account "Foo\Bar\User".'); + + $this->passwordEncoderCommandTester->execute(array( + 'command' => 'security:encode-password', + 'password' => 'password', + 'user-class' => 'Foo\Bar\User', + ), array('interactive' => false)); + } + + protected function setUp() + { + $kernel = $this->createKernel(array('test_case' => 'PasswordEncode')); + $kernel->boot(); + + $application = new Application($kernel); + + $application->add(new UserPasswordEncoderCommand()); + $passwordEncoderCommand = $application->find('security:encode-password'); + + $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); + } + + protected function tearDown() + { + $this->passwordEncoderCommandTester = null; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/bundles.php new file mode 100644 index 000000000000..51337913d537 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/bundles.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return array( + new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AclBundle\AclBundle(), +); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/config.yml new file mode 100644 index 000000000000..33eadbc7cdf0 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/config.yml @@ -0,0 +1,24 @@ +imports: + - { resource: ./../config/framework.yml } + +doctrine: + dbal: + driver: pdo_sqlite + memory: true + charset: UTF8 + +security: + firewalls: + test: + pattern: ^/ + security: false + acl: + connection: default + encoders: + Symfony\Component\Security\Core\User\User: plaintext + providers: + in_memory: + memory: + users: + kevin: { password: test, roles: [ROLE_USER] } + anne: { password: test, roles: [ROLE_ADMIN]} 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 d77013a2197b..acbca59fa1bb 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/PasswordEncode/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php new file mode 100644 index 000000000000..2e9243712c8d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return array( + new Symfony\Bundle\SecurityBundle\SecurityBundle(), + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), +); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml new file mode 100644 index 000000000000..82416b095748 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml @@ -0,0 +1,27 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + Custom\Class\Bcrypt\User: + algorithm: bcrypt + cost: 10 + Custom\Class\Pbkdf2\User: + algorithm: pbkdf2 + hash_algorithm: sha512 + encode_as_base64: true + iterations: 1000 + Custom\Class\Test\User: test + + providers: + in_memory: + memory: + users: + user: { password: userpass, roles: [ 'ROLE_USER' ] } + admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } + + firewalls: + test: + pattern: ^/ + security: false diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt new file mode 100644 index 000000000000..9c8d3deb1b46 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt @@ -0,0 +1,13 @@ + +Symfony Password Encoder Utility +================================ + + ------------------ ------------------------------------------------------------------ + Key Value + ------------------ ------------------------------------------------------------------ + Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder + Encoded password password + ------------------ ------------------------------------------------------------------ + + [OK] Password encoding succeeded + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Resources/views/base.html.twig b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Resources/views/base.html.twig index 58ba1fe89e46..caf6f6efb6db 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Resources/views/base.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Resources/views/base.html.twig @@ -4,7 +4,6 @@ {% block title %}Welcome!{% endblock %} {% block stylesheets %}{% endblock %} - {% block body %}{% endblock %} 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 58bd9f2d8af3..19b9d8952ec5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml @@ -23,12 +23,22 @@ security: form_login: check_path: /login_check default_target_path: /profile + logout: ~ anonymous: ~ + # This firewall is here just to check its the logout functionality + second_area: + http_basic: ~ + anonymous: ~ + logout: + target: /second/target + path: /second/logout + access_control: - { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { 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/Twig/Extension/LogoutUrlExtension.php b/src/Symfony/Bundle/SecurityBundle/Twig/Extension/LogoutUrlExtension.php index 8d28b3f246f7..d1eae0ef2048 100644 --- a/src/Symfony/Bundle/SecurityBundle/Twig/Extension/LogoutUrlExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/Twig/Extension/LogoutUrlExtension.php @@ -11,12 +11,16 @@ namespace Symfony\Bundle\SecurityBundle\Twig\Extension; +@trigger_error('The '.__NAMESPACE__.'\LogoutUrlExtension class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bridge\Twig\Extension\LogoutUrlExtension instead.', E_USER_DEPRECATED); + use Symfony\Bundle\SecurityBundle\Templating\Helper\LogoutUrlHelper; /** * LogoutUrlHelper provides generator functions for the logout URL to Twig. * * @author Jeremy Mikola + * + * @deprecated since version 2.7, to be removed in 3.0. Use Symfony\Bridge\Twig\Extension\LogoutUrlExtension instead. */ class LogoutUrlExtension extends \Twig_Extension { @@ -41,11 +45,11 @@ public function getFunctions() /** * Generates the relative logout URL for the firewall. * - * @param string $key The firewall key + * @param string|null $key The firewall key or null to use the current firewall key * * @return string The relative logout URL */ - public function getLogoutPath($key) + public function getLogoutPath($key = null) { return $this->helper->getLogoutPath($key); } @@ -53,11 +57,11 @@ public function getLogoutPath($key) /** * Generates the absolute logout URL for the firewall. * - * @param string $key The firewall key + * @param string|null $key The firewall key or null to use the current firewall key * * @return string The absolute logout URL */ - public function getLogoutUrl($key) + public function getLogoutUrl($key = null) { return $this->helper->getLogoutUrl($key); } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 2c18a26bc54c..f00e09ca3c90 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -16,37 +16,40 @@ } ], "require": { - "php": ">=5.3.3", - "symfony/security": "~2.3.37|~2.6.13|~2.7.9|~2.8", - "symfony/security-acl": "~2.2", + "php": ">=5.3.9", + "symfony/security": "~2.7", + "symfony/security-acl": "~2.7", "symfony/http-kernel": "~2.2" }, "require-dev": { - "symfony/browser-kit": "~2.3", + "symfony/browser-kit": "~2.4", + "symfony/console": "~2.7", "symfony/css-selector": "~2.0,>=2.0.5", - "symfony/dependency-injection": "~2.3", + "symfony/dependency-injection": "~2.6,>=2.6.6", "symfony/dom-crawler": "~2.0,>=2.0.5", - "symfony/form": "~2.3", - "symfony/framework-bundle": "~2.2", + "symfony/form": "~2.7", + "symfony/framework-bundle": "~2.7", "symfony/http-foundation": "~2.3", - "symfony/twig-bundle": "~2.2", - "symfony/twig-bridge": "~2.2,>=2.2.6", + "symfony/twig-bundle": "~2.7", + "symfony/twig-bridge": "~2.7", "symfony/process": "~2.0,>=2.0.5", - "symfony/validator": "~2.2", + "symfony/validator": "~2.5", "symfony/yaml": "~2.0,>=2.0.5", - "twig/twig": "~1.23|~2.0" + "symfony/expression-language": "~2.6", + "doctrine/doctrine-bundle": "~1.2", + "twig/twig": "~1.23|~2.0", + "ircmaxell/password-compat": "~1.0" }, "autoload": { - "psr-0": { "Symfony\\Bundle\\SecurityBundle\\": "" }, + "psr-4": { "Symfony\\Bundle\\SecurityBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bundle/SecurityBundle", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index de36165b56c1..90f7cbd1e017 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -1,6 +1,18 @@ CHANGELOG ========= +2.7.0 +----- + + * made it possible to configure the default formats for both the `date` and the `number_format` filter + * added support for the new Asset component (from Twig bridge) + * deprecated the assets extension (use the one from the Twig bridge instead) + +2.6.0 +----- + + * [BC BREAK] changed exception.json.twig to match same structure as error.json.twig making clients independent of runtime environment. + 2.3.0 ----- diff --git a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php index 65827eba5a6b..8557a2f55aca 100644 --- a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php +++ b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php @@ -31,15 +31,16 @@ class TemplateCacheCacheWarmer implements CacheWarmerInterface /** * Constructor. * - * @param ContainerInterface $container The dependency injection container - * @param TemplateFinderInterface $finder The template paths cache warmer + * @param ContainerInterface $container The dependency injection container + * @param TemplateFinderInterface|null $finder The template paths cache warmer */ - public function __construct(ContainerInterface $container, TemplateFinderInterface $finder) + public function __construct(ContainerInterface $container, TemplateFinderInterface $finder = null) { // We don't inject the Twig environment directly as it depends on the // template locator (via the loader) which might be a cached one. // The cached template locator is available once the TemplatePathsCacheWarmer - // has been warmed up + // has been warmed up. + // But it can also be null if templating has been disabled. $this->container = $container; $this->finder = $finder; } @@ -51,6 +52,10 @@ public function __construct(ContainerInterface $container, TemplateFinderInterfa */ public function warmUp($cacheDir) { + if (null === $this->finder) { + return; + } + $twig = $this->container->get('twig'); foreach ($this->finder->findAllTemplates() as $template) { diff --git a/src/Symfony/Bundle/TwigBundle/Command/DebugCommand.php b/src/Symfony/Bundle/TwigBundle/Command/DebugCommand.php new file mode 100644 index 000000000000..c4aa34098b62 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Command/DebugCommand.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\Bundle\TwigBundle\Command; + +use Symfony\Bridge\Twig\Command\DebugCommand as BaseDebugCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; + +/** + * Lists twig functions, filters, globals and tests present in the current project. + * + * @author Jordi Boggiano + */ +class DebugCommand extends BaseDebugCommand implements ContainerAwareInterface +{ + /** + * @var ContainerInterface|null + */ + private $container; + + /** + * {@inheritdoc} + */ + public function setContainer(ContainerInterface $container = null) + { + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + protected function getTwigEnvironment() + { + return $this->container->get('twig'); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + parent::configure(); + + $this->setAliases(array('twig:debug')); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (false !== strpos($input->getFirstArgument(), ':d')) { + $output->writeln('The use of "twig:debug" command is deprecated since version 2.7 and will be removed in 3.0. Use the "debug:twig" instead.'); + } + + parent::execute($input, $output); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php index 997f29fefacc..e902ab88f953 100644 --- a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php +++ b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php @@ -11,140 +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(<<<'EOF' -The %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)); - } - - 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().<<<'EOF' - protected function renderException(OutputInterface $output, $template, \Twig_Error $exception, $file = null) - { - $line = $exception->getTemplateLine(); - $lines = $this->getContext($template, $line); +Or all template files in a bundle: - if ($file) { - $output->writeln(sprintf('KO in %s (line %s)', $file, $line)); - } else { - $output->writeln(sprintf('KO (line %s)', $line)); - } + php %command.full_name% @AcmeDemoBundle - foreach ($lines as $no => $code) { - $output->writeln(sprintf( - '%s %-6s %s', - $no == $line ? '>>' : ' ', - $no, - $code - )); - if ($no == $line) { - $output->writeln(sprintf('>> %s ', $exception->getRawMessage())); - } - } +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/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index 9f1edad12c8d..b0c172c1e9e1 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -19,13 +19,19 @@ use Symfony\Component\Templating\TemplateReferenceInterface; /** - * ExceptionController. + * ExceptionController renders error or exception pages for a given + * FlattenException. * * @author Fabien Potencier + * @author Matthias Pigulla */ class ExceptionController { protected $twig; + + /** + * @var bool Show error (false) or exception (true) pages by default. + */ protected $debug; public function __construct(\Twig_Environment $twig, $debug) @@ -37,6 +43,10 @@ public function __construct(\Twig_Environment $twig, $debug) /** * Converts an Exception to a Response. * + * A "showException" request parameter can be used to force display of an error page (when set to false) or + * the exception page (when true). If it is not present, the "debug" value passed into the constructor will + * be used. + * * @param Request $request The request * @param FlattenException $exception A FlattenException instance * @param DebugLoggerInterface $logger A DebugLoggerInterface instance @@ -48,11 +58,12 @@ public function __construct(\Twig_Environment $twig, $debug) public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null) { $currentContent = $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level', -1)); + $showException = $request->attributes->get('showException', $this->debug); // As opposed to an additional parameter, this maintains BC $code = $exception->getStatusCode(); return new Response($this->twig->render( - (string) $this->findTemplate($request, $request->getRequestFormat(), $code, $this->debug), + (string) $this->findTemplate($request, $request->getRequestFormat(), $code, $showException), array( 'status_code' => $code, 'status_text' => isset(Response::$statusTexts[$code]) ? Response::$statusTexts[$code] : '', @@ -70,36 +81,32 @@ public function showAction(Request $request, FlattenException $exception, DebugL */ protected function getAndCleanOutputBuffering($startObLevel) { - // ob_get_level() never returns 0 on some Windows configurations, so if - // the level is the same two times in a row, the loop should be stopped. - $previousObLevel = null; - $currentContent = ''; - - while (($obLevel = ob_get_level()) > $startObLevel && $obLevel !== $previousObLevel) { - $previousObLevel = $obLevel; - $currentContent .= ob_get_clean(); + if (ob_get_level() <= $startObLevel) { + return ''; } - return $currentContent; + Response::closeOutputBuffers($startObLevel + 1, true); + + return ob_get_clean(); } /** * @param Request $request * @param string $format - * @param int $code An HTTP response status code - * @param bool $debug + * @param int $code An HTTP response status code + * @param bool $showException * * @return TemplateReferenceInterface */ - protected function findTemplate(Request $request, $format, $code, $debug) + protected function findTemplate(Request $request, $format, $code, $showException) { - $name = $debug ? 'exception' : 'error'; - if ($debug && 'html' == $format) { + $name = $showException ? 'exception' : 'error'; + if ($showException && 'html' == $format) { $name = 'exception_full'; } - // when not in debug, try to find a template for the specific HTTP status code and format - if (!$debug) { + // For error pages, try to find a template for the specific HTTP status code and format + if (!$showException) { $template = new TemplateReference('TwigBundle', 'Exception', $name.$code, $format, 'twig'); if ($this->templateExists($template)) { return $template; @@ -115,12 +122,14 @@ protected function findTemplate(Request $request, $format, $code, $debug) // default to a generic HTML exception $request->setRequestFormat('html'); - return new TemplateReference('TwigBundle', 'Exception', $debug ? 'exception_full' : $name, 'html', 'twig'); + return new TemplateReference('TwigBundle', 'Exception', $showException ? 'exception_full' : $name, 'html', 'twig'); } - // to be removed when the minimum required version of Twig is >= 2.0 + // to be removed when the minimum required version of Twig is >= 3.0 protected function templateExists($template) { + $template = (string) $template; + $loader = $this->twig->getLoader(); if ($loader instanceof \Twig_ExistsLoaderInterface) { return $loader->exists($template); diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php new file mode 100644 index 000000000000..a907ef0abe5b --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.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\Bundle\TwigBundle\Controller; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * PreviewErrorController can be used to test error pages. + * + * It will create a test exception and forward it to another controller. + * + * @author Matthias Pigulla + */ +class PreviewErrorController +{ + protected $kernel; + protected $controller; + + public function __construct(HttpKernelInterface $kernel, $controller) + { + $this->kernel = $kernel; + $this->controller = $controller; + } + + public function previewErrorPageAction(Request $request, $code) + { + $exception = FlattenException::create(new \Exception('Something has intentionally gone wrong.'), $code); + + /* + * This Request mimics the parameters set by + * \Symfony\Component\HttpKernel\EventListener\ExceptionListener::duplicateRequest, with + * the additional "showException" flag. + */ + + $subRequest = $request->duplicate(null, null, array( + '_controller' => $this->controller, + 'exception' => $exception, + 'logger' => null, + 'format' => $request->getRequestFormat(), + 'showException' => false, + )); + + return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Debug/TimedTwigEngine.php b/src/Symfony/Bundle/TwigBundle/Debug/TimedTwigEngine.php index 1a73eee9bb15..e09479a2845f 100644 --- a/src/Symfony/Bundle/TwigBundle/Debug/TimedTwigEngine.php +++ b/src/Symfony/Bundle/TwigBundle/Debug/TimedTwigEngine.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\TwigBundle\Debug; +@trigger_error('The '.__NAMESPACE__.'\TimedTwigEngine class is deprecated since version 2.7 and will be removed in 3.0. Use the Twig native profiler instead.', E_USER_DEPRECATED); + use Symfony\Bundle\TwigBundle\TwigEngine; use Symfony\Component\Templating\TemplateNameParserInterface; use Symfony\Component\Stopwatch\Stopwatch; @@ -20,6 +22,8 @@ * Times the time spent to render a template. * * @author Fabien Potencier + * + * @deprecated since version 2.7, to be removed in 3.0. Use the Twig native profiler instead. */ class TimedTwigEngine extends TwigEngine { diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php index 18d98b356dfe..9fbfd65950a5 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php @@ -28,9 +28,11 @@ public function process(ContainerBuilder $container) } // register the exception controller only if Twig is enabled - $engines = $container->getParameter('templating.engines'); - if (!in_array('twig', $engines)) { - $container->removeDefinition('twig.exception_listener'); + if ($container->hasParameter('templating.engines')) { + $engines = $container->getParameter('templating.engines'); + if (!in_array('twig', $engines)) { + $container->removeDefinition('twig.exception_listener'); + } } } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 069083d27f0f..87469984688a 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -13,6 +13,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; /** * @author Jean-François Simon @@ -27,6 +28,10 @@ public function process(ContainerBuilder $container) $container->getDefinition('twig.loader.filesystem')->addMethodCall('addPath', array(dirname(dirname($reflClass->getFileName())).'/Resources/views/Form')); } + if ($container->has('fragment.handler')) { + $container->getDefinition('twig.extension.actions')->addTag('twig.extension'); + } + if ($container->has('translator')) { $container->getDefinition('twig.extension.trans')->addTag('twig.extension'); } @@ -37,6 +42,47 @@ public function process(ContainerBuilder $container) if ($container->has('fragment.handler')) { $container->getDefinition('twig.extension.httpkernel')->addTag('twig.extension'); + + // inject Twig in the hinclude service if Twig is the only registered templating engine + if ( + !$container->hasParameter('templating.engines') + || array('twig') == $container->getParameter('templating.engines') + ) { + $container->getDefinition('fragment.renderer.hinclude') + ->addTag('kernel.fragment_renderer', array('alias' => 'hinclude')) + ->replaceArgument(0, new Reference('twig')) + ; + } + } + + if ($container->has('request_stack')) { + $container->getDefinition('twig.extension.httpfoundation')->addTag('twig.extension'); + } + + if ($container->hasParameter('templating.helper.code.file_link_format')) { + $container->getDefinition('twig.extension.code')->replaceArgument(0, $container->getParameter('templating.helper.code.file_link_format')); + } + + if ($container->getParameter('kernel.debug')) { + $container->getDefinition('twig.extension.profiler')->addTag('twig.extension'); + $container->getDefinition('twig.extension.debug')->addTag('twig.extension'); + } + + if (!$container->has('templating')) { + $loader = $container->getDefinition('twig.loader.native_filesystem'); + $loader->addTag('twig.loader'); + $loader->setMethodCalls($container->getDefinition('twig.loader.filesystem')->getMethodCalls()); + + $container->setDefinition('twig.loader.filesystem', $loader); + } + + if ($container->has('assets.packages')) { + $container->getDefinition('twig.extension.assets')->addTag('twig.extension'); + } + + if (method_exists('Symfony\Bridge\Twig\AppVariable', 'setContainer')) { + // we are on Symfony <3.0, where the setContainer method exists + $container->getDefinition('twig.app_variable')->addMethodCall('setContainer', array(new Reference('service_container'))); } } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php index c34f95839326..bc3b71c696ed 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php @@ -40,9 +40,24 @@ public function process(ContainerBuilder $container) $container->setAlias('twig.loader', key($loaderIds)); } else { $chainLoader = $container->getDefinition('twig.loader.chain'); - foreach (array_keys($loaderIds) as $id) { - $chainLoader->addMethodCall('addLoader', array(new Reference($id))); + + $prioritizedLoaders = array(); + + foreach ($loaderIds as $id => $tags) { + foreach ($tags as $tag) { + $priority = isset($tag['priority']) ? $tag['priority'] : 0; + $prioritizedLoaders[$priority][] = $id; + } + } + + krsort($prioritizedLoaders); + + foreach ($prioritizedLoaders as $loaders) { + foreach ($loaders as $loader) { + $chainLoader->addMethodCall('addLoader', array(new Reference($loader))); + } } + $container->setAlias('twig.loader', 'twig.loader.chain'); } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index f4d3dd5ac9e9..52c70f33187d 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -39,8 +39,10 @@ public function getConfigTreeBuilder() ; $this->addFormSection($rootNode); + $this->addFormThemesSection($rootNode); $this->addGlobalsSection($rootNode); $this->addTwigOptions($rootNode); + $this->addTwigFormatOptions($rootNode); return $treeBuilder; } @@ -48,8 +50,29 @@ public function getConfigTreeBuilder() private function addFormSection(ArrayNodeDefinition $rootNode) { $rootNode + // Check deprecation before the config is processed to ensure + // the setting has been explicitly defined in a configuration file. + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['form']['resources']); }) + ->then(function ($v) { + @trigger_error('The twig.form.resources configuration key is deprecated since version 2.6 and will be removed in 3.0. Use the twig.form_themes configuration key instead.', E_USER_DEPRECATED); + + return $v; + }) + ->end() + ->validate() + ->ifTrue(function ($v) { + return count($v['form']['resources']) > 0; + }) + ->then(function ($v) { + $v['form_themes'] = array_values(array_unique(array_merge($v['form']['resources'], $v['form_themes']))); + + return $v; + }) + ->end() ->children() ->arrayNode('form') + ->info('Deprecated since version 2.6, to be removed in 3.0. Use twig.form_themes instead') ->addDefaultsIfNotSet() ->fixXmlConfig('resource') ->children() @@ -70,6 +93,26 @@ private function addFormSection(ArrayNodeDefinition $rootNode) ; } + private function addFormThemesSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->fixXmlConfig('form_theme') + ->children() + ->arrayNode('form_themes') + ->addDefaultChildrenIfNoneSet() + ->prototype('scalar')->defaultValue('form_div_layout.html.twig')->end() + ->example(array('MyBundle::form.html.twig')) + ->validate() + ->ifTrue(function ($v) { return !in_array('form_div_layout.html.twig', $v); }) + ->then(function ($v) { + return array_merge(array('form_div_layout.html.twig'), $v); + }) + ->end() + ->end() + ->end() + ; + } + private function addGlobalsSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -124,9 +167,7 @@ private function addTwigOptions(ArrayNodeDefinition $rootNode) $rootNode ->fixXmlConfig('path') ->children() - ->variableNode('autoescape') - ->defaultValue(array('Symfony\Bundle\TwigBundle\TwigDefaultEscapingStrategy', 'guess')) - ->end() + ->variableNode('autoescape')->defaultValue('filename')->end() ->scalarNode('autoescape_service')->defaultNull()->end() ->scalarNode('autoescape_service_method')->defaultNull()->end() ->scalarNode('base_template_class')->example('Twig_Template')->cannotBeEmpty()->end() @@ -167,4 +208,33 @@ private function addTwigOptions(ArrayNodeDefinition $rootNode) ->end() ; } + + private function addTwigFormatOptions(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('date') + ->info('The default format options used by the date filter') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('format')->defaultValue('F j, Y H:i')->end() + ->scalarNode('interval_format')->defaultValue('%d days')->end() + ->scalarNode('timezone') + ->info('The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used') + ->defaultNull() + ->end() + ->end() + ->end() + ->arrayNode('number_format') + ->info('The default format options for the number_format filter') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('decimals')->defaultValue(0)->end() + ->scalarNode('decimal_point')->defaultValue('.')->end() + ->scalarNode('thousands_separator')->defaultValue(',')->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php new file mode 100644 index 000000000000..21e9a1a25c61 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.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\Bundle\TwigBundle\DependencyInjection\Configurator; + +/** + * Twig environment configurator. + * + * @author Christian Flothmann + */ +class EnvironmentConfigurator +{ + private $dateFormat; + private $intervalFormat; + private $timezone; + private $decimals; + private $decimalPoint; + private $thousandsSeparator; + + public function __construct($dateFormat, $intervalFormat, $timezone, $decimals, $decimalPoint, $thousandsSeparator) + { + $this->dateFormat = $dateFormat; + $this->intervalFormat = $intervalFormat; + $this->timezone = $timezone; + $this->decimals = $decimals; + $this->decimalPoint = $decimalPoint; + $this->thousandsSeparator = $thousandsSeparator; + } + + public function configure(\Twig_Environment $environment) + { + $environment->getExtension('core')->setDateFormat($this->dateFormat, $this->intervalFormat); + + if (null !== $this->timezone) { + $environment->getExtension('core')->setTimezone($this->timezone); + } + + $environment->getExtension('core')->setNumberFormat($this->decimals, $this->decimalPoint, $this->thousandsSeparator); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index f29d8a87427f..7b97e120baa4 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -55,7 +55,15 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('twig.exception_listener.controller', $config['exception_controller']); - $container->setParameter('twig.form.resources', $config['form']['resources']); + $container->setParameter('twig.form.resources', $config['form_themes']); + + $envConfiguratorDefinition = $container->getDefinition('twig.configurator.environment'); + $envConfiguratorDefinition->replaceArgument(0, $config['date']['format']); + $envConfiguratorDefinition->replaceArgument(1, $config['date']['interval_format']); + $envConfiguratorDefinition->replaceArgument(2, $config['date']['timezone']); + $envConfiguratorDefinition->replaceArgument(3, $config['number_format']['decimals']); + $envConfiguratorDefinition->replaceArgument(4, $config['number_format']['decimal_point']); + $envConfiguratorDefinition->replaceArgument(5, $config['number_format']['thousands_separator']); $twigFilesystemLoaderDefinition = $container->getDefinition('twig.loader.filesystem'); @@ -101,19 +109,12 @@ public function load(array $configs, ContainerBuilder $container) $config['extensions'] ); - if ($container->getParameter('kernel.debug')) { - $loader->load('debug.xml'); - - $container->setDefinition('templating.engine.twig', $container->findDefinition('debug.templating.engine.twig')); - $container->setAlias('debug.templating.engine.twig', 'templating.engine.twig'); - } - if (isset($config['autoescape_service']) && isset($config['autoescape_service_method'])) { $config['autoescape'] = array(new Reference($config['autoescape_service']), $config['autoescape_service_method']); } unset($config['autoescape_service'], $config['autoescape_service_method']); - $container->setParameter('twig.options', $config); + $container->getDefinition('twig')->replaceArgument(1, $config); $this->addClassesToCompile(array( 'Twig_Environment', diff --git a/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php index 76f4dff5c3a2..07f956b07ef5 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php @@ -12,28 +12,38 @@ namespace Symfony\Bundle\TwigBundle\Extension; use Symfony\Bundle\TwigBundle\TokenParser\RenderTokenParser; -use Symfony\Bundle\FrameworkBundle\Templating\Helper\ActionsHelper; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Fragment\FragmentHandler; /** * Twig extension for Symfony actions helper. * * @author Fabien Potencier * - * @deprecated Deprecated in 2.2, to be removed in 3.0. + * @deprecated since version 2.2, to be removed in 3.0. */ class ActionsExtension extends \Twig_Extension { - private $container; + private $handler; /** - * Constructor. + * @param FragmentHandler|ContainerInterface $handler * - * @param ContainerInterface $container The service container + * @deprecated Passing a ContainerInterface as a first argument is deprecated since 2.7 and will be removed in 3.0. */ - public function __construct(ContainerInterface $container) + public function __construct($handler) { - $this->container = $container; + if ($handler instanceof FragmentHandler) { + $this->handler = $handler; + } elseif ($handler instanceof ContainerInterface) { + @trigger_error('The ability to pass a ContainerInterface instance as a first argument to '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0. Pass a FragmentHandler instance instead.', E_USER_DEPRECATED); + + $this->handler = $handler->get('fragment.handler'); + } else { + throw new \BadFunctionCallException(sprintf('%s takes a FragmentHandler or a ContainerInterface object as its first argument.', __METHOD__)); + } + + $this->handler = $handler; } /** @@ -42,11 +52,16 @@ public function __construct(ContainerInterface $container) * @param string $uri A URI * @param array $options An array of options * - * @see ActionsHelper::render() + * @see FragmentHandler::render() */ public function renderUri($uri, array $options = array()) { - return $this->container->get('templating.helper.actions')->render($uri, $options); + @trigger_error('The Twig render tag was deprecated in version 2.2 and will be removed in version 3.0. Use the Twig render function instead.', E_USER_DEPRECATED); + + $strategy = isset($options['strategy']) ? $options['strategy'] : 'inline'; + unset($options['strategy']); + + return $this->handler->render($uri, $strategy, $options); } /** diff --git a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php index 4f40c7a0d8a2..976086920e07 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php @@ -12,19 +12,26 @@ namespace Symfony\Bundle\TwigBundle\Extension; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Routing\RequestContext; + +@trigger_error('The '.__NAMESPACE__.'\AssetsExtension class is deprecated since version 2.7 and will be removed in 3.0. Use the Symfony\Bridge\Twig\Extension\AssetExtension class instead.', E_USER_DEPRECATED); /** * Twig extension for Symfony assets helper. * * @author Fabien Potencier + * + * @deprecated since 2.7, to be removed in 3.0. Use Symfony\Bridge\Twig\Extension\AssetExtension instead. */ class AssetsExtension extends \Twig_Extension { private $container; + private $context; - public function __construct(ContainerInterface $container) + public function __construct(ContainerInterface $container, RequestContext $requestContext = null) { $this->container = $container; + $this->context = $requestContext; } /** @@ -45,14 +52,22 @@ public function getFunctions() * * Absolute paths (i.e. http://...) are returned unmodified. * - * @param string $path A public path - * @param string $packageName The name of the asset package to use + * @param string $path A public path + * @param string $packageName The name of the asset package to use + * @param bool $absolute Whether to return an absolute URL or a relative one + * @param string|bool|null $version A specific version * * @return string A public path which takes into account the base path and URL path */ - public function getAssetUrl($path, $packageName = null) + public function getAssetUrl($path, $packageName = null, $absolute = false, $version = null) { - return $this->container->get('templating.helper.assets')->getUrl($path, $packageName); + $url = $this->container->get('templating.helper.assets')->getUrl($path, $packageName, $version); + + if (!$absolute) { + return $url; + } + + return $this->ensureUrlIsAbsolute($url); } /** @@ -74,4 +89,39 @@ public function getName() { return 'assets'; } + + /** + * Ensures an URL is absolute, if possible. + * + * @param string $url The URL that has to be absolute + * + * @return string The absolute URL + * + * @throws \RuntimeException + */ + private function ensureUrlIsAbsolute($url) + { + if (false !== strpos($url, '://') || 0 === strpos($url, '//')) { + return $url; + } + + if (!$this->context) { + throw new \RuntimeException('To generate an absolute URL for an asset, the Symfony Routing component is required.'); + } + + if ('' === $host = $this->context->getHost()) { + return $url; + } + + $scheme = $this->context->getScheme(); + $port = ''; + + if ('http' === $scheme && 80 != $this->context->getHttpPort()) { + $port = ':'.$this->context->getHttpPort(); + } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) { + $port = ':'.$this->context->getHttpsPort(); + } + + return $scheme.'://'.$host.$port.$url; + } } diff --git a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php index 2a21c30fbaca..4e0bef365ebb 100644 --- a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php +++ b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php @@ -11,8 +11,8 @@ 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; /** @@ -38,7 +38,6 @@ public function __construct(FileLocatorInterface $locator, TemplateNameParserInt $this->locator = $locator; $this->parser = $parser; - $this->cache = array(); } /** diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/debug.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/debug.xml deleted file mode 100644 index c44ae6312155..000000000000 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/debug.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - Symfony\Bundle\TwigBundle\Debug\TimedTwigEngine - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/routing/errors.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/routing/errors.xml new file mode 100644 index 000000000000..bf87f8be7ab6 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/routing/errors.xml @@ -0,0 +1,12 @@ + + + + + + twig.controller.preview_error:previewErrorPageAction + html + \d+ + + diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd b/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd index a75f645a4e75..474b6c9721e0 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd @@ -9,7 +9,9 @@ + + diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 971f4f17ebef..f2c2a4cee007 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -11,34 +11,48 @@ Symfony\Bundle\TwigBundle\TwigEngine Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheCacheWarmer Symfony\Bridge\Twig\Extension\TranslationExtension - Symfony\Bundle\TwigBundle\Extension\AssetsExtension Symfony\Bundle\TwigBundle\Extension\ActionsExtension Symfony\Bridge\Twig\Extension\CodeExtension Symfony\Bridge\Twig\Extension\RoutingExtension 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 Symfony\Component\HttpKernel\EventListener\ExceptionListener Symfony\Bundle\TwigBundle\Controller\ExceptionController + Symfony\Bundle\TwigBundle\Controller\PreviewErrorController - %twig.options% + app - + + + + + + %kernel.environment% + %kernel.debug% + + - + + + + + @@ -57,23 +71,34 @@ + + + + + + + + + + + + - - - + + + - - + - %templating.helper.code.file_link_format% + %kernel.root_dir% %kernel.charset% @@ -86,22 +111,38 @@ + + + + %kernel.debug% + + + + + + + + + + + + %twig.form.resources% - + @@ -120,5 +161,19 @@ %kernel.debug% + + + + %twig.exception_listener.controller% + + + + + + + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig index 042e082b598a..13a41476f2a7 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig @@ -1 +1 @@ -{{ exception.toarray|json_encode|raw }} +{{ { 'error': { 'code': status_code, 'message': status_text, 'exception': exception.toarray } }|json_encode|raw }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception_full.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception_full.html.twig index 1920b6008b11..24d437d45e81 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception_full.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception_full.html.twig @@ -1,7 +1,7 @@ {% extends 'TwigBundle::layout.html.twig' %} {% block head %} - + {% endblock %} {% block title %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig index 7ae1d42be3f8..567213572e80 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig @@ -4,8 +4,8 @@ {% block title %}{% endblock %} - - + + {% block head %}{% endblock %} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php new file mode 100644 index 000000000000..b3ccd2af0199 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.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\Bundle\TwigBundle\Tests\Controller; + +use Symfony\Bundle\TwigBundle\Tests\TestCase; +use Symfony\Bundle\TwigBundle\Controller\ExceptionController; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\HttpFoundation\Request; + +class ExceptionControllerTest extends TestCase +{ + public function testShowActionCanBeForcedToShowErrorPage() + { + $twig = new \Twig_Environment( + new \Twig_Loader_Array(array( + 'TwigBundle:Exception:error404.html.twig' => 'ok', + )) + ); + + $request = Request::create('whatever', 'GET'); + $request->headers->set('X-Php-Ob-Level', 1); + $request->attributes->set('showException', false); + $exception = FlattenException::create(new \Exception(), 404); + $controller = new ExceptionController($twig, /* "showException" defaults to --> */ true); + + $response = $controller->showAction($request, $exception, null); + + $this->assertEquals(200, $response->getStatusCode()); // successful request + $this->assertEquals('ok', $response->getContent()); // content of the error404.html template + } + + public function testFallbackToHtmlIfNoTemplateForRequestedFormat() + { + $twig = new \Twig_Environment( + new \Twig_Loader_Array(array( + 'TwigBundle:Exception:error.html.twig' => 'html', + )) + ); + + $request = Request::create('whatever'); + $request->headers->set('X-Php-Ob-Level', 1); + $request->setRequestFormat('txt'); + $exception = FlattenException::create(new \Exception()); + $controller = new ExceptionController($twig, false); + + $response = $controller->showAction($request, $exception); + + $this->assertEquals('html', $request->getRequestFormat()); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php new file mode 100644 index 000000000000..7bef647a7e28 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.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\Bundle\TwigBundle\Tests\Controller; + +use Symfony\Bundle\TwigBundle\Controller\PreviewErrorController; +use Symfony\Bundle\TwigBundle\Tests\TestCase; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +class PreviewErrorControllerTest extends TestCase +{ + public function testForwardRequestToConfiguredController() + { + $self = $this; + + $request = Request::create('whatever'); + $response = new Response(''); + $code = 123; + $logicalControllerName = 'foo:bar:baz'; + + $kernel = $this->getMock('\Symfony\Component\HttpKernel\HttpKernelInterface'); + $kernel + ->expects($this->once()) + ->method('handle') + ->with( + $this->callback(function (Request $request) use ($self, $logicalControllerName, $code) { + + $self->assertEquals($logicalControllerName, $request->attributes->get('_controller')); + + $exception = $request->attributes->get('exception'); + $self->assertInstanceOf('Symfony\Component\Debug\Exception\FlattenException', $exception); + $self->assertEquals($code, $exception->getStatusCode()); + + $self->assertFalse($request->attributes->get('showException')); + + return true; + }), + $this->equalTo(HttpKernelInterface::SUB_REQUEST) + ) + ->will($this->returnValue($response)); + + $controller = new PreviewErrorController($kernel, $logicalControllerName); + + $this->assertSame($response, $controller->previewErrorPageAction($request, $code)); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php index 1bda8e30dbe6..08e9d4602d8c 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php @@ -43,6 +43,7 @@ public function testMapperPassWithOneTaggedLoaders() { $serviceIds = array( 'test_loader_1' => array( + array(), ), ); @@ -65,8 +66,10 @@ public function testMapperPassWithTwoTaggedLoaders() { $serviceIds = array( 'test_loader_1' => array( + array(), ), 'test_loader_2' => array( + array(), ), ); @@ -90,6 +93,45 @@ public function testMapperPassWithTwoTaggedLoaders() $calls = $this->chainLoader->getMethodCalls(); $this->assertCount(2, $calls); $this->assertEquals('addLoader', $calls[0][0]); + $this->assertEquals('addLoader', $calls[1][0]); + $this->assertEquals('test_loader_1', (string) $calls[0][1][0]); + $this->assertEquals('test_loader_2', (string) $calls[1][1][0]); + } + + public function testMapperPassWithTwoTaggedLoadersWithPriority() + { + $serviceIds = array( + 'test_loader_1' => array( + array('priority' => 100), + ), + 'test_loader_2' => array( + array('priority' => 200), + ), + ); + + $this->builder->expects($this->once()) + ->method('hasDefinition') + ->with('twig') + ->will($this->returnValue(true)); + $this->builder->expects($this->once()) + ->method('findTaggedServiceIds') + ->with('twig.loader') + ->will($this->returnValue($serviceIds)); + $this->builder->expects($this->once()) + ->method('getDefinition') + ->with('twig.loader.chain') + ->will($this->returnValue($this->chainLoader)); + $this->builder->expects($this->once()) + ->method('setAlias') + ->with('twig.loader', 'twig.loader.chain'); + + $this->pass->process($this->builder); + $calls = $this->chainLoader->getMethodCalls(); + $this->assertCount(2, $calls); + $this->assertEquals('addLoader', $calls[0][0]); + $this->assertEquals('addLoader', $calls[1][0]); + $this->assertEquals('test_loader_2', (string) $calls[0][1][0]); + $this->assertEquals('test_loader_1', (string) $calls[1][1][0]); } /** diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php index 7160345fd354..4dfb54eb7687 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -19,12 +19,12 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase public function testDoNoDuplicateDefaultFormResources() { $input = array( - 'form' => array('resources' => array('form_div_layout.html.twig')), + 'form_themes' => array('form_div_layout.html.twig'), ); $processor = new Processor(); $config = $processor->processConfiguration(new Configuration(), array($input)); - $this->assertEquals(array('form_div_layout.html.twig'), $config['form']['resources']); + $this->assertEquals(array('form_div_layout.html.twig'), $config['form_themes']); } } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php index 4a2bdd5b290f..9385131730d8 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -1,28 +1,26 @@ loadFromExtension('twig', array( - 'form' => array( - 'resources' => array( - 'MyBundle::form.html.twig', - ), - ), - 'globals' => array( - 'foo' => '@bar', - 'baz' => '@@qux', - 'pi' => 3.14, - 'bad' => array('key' => 'foo'), - ), - 'auto_reload' => true, - 'autoescape' => true, - 'base_template_class' => 'stdClass', - 'cache' => '/tmp', - 'charset' => 'ISO-8859-1', - 'debug' => true, - 'strict_variables' => true, - 'paths' => array( - 'path1', - 'path2', - 'namespaced_path1' => 'namespace1', - 'namespaced_path2' => 'namespace2', - ), + 'form_themes' => array( + 'MyBundle::form.html.twig', + ), + 'globals' => array( + 'foo' => '@bar', + 'baz' => '@@qux', + 'pi' => 3.14, + 'bad' => array('key' => 'foo'), + ), + 'auto_reload' => true, + 'autoescape' => true, + 'base_template_class' => 'stdClass', + 'cache' => '/tmp', + 'charset' => 'ISO-8859-1', + 'debug' => true, + 'strict_variables' => true, + 'paths' => array( + 'path1', + 'path2', + 'namespaced_path1' => 'namespace1', + 'namespaced_path2' => 'namespace2', + ), )); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-form-resources-only.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-form-resources-only.php new file mode 100644 index 000000000000..fbd2b83f133e --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-form-resources-only.php @@ -0,0 +1,10 @@ +loadFromExtension('twig', array( + 'form' => array( + 'resources' => array( + 'form_table_layout.html.twig', + 'MyBundle:Form:my_theme.html.twig', + ), + ), +)); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-merge-form-resources-with-form-themes.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-merge-form-resources-with-form-themes.php new file mode 100644 index 000000000000..dc36daf8cf0c --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-merge-form-resources-with-form-themes.php @@ -0,0 +1,13 @@ +loadFromExtension('twig', array( + 'form' => array( + 'resources' => array( + 'form_table_layout.html.twig', + 'MyBundle:Form:my_theme.html.twig', + ), + ), + 'form_themes' => array( + 'FooBundle:Form:bar.html.twig', + ), +)); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 3e1acaeb6eef..14c95af97e8f 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -7,9 +7,7 @@ http://symfony.com/schema/dic/twig http://symfony.com/schema/dic/twig/twig-1.0.xsd"> - - MyBundle::form.html.twig - + MyBundle::form.html.twig @@qux 3.14 diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-form-resources-only.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-form-resources-only.xml new file mode 100644 index 000000000000..9aa2486c5f04 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-form-resources-only.xml @@ -0,0 +1,15 @@ + + + + + + + form_table_layout.html.twig + MyBundle:Form:my_theme.html.twig + + + diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-merge-form-resources-with-form-themes.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-merge-form-resources-with-form-themes.xml new file mode 100644 index 000000000000..efe83ea7b78e --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-merge-form-resources-with-form-themes.xml @@ -0,0 +1,16 @@ + + + + + + + form_table_layout.html.twig + MyBundle:Form:my_theme.html.twig + + FooBundle:Form:bar.html.twig + + diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index 1d453c553fab..68ecf463f09f 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -1,7 +1,6 @@ twig: - form: - resources: - - MyBundle::form.html.twig + form_themes: + - MyBundle::form.html.twig globals: foo: "@bar" baz: "@@qux" diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-form-resources-only.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-form-resources-only.yml new file mode 100644 index 000000000000..bb1a75f60351 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-form-resources-only.yml @@ -0,0 +1,5 @@ +twig: + form: + resources: + - "form_table_layout.html.twig" + - "MyBundle:Form:my_theme.html.twig" diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-merge-form-resources-with-form-themes.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-merge-form-resources-with-form-themes.yml new file mode 100644 index 000000000000..8fc9f92e869d --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-merge-form-resources-with-form-themes.yml @@ -0,0 +1,7 @@ +twig: + form_themes: + - "FooBundle:Form:bar.html.twig" + form: + resources: + - "form_table_layout.html.twig" + - "MyBundle:Form:my_theme.html.twig" diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php index becd73bc8dce..cd3968034e10 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -23,6 +23,42 @@ class TwigExtensionTest extends TestCase { + /** + * @dataProvider getFormats + * @group legacy + */ + public function testLegacyFormResourcesConfigurationKey($format) + { + $container = $this->createContainer(); + $container->registerExtension(new TwigExtension()); + $this->loadFromFile($container, 'legacy-form-resources-only', $format); + $this->compileContainer($container); + + // Form resources + $this->assertCount(3, $container->getParameter('twig.form.resources')); + $this->assertContains('form_div_layout.html.twig', $container->getParameter('twig.form.resources')); + $this->assertContains('form_table_layout.html.twig', $container->getParameter('twig.form.resources')); + $this->assertContains('MyBundle:Form:my_theme.html.twig', $container->getParameter('twig.form.resources')); + } + + /** + * @dataProvider getFormats + * @group legacy + */ + public function testLegacyMergeFormResourcesConfigurationKeyWithFormThemesConfigurationKey($format) + { + $container = $this->createContainer(); + $container->registerExtension(new TwigExtension()); + $this->loadFromFile($container, 'legacy-merge-form-resources-with-form-themes', $format); + $this->compileContainer($container); + + $this->assertCount(4, $container->getParameter('twig.form.resources')); + $this->assertContains('form_div_layout.html.twig', $container->getParameter('twig.form.resources')); + $this->assertContains('form_table_layout.html.twig', $container->getParameter('twig.form.resources')); + $this->assertContains('MyBundle:Form:my_theme.html.twig', $container->getParameter('twig.form.resources')); + $this->assertContains('FooBundle:Form:bar.html.twig', $container->getParameter('twig.form.resources')); + } + public function testLoadEmptyConfiguration() { $container = $this->createContainer(); @@ -31,13 +67,14 @@ public function testLoadEmptyConfiguration() $this->compileContainer($container); $this->assertEquals('Twig_Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file'); + $this->assertContains('form_div_layout.html.twig', $container->getParameter('twig.form.resources'), '->load() includes default template for form resources'); // Twig options - $options = $container->getParameter('twig.options'); - $this->assertEquals(__DIR__.'/twig', $options['cache'], '->load() sets default value for cache option'); - $this->assertEquals('UTF-8', $options['charset'], '->load() sets default value for charset option'); - $this->assertFalse($options['debug'], '->load() sets default value for debug option'); + $options = $container->getDefinition('twig')->getArgument(1); + $this->assertEquals('%kernel.cache_dir%/twig', $options['cache'], '->load() sets default value for cache option'); + $this->assertEquals('%kernel.charset%', $options['charset'], '->load() sets default value for charset option'); + $this->assertEquals('%kernel.debug%', $options['debug'], '->load() sets default value for debug option'); } /** @@ -60,7 +97,7 @@ public function testLoadFullConfiguration($format) // Globals $calls = $container->getDefinition('twig')->getMethodCalls(); $this->assertEquals('app', $calls[0][1][0], '->load() registers services as Twig globals'); - $this->assertEquals(new Reference('templating.globals'), $calls[0][1][1]); + $this->assertEquals(new Reference('twig.app_variable'), $calls[0][1][1]); $this->assertEquals('foo', $calls[1][1][0], '->load() registers services as Twig globals'); $this->assertEquals(new Reference('bar'), $calls[1][1][1], '->load() registers services as Twig globals'); $this->assertEquals('baz', $calls[2][1][0], '->load() registers variables as Twig globals'); @@ -75,7 +112,7 @@ public function testLoadFullConfiguration($format) } // Twig options - $options = $container->getParameter('twig.options'); + $options = $container->getDefinition('twig')->getArgument(1); $this->assertTrue($options['auto_reload'], '->load() sets the auto_reload option'); $this->assertTrue($options['autoescape'], '->load() sets the autoescape option'); $this->assertEquals('stdClass', $options['base_template_class'], '->load() sets the base_template_class option'); @@ -95,7 +132,7 @@ public function testLoadCustomTemplateEscapingGuesserConfiguration($format) $this->loadFromFile($container, 'customTemplateEscapingGuesser', $format); $this->compileContainer($container); - $options = $container->getParameter('twig.options'); + $options = $container->getDefinition('twig')->getArgument(1); $this->assertEquals(array(new Reference('my_project.some_bundle.template_escaping_guesser'), 'guess'), $options['autoescape']); } @@ -109,8 +146,8 @@ public function testLoadDefaultTemplateEscapingGuesserConfiguration($format) $this->loadFromFile($container, 'empty', $format); $this->compileContainer($container); - $options = $container->getParameter('twig.options'); - $this->assertEquals(array('Symfony\Bundle\TwigBundle\TwigDefaultEscapingStrategy', 'guess'), $options['autoescape']); + $options = $container->getDefinition('twig')->getArgument(1); + $this->assertEquals('filename', $options['autoescape']); } public function testGlobalsWithDifferentTypesAndValues() @@ -153,10 +190,8 @@ public function testTwigLoaderPaths($format) $def = $container->getDefinition('twig.loader.filesystem'); $paths = array(); foreach ($def->getMethodCalls() as $call) { - if ('addPath' === $call[0]) { - if (false === strpos($call[1][0], 'Form')) { - $paths[] = $call[1]; - } + if ('addPath' === $call[0] && false === strpos($call[1][0], 'Form')) { + $paths[] = $call[1]; } } @@ -181,6 +216,37 @@ public function getFormats() ); } + /** + * @dataProvider stopwatchExtensionAvailabilityProvider + */ + public function testStopwatchExtensionAvailability($debug, $stopwatchEnabled, $expected) + { + $container = $this->createContainer(); + $container->setParameter('kernel.debug', $debug); + if ($stopwatchEnabled) { + $container->register('debug.stopwatch', 'Symfony\Component\Stopwatch\Stopwatch'); + } + $container->registerExtension(new TwigExtension()); + $container->loadFromExtension('twig', array()); + $this->compileContainer($container); + + $tokenParsers = $container->get('twig.extension.debug.stopwatch')->getTokenParsers(); + $stopwatchIsAvailable = new \ReflectionProperty($tokenParsers[0], 'stopwatchIsAvailable'); + $stopwatchIsAvailable->setAccessible(true); + + $this->assertSame($expected, $stopwatchIsAvailable->getValue($tokenParsers[0])); + } + + public function stopwatchExtensionAvailabilityProvider() + { + return array( + 'debug-and-stopwatch-enabled' => array(true, true, true), + 'only-stopwatch-enabled' => array(false, true, false), + 'only-debug-enabled' => array(true, false, false), + 'debug-and-stopwatch-disabled' => array(false, false, false), + ); + } + private function createContainer() { $container = new ContainerBuilder(new ParameterBag(array( diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php new file mode 100644 index 000000000000..b7b5ba438283 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\TwigBundle\Tests\Extension; + +use Symfony\Bundle\TwigBundle\Extension\AssetsExtension; +use Symfony\Bundle\TwigBundle\Tests\TestCase; + +/** + * @group legacy + */ +class LegacyAssetsExtensionTest extends TestCase +{ + /** + * @dataProvider provideGetAssetUrlArguments + */ + public function testGetAssetUrl($path, $packageName, $absolute, $relativeUrl, $expectedUrl, $scheme, $host, $httpPort, $httpsPort) + { + $helper = $this->createHelperMock($path, $packageName, $relativeUrl); + $container = $this->createContainerMock($helper); + + $context = $this->createRequestContextMock($scheme, $host, $httpPort, $httpsPort); + + $extension = new AssetsExtension($container, $context); + $this->assertEquals($expectedUrl, $extension->getAssetUrl($path, $packageName, $absolute)); + } + + public function testGetAssetWithoutHost() + { + $path = '/path/to/asset'; + $packageName = null; + $relativeUrl = '/bundle-name/path/to/asset'; + + $helper = $this->createHelperMock($path, $packageName, $relativeUrl); + $container = $this->createContainerMock($helper); + + $context = $this->createRequestContextMock('http', '', 80, 443); + + $extension = new AssetsExtension($container, $context); + $this->assertEquals($relativeUrl, $extension->getAssetUrl($path, $packageName, true)); + } + + public function provideGetAssetUrlArguments() + { + return array( + array('/path/to/asset', 'package-name', false, '/bundle-name/path/to/asset', '/bundle-name/path/to/asset', 'http', 'symfony.com', 80, null), + array('/path/to/asset', 'package-name', false, 'http://subdomain.symfony.com/bundle-name/path/to/asset', 'http://subdomain.symfony.com/bundle-name/path/to/asset', 'http', 'symfony.com', 80, null), + array('/path/to/asset', null, false, '/bundle-name/path/to/asset', '/bundle-name/path/to/asset', 'http', 'symfony.com', 80, null), + array('/path/to/asset', 'package-name', true, '/bundle-name/path/to/asset', 'http://symfony.com/bundle-name/path/to/asset', 'http', 'symfony.com', 80, null), + array('/path/to/asset', 'package-name', true, 'http://subdomain.symfony.com/bundle-name/path/to/asset', 'http://subdomain.symfony.com/bundle-name/path/to/asset', 'http', 'symfony.com', 80, null), + array('/path/to/asset', null, true, '/bundle-name/path/to/asset', 'https://symfony.com:92/bundle-name/path/to/asset', 'https', 'symfony.com', null, 92), + array('/path/to/asset', null, true, '/bundle-name/path/to/asset', 'http://symfony.com:660/bundle-name/path/to/asset', 'http', 'symfony.com', 660, null), + ); + } + + private function createRequestContextMock($scheme, $host, $httpPort, $httpsPort) + { + $context = $this->getMockBuilder('Symfony\Component\Routing\RequestContext') + ->disableOriginalConstructor() + ->getMock(); + $context->expects($this->any()) + ->method('getScheme') + ->will($this->returnValue($scheme)); + $context->expects($this->any()) + ->method('getHost') + ->will($this->returnValue($host)); + $context->expects($this->any()) + ->method('getHttpPort') + ->will($this->returnValue($httpPort)); + $context->expects($this->any()) + ->method('getHttpsPort') + ->will($this->returnValue($httpsPort)); + + return $context; + } + + private function createContainerMock($helper) + { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container->expects($this->any()) + ->method('get') + ->with('templating.helper.assets') + ->will($this->returnValue($helper)); + + return $container; + } + + private function createHelperMock($path, $packageName, $returnValue) + { + $helper = $this->getMockBuilder('Symfony\Component\Templating\Helper\CoreAssetsHelper') + ->disableOriginalConstructor() + ->getMock(); + $helper->expects($this->any()) + ->method('getUrl') + ->with($path, $packageName) + ->will($this->returnValue($returnValue)); + + return $helper; + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php new file mode 100644 index 000000000000..610bfa3b58be --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\TwigBundle\Tests; + +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; + +class NewCacheWamingTest extends \PHPUnit_Framework_TestCase +{ + public function testCacheIsProperlyWarmedWhenTemplatingIsAvailable() + { + $kernel = new CacheWarmingKernel(true); + $kernel->boot(); + + $warmer = $kernel->getContainer()->get('cache_warmer'); + $warmer->enableOptionalWarmers(); + $warmer->warmUp($kernel->getCacheDir()); + + $this->assertTrue(file_exists($kernel->getCacheDir().'/twig')); + } + + public function testCacheIsNotWarmedWhenTemplatingIsDisabled() + { + $kernel = new CacheWarmingKernel(false); + $kernel->boot(); + + $warmer = $kernel->getContainer()->get('cache_warmer'); + $warmer->enableOptionalWarmers(); + $warmer->warmUp($kernel->getCacheDir()); + + $this->assertFalse(file_exists($kernel->getCacheDir().'/twig')); + } + + protected function setUp() + { + $this->deleteTempDir(); + } + + protected function tearDown() + { + $this->deleteTempDir(); + } + + private function deleteTempDir() + { + if (!file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/CacheWarmingKernel')) { + return; + } + + $fs = new Filesystem(); + $fs->remove($dir); + } +} + +class CacheWarmingKernel extends Kernel +{ + private $withTemplating; + + public function __construct($withTemplating) + { + $this->withTemplating = $withTemplating; + + parent::__construct('dev', true); + } + + public function getName() + { + return 'CacheWarming'; + } + + public function registerBundles() + { + return array(new FrameworkBundle(), new TwigBundle()); + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function ($container) { + $container->loadFromExtension('framework', array( + 'secret' => '$ecret', + )); + }); + + if ($this->withTemplating) { + $loader->load(function ($container) { + $container->loadFromExtension('framework', array( + 'secret' => '$ecret', + 'templating' => array('engines' => array('twig')), + 'router' => array('resource' => '%kernel.root_dir%/Resources/config/empty_routing.yml'), + )); + }); + } + } + + public function getCacheDir() + { + return sys_get_temp_dir().'/'.Kernel::VERSION.'/CacheWarmingKernel/cache'; + } + + public function getLogDir() + { + return sys_get_temp_dir().'/'.Kernel::VERSION.'/CacheWarmingKernel/logs'; + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php new file mode 100644 index 000000000000..068a10526cb6 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.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\Bundle\TwigBundle\Tests; + +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; + +class NoTemplatingEntryTest extends \PHPUnit_Framework_TestCase +{ + public function test() + { + $kernel = new NoTemplatingEntryKernel('dev', true); + $kernel->boot(); + + $container = $kernel->getContainer(); + $content = $container->get('twig')->render('index.html.twig'); + $this->assertContains('{ a: b }', $content); + } + + protected function setUp() + { + $this->deleteTempDir(); + } + + protected function tearDown() + { + $this->deleteTempDir(); + } + + protected function deleteTempDir() + { + if (!file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/NoTemplatingEntryKernel')) { + return; + } + + $fs = new Filesystem(); + $fs->remove($dir); + } +} + +class NoTemplatingEntryKernel extends Kernel +{ + public function registerBundles() + { + return array(new FrameworkBundle(), new TwigBundle()); + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function ($container) { + $container->loadFromExtension('framework', array( + 'secret' => '$ecret', + )); + }); + } + + public function getCacheDir() + { + return sys_get_temp_dir().'/'.Kernel::VERSION.'/NoTemplatingEntryKernel/cache/'.$this->environment; + } + + public function getLogDir() + { + return sys_get_temp_dir().'/'.Kernel::VERSION.'/NoTemplatingEntryKernel/logs'; + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/Resources/config/empty_routing.yml b/src/Symfony/Bundle/TwigBundle/Tests/Functional/Resources/config/empty_routing.yml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/Resources/views/index.html.twig b/src/Symfony/Bundle/TwigBundle/Tests/Functional/Resources/views/index.html.twig new file mode 100644 index 000000000000..ddc4eb4404eb --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/Resources/views/index.html.twig @@ -0,0 +1 @@ +{{ {a: 'b'}|yaml_encode }} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php index 8f1b24c877cd..0ee259fb7b23 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php @@ -11,9 +11,9 @@ namespace Symfony\Bundle\TwigBundle\Tests\Loader; -use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader; use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; +use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader; +use Symfony\Bundle\TwigBundle\Tests\TestCase; class FilesystemLoaderTest extends TestCase { diff --git a/src/Symfony/Bundle/TwigBundle/TokenParser/RenderTokenParser.php b/src/Symfony/Bundle/TwigBundle/TokenParser/RenderTokenParser.php index 0d3ae2b86d12..b386c9b57fc6 100644 --- a/src/Symfony/Bundle/TwigBundle/TokenParser/RenderTokenParser.php +++ b/src/Symfony/Bundle/TwigBundle/TokenParser/RenderTokenParser.php @@ -27,7 +27,7 @@ class RenderTokenParser extends \Twig_TokenParser * * @param \Twig_Token $token A \Twig_Token instance * - * @return \Twig_NodeInterface A \Twig_NodeInterface instance + * @return \Twig_Node A \Twig_Node instance */ public function parse(\Twig_Token $token) { diff --git a/src/Symfony/Bundle/TwigBundle/TwigDefaultEscapingStrategy.php b/src/Symfony/Bundle/TwigBundle/TwigDefaultEscapingStrategy.php index d3a7fb026ec6..c5429ee89a5b 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigDefaultEscapingStrategy.php +++ b/src/Symfony/Bundle/TwigBundle/TwigDefaultEscapingStrategy.php @@ -11,8 +11,12 @@ namespace Symfony\Bundle\TwigBundle; +@trigger_error('The '.__NAMESPACE__.'\TwigDefaultEscapingStrategy class is deprecated in version 2.7 and will be removed in version 3.0. Use the "filename" auto-escaping strategy instead.', E_USER_DEPRECATED); + /** * @author Fabien Potencier + * + * @deprecated since version 2.7, will be removed in 3.0. Use the "filename" auto-escaping strategy instead. */ class TwigDefaultEscapingStrategy { diff --git a/src/Symfony/Bundle/TwigBundle/TwigEngine.php b/src/Symfony/Bundle/TwigBundle/TwigEngine.php index 927256a20cb3..5cd666a3178e 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigEngine.php +++ b/src/Symfony/Bundle/TwigBundle/TwigEngine.php @@ -42,33 +42,29 @@ public function __construct(\Twig_Environment $environment, TemplateNameParserIn } /** - * @deprecated Deprecated since version 2.3, to be removed in 3.0. Inject the escaping - * strategy on Twig_Environment instead + * @deprecated since version 2.7, to be removed in 3.0. + * Inject the escaping strategy on \Twig_Environment instead. */ public function setDefaultEscapingStrategy($strategy) { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0. Inject the escaping strategy in the Twig_Environment object instead.', E_USER_DEPRECATED); + $this->environment->getExtension('escaper')->setDefaultStrategy($strategy); } /** - * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use TwigDefaultEscapingStrategy instead. + * @deprecated since version 2.7, to be removed in 3.0. + * Use the 'filename' strategy instead. */ public function guessDefaultEscapingStrategy($filename) { - return TwigDefaultEscapingStrategy::guess($filename); + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0. Use the Twig_FileExtensionEscapingStrategy::guess method instead.', E_USER_DEPRECATED); + + return \Twig_FileExtensionEscapingStrategy::guess($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()) { @@ -88,13 +84,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 168701fa6547..c05ebe38e8db 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -16,28 +16,33 @@ } ], "require": { - "php": ">=5.3.3", - "symfony/twig-bridge": "~2.3,>=2.3.10", - "symfony/http-kernel": "~2.3.24|~2.5.9|~2.6,>=2.6.2" + "php": ">=5.3.9", + "symfony/asset": "~2.7", + "symfony/twig-bridge": "~2.7", + "symfony/http-foundation": "~2.5", + "symfony/http-kernel": "~2.7" }, "require-dev": { "symfony/stopwatch": "~2.2", - "symfony/dependency-injection": "~2.2", + "symfony/dependency-injection": "~2.6,>=2.6.6", + "symfony/expression-language": "~2.4", "symfony/config": "~2.2", + "symfony/finder": "~2.0,>=2.0.5", + "symfony/routing": "~2.1", + "symfony/templating": "~2.1", "symfony/yaml": "~2.3", - "symfony/framework-bundle": "~2.1" + "symfony/framework-bundle": "~2.7" }, "autoload": { - "psr-0": { "Symfony\\Bundle\\TwigBundle\\": "" }, + "psr-4": { "Symfony\\Bundle\\TwigBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "target-dir": "Symfony/Bundle/TwigBundle", "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.7-dev" } } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index a27cdd883582..3ccdf96b867c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +2.7.0 +----- + + * [BC BREAK] if you are using a DB to store profiles, the table must be dropped + * added the HTTP status code to profiles + 2.3.0 ----- diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index 173616fd9f50..2748910a19ae 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -241,15 +241,20 @@ public function searchBarAction(Request $request) $token = $session->get('_profiler_search_token'); } - return new Response($this->twig->render('@WebProfiler/Profiler/search.html.twig', array( - 'token' => $token, - 'ip' => $ip, - 'method' => $method, - 'url' => $url, - 'start' => $start, - 'end' => $end, - 'limit' => $limit, - )), 200, array('Content-Type' => 'text/html')); + return new Response( + $this->twig->render('@WebProfiler/Profiler/search.html.twig', array( + 'token' => $token, + 'ip' => $ip, + 'method' => $method, + 'url' => $url, + 'start' => $start, + 'end' => $end, + 'limit' => $limit, + 'request' => $request, + )), + 200, + array('Content-Type' => 'text/html') + ); } /** diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php index f4a84bf56873..d77eb279f4a6 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\Matcher\TraceableUrlMatcher; @@ -18,6 +19,7 @@ use Symfony\Component\Routing\RouterInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; /** * RouterController. @@ -62,16 +64,39 @@ public function panelAction($token) $profile = $this->profiler->loadProfile($token); - $context = $this->matcher->getContext(); - $context->setMethod($profile->getMethod()); - $matcher = new TraceableUrlMatcher($this->routes, $context); - + /** @var RequestDataCollector $request */ $request = $profile->getCollector('request'); return new Response($this->twig->render('@WebProfiler/Router/panel.html.twig', array( 'request' => $request, 'router' => $profile->getCollector('router'), - 'traces' => $matcher->getTraces($request->getPathInfo()), + 'traces' => $this->getTraces($request, $profile->getMethod()), )), 200, array('Content-Type' => 'text/html')); } + + /** + * Returns the routing traces associated to the given request. + * + * @param RequestDataCollector $request + * @param string $method + * + * @return array + */ + private function getTraces(RequestDataCollector $request, $method) + { + $traceRequest = Request::create( + $request->getPathInfo(), + $request->getRequestServer()->get('REQUEST_METHOD'), + $request->getRequestAttributes()->all(), + $request->getRequestCookies()->all(), + array(), + $request->getRequestServer()->all() + ); + + $context = $this->matcher->getContext(); + $context->setMethod($method); + $matcher = new TraceableUrlMatcher($this->routes, $context); + + return $matcher->getTracesForRequest($traceRequest); + } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php index 4f84378f8d0c..957a683ca356 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php @@ -45,6 +45,7 @@ public function getConfigTreeBuilder() ->end() ->end() ->booleanNode('intercept_redirects')->defaultFalse()->end() + ->scalarNode('excluded_ajax_paths')->defaultValue('^/(app(_[\\w]+)?\\.php/)?_wdt')->end() ->end() ; diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php index c0bd0cdb397c..8af7c63e64aa 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php @@ -44,18 +44,14 @@ 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->getDefinition('web_profiler.debug_toolbar')->replaceArgument(5, $config['excluded_ajax_paths']); + $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 1a2663d3fa02..ab67078dd6ec 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -11,12 +11,13 @@ namespace Symfony\Bundle\WebProfilerBundle\EventListener; +use Symfony\Component\HttpFoundation\Request; 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,16 +35,20 @@ class WebDebugToolbarListener implements EventSubscriberInterface const ENABLED = 2; protected $twig; + protected $urlGenerator; protected $interceptRedirects; protected $mode; protected $position; + protected $excludedAjaxPaths; - 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, $excludedAjaxPaths = '^/bundles|^/_wdt') { $this->twig = $twig; + $this->urlGenerator = $urlGenerator; $this->interceptRedirects = (bool) $interceptRedirects; $this->mode = (int) $mode; $this->position = $position; + $this->excludedAjaxPaths = $excludedAjaxPaths; } public function isEnabled() @@ -53,13 +58,24 @@ 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) { + try { + $response->headers->set( + 'X-Debug-Token-Link', + $this->urlGenerator->generate('_profiler', array('token' => $response->headers->get('X-Debug-Token')), UrlGeneratorInterface::ABSOLUTE_URL) + ); + } catch (\Exception $e) { + $response->headers->set('X-Debug-Error', get_class($e).': '.$e->getMessage()); + } + } + + if (!$event->isMasterRequest()) { + return; + } + // do not capture redirects or modify XML HTTP Requests if ($request->isXmlHttpRequest()) { return; @@ -82,11 +98,12 @@ public function onKernelResponse(FilterResponseEvent $event) || $response->isRedirection() || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) || 'html' !== $request->getRequestFormat() + || false !== stripos($response->headers->get('Content-Disposition'), 'attachment;') ) { return; } - $this->injectToolbar($response); + $this->injectToolbar($response, $request); } /** @@ -94,7 +111,7 @@ public function onKernelResponse(FilterResponseEvent $event) * * @param Response $response A Response instance */ - protected function injectToolbar(Response $response) + protected function injectToolbar(Response $response, Request $request) { $content = $response->getContent(); $pos = strripos($content, ''); @@ -104,7 +121,9 @@ protected function injectToolbar(Response $response) '@WebProfiler/Profiler/toolbar_js.html.twig', array( 'position' => $this->position, + 'excluded_ajax_paths' => $this->excludedAjaxPaths, 'token' => $response->headers->get('X-Debug-Token'), + 'request' => $request, ) ))."\n"; $content = substr($content, 0, $pos).$toolbar.substr($content, $pos); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml index 874ba8b21624..ed7e923f0d05 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml @@ -8,6 +8,7 @@ Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController Symfony\Bundle\WebProfilerBundle\Controller\RouterController Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController + Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension @@ -30,5 +31,9 @@ %kernel.debug%
+ + + +
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml index 1372b0a61f10..d72c28532c33 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml @@ -15,6 +15,8 @@ %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/ajax.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/ajax.html.twig new file mode 100644 index 000000000000..d557629d18b1 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/ajax.html.twig @@ -0,0 +1,30 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set icon %} + + + 0 + + {% endset %} + {% set text %} +
+ AJAX requests + +
+
+ + + + + + + + + + +
MethodURLTimeProfile
+
+ {% endset %} + {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': false } %} +{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig index a5f091ecc5e4..d25122b90bf2 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig @@ -4,12 +4,23 @@ {# Symfony Logo #} {% set icon %} - Symfony + {% if collector.applicationname %} {{ collector.applicationname }} {{ collector.applicationversion }} - {% else %} - {{ collector.symfonyversion }} + {% elseif collector.symfonyState is defined %} + {% if 'unknown' == collector.symfonyState -%} + + {%- elseif 'eol' == collector.symfonyState -%} + + {%- elseif 'eom' == collector.symfonyState -%} + + {%- elseif 'dev' == collector.symfonyState -%} + + {%- else -%} + + {%- endif -%} + {{ collector.symfonyversion }} {% endif %} @@ -32,7 +43,7 @@ {# PHP Information #} {% set icon %} - PHP + {% endset %} {% set text %} @@ -57,7 +68,7 @@ {# Environment #} {% set debug_status_class %}sf-toolbar-status sf-toolbar-status-{{ collector.debug ? 'green' : 'red' }}{% endset %} {% set icon %} - Environment + {{ token }} {% if 'n/a' != collector.appname or 'n/a' != collector.env %} @@ -104,7 +115,7 @@ {% block menu %} - Configuration + Config {% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig index 0607aa8a3b64..879e537f13ce 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig @@ -4,7 +4,7 @@ {% block menu %} - Events + Events {% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig index 121a9ee5d696..5175f6610769 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig @@ -11,7 +11,7 @@ {% block menu %} - Exception + Exception {% if collector.hasexception %} 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 000000000000..33cbf8f24236 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -0,0 +1,680 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% from _self import form_tree_entry, form_tree_details %} + +{% block toolbar %} + {% if collector.data|length %} + {% set icon %} + + {% 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) %} + {% import _self as tree %} +
  • +
    + {% if data.children is not empty %} + + {% else %} +
    + {% endif %} + {{ name|default('(no name)') }} {% if data.type_class is defined and data.type is defined %}[{{ data.type }}]{% endif %} + {% if data.errors is defined and data.errors|length > 0 %} +
    {{ data.errors|length }}
    + {% endif %} +
    + + {% if data.children is not empty %} + + {% endif %} +
  • +{% endmacro %} + +{% macro form_tree_details(name, data, forms_by_hash) %} + {% import _self as tree %} +
    +

    + {{ name|default('(no name)') }} + {% if data.type_class is defined and data.type is defined %} + [{{ data.type }}] + {% endif %} +

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

    + + 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 %} + + {% for trace in error.trace %} + {% if not loop.first %} +
    Caused by:

    + {% endif %} + {% if trace.root is defined %} + {{ trace.class }}
    +
    +                                    {{- trace.root -}}
    +                                    {%- if trace.path is not empty -%}
    +                                        {%- if trace.path|first != '[' %}.{% endif -%}
    +                                        {{- trace.path -}}
    +                                    {%- endif %} = {{ trace.value -}}
    +                                
    + {% elseif trace.message is defined %} + {{ trace.class }}
    +
    {{ trace.message }}
    + {% else %} +
    {{ trace }}
    + {% endif %} + {% else %} + Unknown. + {% endfor %} +
    +
    + {% 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 same as(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 %} + {{ tree.form_tree_details(childName, childData, forms_by_hash) }} + {% endfor %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig index 7bed56df668a..261163090bca 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -3,21 +3,21 @@ {% import _self as logger %} {% block toolbar %} - {% if collector.counterrors or collector.countdeprecations %} + {% if collector.counterrors or collector.countdeprecations or collector.countscreams %} {% set icon %} - Logs + {% if collector.counterrors %} {% set status_color = "red" %} - {% else %} + {% elseif collector.countdeprecations %} {% set status_color = "yellow" %} {% endif %} - {% set error_count = collector.counterrors + collector.countdeprecations %} - {{ error_count }} + {% set error_count = collector.counterrors + collector.countdeprecations + collector.countscreams %} + {{ error_count }} {% endset %} {% set text %} {% if collector.counterrors %}
    - Exception + Errors {{ collector.counterrors }}
    {% endif %} @@ -27,6 +27,12 @@ {{ collector.countdeprecations }} {% endif %} + {% if collector.countscreams %} +
    + Silenced Errors + {{ collector.countscreams }} +
    + {% endif %} {% endset %} {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %} {% endif %} @@ -34,10 +40,10 @@ {% block menu %} - Logger + Logs - {% if collector.counterrors or collector.countdeprecations %} - {% set error_count = collector.counterrors + collector.countdeprecations %} + {% if collector.counterrors or collector.countdeprecations or collector.countscreams %} + {% set error_count = collector.counterrors + collector.countdeprecations + collector.countscreams %} {{ error_count }} @@ -56,12 +62,21 @@ - +