diff --git a/.php_cs.dist b/.php_cs.dist index 71c4a35f5883b..ee298bd15678e 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -37,5 +37,7 @@ return PhpCsFixer\Config::create() ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') // explicit heredoc test ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php') + // purposefully invalid JSON + ->notPath('Symfony/Component/Asset/Tests/fixtures/manifest-invalid.json') ) ; diff --git a/.travis.yml b/.travis.yml index af674057da4b8..cc245b657ef76 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,24 +10,18 @@ addons: apt_packages: - parallel - language-pack-fr-base + - ldap-utils + - slapd env: global: - - MIN_PHP=5.3.9 - - SYMFONY_PROCESS_PHP_TEST_BINARY=~/.phpenv/versions/5.6/bin/php + - MIN_PHP=7.1.3 + - SYMFONY_PROCESS_PHP_TEST_BINARY=~/.phpenv/versions/7.1/bin/php matrix: include: - # 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.18 - sudo: required - dist: trusty - group: edge - - php: 5.3 - - php: 5.4 - - php: 5.5 - - php: 5.6 - - php: 7.0 + - php: 7.1.3 + - php: 7.1 env: deps=high - php: 7.1 env: deps=low @@ -38,12 +32,17 @@ cache: - .phpunit - php-$MIN_PHP -services: mongodb +services: + - memcached + - mongodb + - redis-server before_install: - | # General configuration stty cols 120 + mkdir /tmp/slapd + slapd -f src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf -h ldap://localhost:3389 & PHP=$TRAVIS_PHP_VERSION [ -d ~/.composer ] || mkdir ~/.composer cp .composer/* ~/.composer/ @@ -64,28 +63,20 @@ before_install: export -f tfold # php.ini configuration - if [[ $PHP = hhvm* ]]; then - INI=/etc/hhvm/php.ini - else - INI=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - phpenv config-rm xdebug.ini || echo "xdebug not available" - fi + INI=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini + phpenv config-rm xdebug.ini || echo "xdebug not available" echo date.timezone = Europe/Paris >> $INI echo memory_limit = -1 >> $INI echo session.gc_probability = 0 >> $INI echo opcache.enable_cli = 1 >> $INI - echo hhvm.jit = 0 >> $INI echo apc.enable_cli = 1 >> $INI echo extension = ldap.so >> $INI - [[ $PHP = 5.* ]] && echo extension = memcache.so >> $INI - if [[ $PHP = 5.* ]]; then - echo extension = mongo.so >> $INI - elif [[ $PHP = 7.* ]]; then - echo extension = mongodb.so >> $INI - fi + echo extension = redis.so >> $INI + echo extension = memcached.so >> $INI + echo extension = mongodb.so >> $INI # Matrix lines for intermediate PHP versions are skipped for pull requests - if [[ ! $deps && ! $PHP = ${MIN_PHP%.*} && ! $PHP = hhvm* && $TRAVIS_PULL_REQUEST != false ]]; then + if [[ ! $deps && ! $PHP = $MIN_PHP && $TRAVIS_PULL_REQUEST != false ]]; then deps=skip skip=1 else @@ -94,21 +85,24 @@ before_install: - | # Install sigchild-enabled PHP 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 && + if [[ ! $deps && $PHP = $MIN_PHP && ! -d php-$MIN_PHP/sapi ]]; then + wget http://php.net/get/php-$MIN_PHP.tar.bz2/from/this/mirror -O - | tar -xj && (cd php-$MIN_PHP && ./configure --enable-sigchild --enable-pcntl && make -j2) fi - | # Install extra PHP extensions - if [[ ! $skip && $PHP = 5.* ]]; then - ([[ $deps ]] || tfold ext.symfony_debug 'cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo extension = $(pwd)/modules/symfony_debug.so >> '"$INI") && - tfold ext.memcached pecl install -f memcached-2.1.0 && - tfold ext.apcu4 'echo yes | pecl install -f apcu-4.0.11' - elif [[ ! $skip && $PHP = 7.* ]]; then + if [[ ! $skip && $PHP = 7.* ]]; then tfold ext.apcu5 'echo yes | pecl install -f apcu-5.1.6' fi + - | + # Load fixtures + if [[ ! $skip ]]; then + ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif && + ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif + fi + install: - | # Create local composer packages for each patched components and reference them in composer.json files when cross-testing components @@ -141,15 +135,13 @@ install: export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev if [[ ! $skip && $deps ]]; then mv composer.json.phpunit composer.json; fi - if [[ ! $skip && $PHP = 7.* ]]; then + if [[ ! $skip ]]; then ([[ $deps ]] && cd src/Symfony/Component/HttpFoundation; composer require --dev --no-update mongodb/mongodb) fi - if [[ ! $skip ]]; then $COMPOSER_UP; fi - if [[ ! $skip ]]; then ./phpunit install; fi - - | - # phpinfo - if [[ ! $PHP = hhvm* ]]; then php -i; else hhvm --php -r 'print_r($_SERVER);print_r(ini_get_all());'; fi + - php -i - | run_tests () { @@ -159,14 +151,15 @@ install: elif [[ $deps = high ]]; then echo "$COMPONENTS" | parallel --gnu -j10% "tfold {} 'cd {} && $COMPOSER_UP && $PHPUNIT_X$LEGACY'" elif [[ $deps = low ]]; then - echo "$COMPONENTS" | parallel --gnu -j10% "tfold {} 'cd {} && $COMPOSER_UP --prefer-lowest --prefer-stable && $PHPUNIT_X'" - elif [[ $PHP = hhvm* ]]; then - $PHPUNIT --exclude-group benchmark,intl-data + echo "$COMPONENTS" | parallel --gnu -j10% "tfold {} 'cd {} && $COMPOSER_UP --prefer-lowest --prefer-stable && $PHPUNIT_X'" && + # Test the PhpUnit bridge on PHP 5.3, using the original phpunit script + tfold src/Symfony/Bridge/PhpUnit \ + "cd src/Symfony/Bridge/PhpUnit && wget https://phar.phpunit.de/phpunit-4.8.phar && phpenv global 5.3 && composer update --no-progress --ansi && php phpunit-4.8.phar" else echo "$COMPONENTS" | parallel --gnu "tfold {} $PHPUNIT_X {}" tfold tty-group $PHPUNIT --group tty - if [[ $PHP = ${MIN_PHP%.*} ]]; then - echo -e "1\\n0" | xargs -I{} bash -c "tfold src/Symfony/Component/Process.sigchild{} ENHANCE_SIGCHLD={} php-$MIN_PHP/sapi/cli/php .phpunit/phpunit-4.8/phpunit --colors=always src/Symfony/Component/Process/" + if [[ $PHP = $MIN_PHP ]]; then + tfold src/Symfony/Component/Process.sigchild SYMFONY_DEPRECATIONS_HELPER=weak php-$MIN_PHP/sapi/cli/php ./phpunit --colors=always src/Symfony/Component/Process/ fi fi } diff --git a/CHANGELOG-2.2.md b/CHANGELOG-2.2.md deleted file mode 100644 index 274ab05e9216e..0000000000000 --- a/CHANGELOG-2.2.md +++ /dev/null @@ -1,352 +0,0 @@ -CHANGELOG for 2.2.x -=================== - -This changelog references the relevant changes (bug and security fixes) done -in 2.2 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.2.0...v2.2.1 - -* 2.2.11 (2013-12-02) - - * 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 #9643 [WebProfilerBundle] Fixed js escaping in time.html.twig (hason) - * 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 #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) - * bug #9537 [FrameworkBundle] Fix mistake in translation's service definition. (phpmike) - * bug #9367 [Process] Check if the pipe array is empty before calling stream_select() (jfposton) - * bug #9469 [Propel1] re-factor Propel1 ModelChoiceList (havvg) - -* 2.2.10 (2013-11-13) - - * bug #9499 Request::overrideGlobals() may call invalid ini value (denkiryokuhatsuden) - * bug #9212 [Validator] Force Luhn Validator to only work with strings (Richtermeister) - * bug #9431 [DependencyInjection] fixed YamlDumper did not make services private. (realityking) - * bug #9412 [HttpFoundation] added content length header to BinaryFileResponse (kbond) - * bug #9388 [Form] Fixed: The "data" option is taken into account even if it is NULL (bschussek) - * bug #9391 [Serializer] Fixed the error handling when decoding invalid XML to avoid a Warning (stof) - * 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 #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 #9327 [Form] Changed FormTypeCsrfExtension to use the form's name as default intention (bschussek) - * bug #9308 [DoctrineBridge] Loosened CollectionToArrayTransformer::transform() to accept arrays (bschussek) - * 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) - -* 2.2.9 (2013-10-10) - - * [Security] limited the password length passed to encoders - * 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 #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) - * bug #9190 Fix: duplicate usage of Symfony\Component\HttpFoundation\Response (realsim) - * bug #9188 [Form] add support for Length and Range constraint in ValidatorTypeGuesser (franek) - * bug #8809 [Form] enforce correct timezone (Burgov) - * bug #9169 Fixed client insulation when using the terminable event (fabpot) - * bug #9154 Fix problem with Windows file links (backslash in JavaScript string) (fabpot) - * bug #9103 [HttpFoundation] Header `HTTP_X_FORWARDED_PROTO` can contain various values (stloyd) - -* 2.2.8 (2013-09-25) - - * same as 2.2.7 - -* 2.2.7 (2013-09-25) - - * 8980954: bugix: CookieJar returns cookies with domain "domain.com" for domain "foodomain.com" - * 3108c71: [Locale] added support for the position argument to NumberFormatter::parse() - * 0774c79: [Locale] added some more stubs for the number formatter - * e5282e8: [DomCrawler]Crawler guess charset from html - * 0e80d88: fixes RequestDataCollector bug, visible when used on Drupal8 - * c8d0342: [Console] fixed exception rendering when nested styles - * a47d663: [Console] fixed the formatter for single-char tags - * c6c35b3: [Console] Escape exception message during the rendering of an exception - * 0e437c5: [BrowserKit] Fixed the handling of parameters when redirecting - * 958ec09: NativeSessionStorage regenerate - * 0d6af5c: Use setTimeZone if this method exists. - * 773e716: [HttpFoundation] Fixed the way path to directory is trimmed. - * 42019f6: [Console] Fixed argument parsing when a single dash is passed. - * b591419: [HttpFoundation] removed double-slashes (closes #8388) - * 4f5b8f0: [HttpFoundation] tried to keep the original Request URI as much as possible to avoid different behavior between ::createFromGlobals() and ::create() - * 4c1dbc7: [TwigBridge] fixed form rendering when used in a template with dynamic inheritance - * 8444339: [HttpKernel] added a check for private event listeners/subscribers - * ce7de37: [DependencyInjection] fixed a non-detected circular reference in PhpDumper (closes #8425) - * 37102dc: [Process] Close unix pipes before calling `proc_close` to avoid a deadlock - * 8c2a733: [HttpFoundation] fixed format duplication in Request - * 1e75cf9: [Process] Fix #8970 : read output once the process is finished, enable pipe tests on Windows - * ed83752: [Form] Fixed expanded choice field to be marked invalid when unknown choices are submitted - * 30aa1de: [Form] Fixed ChoiceList::get*By*() methods to preserve order and array keys - * 49f5027: [HttpKernel] fixer HInclude src (closes #8951) - * c567262: Fixed escaping of service identifiers in configuration - * 4a76c76: [Process][2.2] Fix Process component on windows - * 65814ba: Request->getPort() should prefer HTTP_HOST over SERVER_PORT - * e75d284: Fixing broken http auth digest in some circumstances (php-fpm + apache). - * 899f176: [Security] fixed a leak in ExceptionListener - * 2fd8a7a: [Security] fixed a leak in the ContextListener - * 4e9d990: Ignore posix_istatty warnings - * 2d34e78: [BrowserKit] fixed method/files/content when redirecting a request - * 64e1655: [BrowserKit] removed some headers when redirecting a request - * 96a4b00: [BrowserKit] fixed headers when redirecting if history is set to false (refs #8697) - * c931eb7: [HttpKernel] fixed route parameters storage in the Request data collector (closes #8867) - * 96bb731: optimized circular reference checker - * 91234cd: [HttpKernel] changed fragment URLs to be relative by default (closes #8458) - * 4922a80: [FrameworkBundle] added support for double-quoted strings in the extractor (closes #8797) - * 0d07af8: [BrowserKit] Pass headers when `followRedirect()` is called - * d400b5a: Return BC compatibility for `@Route` parameters and default values - -* 2.2.6 (2013-08-26) - - * f936b41: clearToken exception is thrown at wrong place. - * d0faf55: [Locale] Fixed: StubLocale::setDefault() throws no exception when "en" is passed - * 566d79c: [Yaml] fixed embedded folded string parsing - * 0951b8d: [Translation] Fixed regression: When only one rule is passed to transChoice(), this rule should be used - * 4563f1b: [Yaml] Fix comment containing a colon on a scalar line being parsed as a hash. - * 7e87eb1: fixed request format when forwarding a request - * ccaaedf: [Form] PropertyPathMapper::mapDataToForms() *always* calls setData() on every child to ensure that all *_DATA events were fired when the initialization phase is over (except for virtual forms) - * 00bc270: [Form] Fixed: submit() reacts to dynamic modifications of the form children - * 05fdb12: Fixed issue #6932 - Inconsistent locale handling in subrequests - * b3c3159: fixed locale of sub-requests when explicitely set by the developer (refs #8821) - * b72bc0b: [Locale] fixed build-data exit code in case of an error - * 9bb7a3d: fixed request format of sub-requests when explicitely set by the developer (closes #8787) - * fa35597: Sets _format attribute only if it wasn't set previously by the user. - * f946108: fixed the format of the request used to render an exception - * 51022c3: Fix typo in the check_path validator - * 5f7219e: added a missing use statement (closes #8808) - * 262879d: fix for Process:isSuccessful() - * 0723c10: [Process] Use a consistent way to reset data of the process latest run - * 85a9c9d: [HttpFoundation] Fixed removing a nonexisting namespaced attribute. - * 191d320: [Validation] Fixed IdentityTranslator to pass correct Locale to MessageSelector - * c6ecd83: SwiftMailerHandler in Monolog bridge now able to react to kernel.terminate event - * 99adcf1: {HttpFoundation] [Session] fixed session compatibility with memcached/redis session storage - * ab9a96b: Fixes for hasParameterOption and getParameterOption methods of ArgvInput - * dbd0855: Added sleep() workaround for windows php rename bug - * fa769a2: [Process] Add more precision to Process::stop timeout - * 3ef517b: [Process] Fix #8739 - * 18896d5a: [Validator] fixed the wrong isAbstract() check against the class (fixed #8589) - * e8e76ec: [TwigBridge] Prevent code extension to display warning - * 1a73b44: added missing support for the new output API in PHP 5.4+ - * e0c7d3d: Fixed bug introduced in #8675 - * 0b965fb: made the filesystem loader compatible with Twig 2.0 - * 322f880: replaced deprecated Twig features - -* 2.2.5 (2013-08-07) - - * c35cc5b: added trusted hosts check - * 6d555bc: Fixed metadata serialization - * cd51d82: [Form] fixed wrong call to setTimeZone() (closes #8644) - * 5c359a8: Fix issue with \DateTimeZone::UTC / 'UTC' for PHP 5.4 - * 97cbb19: [Form] Removed the "disabled" attribute from the placeholder option in select fields due to problems with the BlackBerry 10 browser - * c138304: [routing] added ability for apache matcher to handle array values - * b41cf82: [Validator] fixed StaticMethodLoader trying to invoke methods of abstract classes (closes #8589) - * 3553c71: return 0 if there is no valid data - * ae7fa11: [Twig] fixed TwigEngine::exists() method when a template contains a syntax error (closes #8546) - * 28e0709: [Validator] fixed ConstraintViolation:: incorrect when nested - * 890934d: handle Optional and Required constraints from XML or YAML sources correctly - * a2eca45: Fixed #8455: PhpExecutableFinder::find() does not always return the correct binary - * 485d53a: [DependencyInjection] Fix Container::camelize to convert beginning and ending chars - * 2317443: [Security] fixed issue where authentication listeners clear unrelated tokens - * 2ebb783: fix issue #8499 modelChoiceList call getPrimaryKey on a non object - * d3eb9b7: [Validator] Fixed groups argument misplace for validateValue method from validator class - -* 2.2.4 (2013-07-15) - - * 52e530d: Fixed NativeSessionStorage:regenerate when does not exists - * bb59f40: Reverts JSON_NUMERIC_CHECK - * 9c5f8c6: [Yaml] removed wrong comment removal inside a string block - * 2dc1ee0: [HtppKernel] fixed inline fragment renderer - * 06b69b8: fixed inline fragment renderer - * 91bb757: ProgressHelper shows percentage complete. - * 9d1004b: fix handling of a default 'template' as a string - * 82dbaee: [HttpKernel] fixed the inline renderer when passing objects as attributes (closes #7124) - * 6dbd1e1: [WebProfiler] fix content-type parameter - * a830001: Passed the config when building the Configuration in ConfigurableExtension - * c875d0a: [Form] fixed INF usage which does not work on Solaris (closes #8246) - -* 2.2.3 (2013-06-19) - - * c0da3ae: [Process] Disable exception on stream_select timeout - * 77f2aa8: [HttpFoundation] fixed issue with session_regenerate_id (closes #7380) - * bcbbb28: Throw exception if value is passed to VALUE_NONE input, long syntax - * 6b71513: fixed date type format pattern regex - * 842f3fa: do not re-register commands each time a Console\Application is run - * 0991cd0: [Process] moved env check to the Process class (refs #8227) - * 8764944: fix issue where $_ENV contains array vals - * 4139936: [DomCrawler] Fix handling file:// without a host - * de289d2: [Form] corrected interface bind() method defined against in deprecation notice - * 0c0a3e9: [Console] fixed regression when calling a command foo:bar if there is another one like foo:bar:baz (closes #8245) - * 849f3ed: [Finder] Fix SplFileInfo::getContents isn't working with ssh2 protocol - * 25e3abd: fix many-to-many Propel1 ModelChoiceList - * bce6bd2: [DomCrawler] Fixed a fatal error when setting a value in a malformed field name. - * 445b2e3: [Console] fix status code when Exception::getCode returns something like 0.1 - * bbfde62: Fixed exit code for exceptions with error code 0 - * afad9c7: instantiate valid commands only - * 6d2135b: force the Content-Type to html in the web profiler controllers - -* 2.2.2 (2013-06-02) - - * 2038329: [Form] [Validator] Fixed post_max_size = 0 bug (Issue #8065) - * 169c0b9: [Finder] Fix iteration fails with non-rewindable streams - * 45b68e0: [Finder] Fix unexpected duplicate sub path related AppendIterator issue - * 5321600: Fixed two bugs in HttpCache - * 5c317b7: [Console] fix and refactor exit code handling - * 1469953: [CssSelector] Fix :nth-last-child() translation - * 91b8490: Fix Crawler::children() to not trigger a notice for childless node - * 0a4837d: Fixed XML syntax. - * a5441b2: Fixed parsing of leading blank lines in folded scalars. Closes #7989. - * ef87ba7: [Form] Fixed a method name. - * e8d5d16: Fixed Loader import - * 60edc58: Fixed fatal error in normalize/denormalizeObject. - * 05b987f: [Process] Cleanup tests & prevent assertion that kills randomly Travis-CI - * e4913f8: [Filesystem] Fix regression introduced in 10dea948 - * 5b7e1e6: added a missing check for the provider key - * b0e3ea5: [Validator] fixed wrong URL for XSD - * 59b78c7: [Validator] Fixed: $traverse and $deep is passed to the visitor from Validator::validate() - * bcb5400: [Form] Fixed transform()/reverseTransform() to always throw TransformationFailedExceptions - * 7b2ebbf: [Form] Fixed: String validation groups are never interpreted as callbacks - * 0610750: if the repository method returns an array ensure that it's internal poin... - * dcced01: [Form] Improved multi-byte handling of NumberToLocalizedStringTransformer - * 2b554d7: remove validation related headers when needed - * 2a531d7: Fix getPort() returning 80 instead of 443 when X-FORWARDED-PROTO is set to https - * 10dea94: [Filesystem] copy() is not working when open_basedir is set - * 8757ad4: [Process] Fix #5594 : `termsig` must be used instead of `stopsig` in exceptions when a process is signaled - * be34917: [Console] find command even if its name is a namespace too (closes #7860) - * 3c97004: Reset all catalogues when adding resource to fallback locale (#7715, #7819) - * 0fb35a4: Added reloading of fallback catalogues when calling addResource() (#7715) - * 9e49bc8: Re-added context information to log list - * 06e21ff: Filesystem::touch() not working with different owners (utime/atime issue) - * d98118a: [Config] #7644 add tests for passing number looking attributes as strings - * 36d057b: [HttpFoundation][BrowserKit] fixed path when converting a cookie to a string - * 495d0e3: [HttpFoundation] fixed empty domain= in Cookie::__toString() - * c2bc707: fixed detection of secure cookies received over https - * af819a7: [2.2] Pass ESI header to subrequests - * 54bcf5c: [Translator] added additional conversion for encodings other than utf-8 - * 67b5797: fixed source messages to accept pluralized messages [Validator][translation][japanese] add messages for new validator - * 8a434ed: fix a DI circular reference recognition bug - * 22bf965: [DependencyInjection] fixed wrong exception class - * 5abf887: Fix default value handling for multi-value options - * da156d3: fix overwriting of request's locale if attribute _locale is missing - * 1adbe3c: [HttpKernel] truncate profiler token to 6 chars (see #7665) - * d552e4c: [HttpFoundation] do not use server variable PATH_INFO because it is already decoded and thus symfony is fragile to double encoding of the path - * 4c51ec7: Fix download over SSL using IE < 8 and binary file response - * 46909fa: [Console] Fix merging of application definition, fixes #7068, replaces #7158 - * 972bde7: [HttpKernel] fixed the Kernel when the ClassLoader component is not available (closes #7406) - * f163226: fixed output of bag values - * 047212a: [Yaml] fixed handling an empty value - * 94a9cdc: [Routing][XML Loader] Add a possibility to set a default value to null - * 302d44f: [Console] fixed handling of "0" input on ask - * 383a84b: fixed handling of "0" input on ask - * 0f0c29c: [HttpFoundation] Fixed bug in key searching for NamespacedAttributeBag - * 7fc429f: [Form] DateTimeToRfc3339Transformer use proper transformation exteption in reverse transformation - * 9fcd2f6: [HttpFoundation] fixed the creation of sub-requests under some circumstances for IIS - * 8a9e898: Fix finding ACLs from ObjectIdentity's with different types - * a3826ab: #7531: [HttpKernel][Config] FileLocator adds NULL as global resource path - * 9d71ebe: Fix autocompletion of command names when namespaces conflict - * bec8ff1: Fix timeout in Process::stop method - * 3780fdb: Fix Process timeout - * 99256e4: [HttpKernel] Remove args from 5.3 stack traces to avoid filling log files, fixes #7259 - * e8cae94: fix overwriting of request's locale if attribute _locale is missing - * c4da2d9: [HttpFoundation] getClientIp is fixed. - -* 2.2.1 (2013-04-06) - - * 751abe1: Doctrine cannot handle bare random non-utf8 strings - * 673fd9b: idAsIndex should be true with a smallint or bigint id field. - * 64a1d39: Fixed long multibyte parameter logging in DbalLogger:startQuery - * 4cf06c1: Keep the file extension in the temporary copy and test that it exists (closes #7482) - * 64ac34d: [Security] fixed wrong interface - * 9875c4b: Added '@@' escaping strategy for YamlFileLoader and YamlDumper - * bbcdfe2: [Yaml] fixed bugs with folded scalar parsing - * 5afea04: [Form] made DefaultCsrfProvider using session_status() when available - * c928ddc: [HttpFoudantion] fixed Request::getPreferredLanguage() - * e6b7515: [DomCrawler] added support for query string with slash - * 633c051: Fixed invalid file path for hiddeninput.exe on Windows. - * 7ef90d2: fix xsd definition for strict-requirements - * 39445c5: [WebProfilerBundle] Fixed the toolbar styles to apply them in IE8 - * 601da45: [ClassLoader] fixed heredocs handling - * 17dc2ff: [HttpRequest] fixes Request::getLanguages() bug - * 67fbbac: [DoctrineBridge] Fixed non-utf-8 recognition - * e51432a: sub-requests are now created with the same class as their parent - * cc3a40e: [FrameworkBundle] changed temp kernel name in cache:clear - * d7a7434: [Routing] fix url generation for optional parameter having a null value - * ef53456: [DoctrineBridge] Avoids blob values to be logged by doctrine - * 6575df6: [Security] use current request attributes to generate redirect url? - * 7216cb0: [Validator] fix showing wrong max file size for upload errors - * c423f16: [2.1][TwigBridge] Fixes Issue #7342 in TwigBridge - * 7d87ecd: [FrameworkBundle] fixed cache:clear command's warmup - * 5ad4bd1: [TwigBridge] now enter/leave scope on Twig_Node_Module - * fe4cc24: [TwigBridge] fixed fixed scope & trans_default_domain node visitor - * fc47589: [BrowserKit] added ability to ignored malformed set-cookie header - * 602cdee: replace INF to PHP_INT_MAX inside Finder component. - * 5bc30bb: [Translation] added xliff loader/dumper with resname support - * 663c796: Property accessor custom array object fix - * 4f3771d: [2.2][HttpKernel] fixed wrong option name in FragmentHandler::fixOptions - * a735cbd: fix xargs pipe to work with spaces in dir names - * 15bf033: [FrameworkBundle] fix router debug command - * d16d193: [FramworkBundle] removed unused property of trans update command - * 523ef29: Fix warning for buildXml method - * 7241be9: [Finder] fixed a potential issue on Solaris where INF value is wrong (refs #7269) - * 1d3da29: [FrameworkBundle] avoids cache:clear to break if new/old folders already exist - * b9cdb9a: [HttpKernel] Fixed possible profiler token collision (closes #7272, closes #7171) - * d1f5d25: [FrameworkBundle] Fixes invalid serialized objects in cache - * c82c754: RedisProfilerStorage wrong db-number/index-number selected - * e86fefa: Unset loading[$id] in ContainerBuilder on exception - * 709518b: Default validation message translation fix. - * c0687cd: remove() should not use deprecated getParent() so it does not trigger deprecation internally - * 708c0d3: adjust routing tests to not use prefix in addCollection - * acff735: [Routing] trigger deprecation warning for deprecated features that will be removed in 2.3 - * 41ad9d8: [Routing] make xml loader more tolerant - * 73bead7: [ClassLoader] made DebugClassLoader idempotent - * a4ec677: [DomCrawler] Fix relative path handling in links - * 6681df0: [Console] fixed StringInput binding - * 5bf2f71: [Console] added deprecation annotation - * 8d9cd42: Routing issue with installation in a sub-directory ref: https://github.com/symfony/symfony/issues/7129 - * c97ee8d: [Translator] mention that the message id may also be an object that can be cast to string in TranslatorInterface and fix the IdentityTranslator that did not respect this - * 5a36b2d: [Translator] fix MessageCatalogueInterface::getFallbackCatalogue that can return null - -* 2.2.0 (2013-03-01) - - * 5b19c89: [Console] fixed unparsed StringInput tokens - * e92b76c: Mask PHP_AUTH_PW header in profiler - * bae83c7: [TwigBridge] fixed trans twig extractor - * f40adbc: [Finder] adds adapter selection/unselection capabilities - * 8f8ba38: [DomCrawler] fix handling of schemes by Link::getUri() - * 83382bc: [TwigBridge] fixed the translator extractor that were not trimming the text in trans tags (closes #7056) - * b1ea8e5: Fixed handling absent href attribute in base tag - * 83a61cf: fixed paths/notPaths regex for shell adapters - * 32c5bf7: fix issue 4911 - * 13b8ce0: Adds expandable globs support to shell adapters - * 850bd5a: [HttpFoundation] Fixed messed up headers - * 4ecc246: Fixes AppCache + ESI + Stopwatch problem - * 0690709: added a DebuClassLoader::findFile() method to make the wrapping less invasive - * da22926: [Validator] gracefully handle transChoice errors - * 635b1fc: StringInput resets the given options - -* 2.2.0-RC3 (2013-02-24) - - * b2080c4: [HttpFoundation] Remove Cache-Control when using https download via IE<9 (fixes #6750) - * b7bd630: [Form] Fixed TimeType not to render a "size" attribute in select tags - * 368f62f: Expanded fault-tolerance for unusual cookie dates - * 171cff0: [FrameworkBundle] Fix a BC for Hinclude global template - * 3e40c17: [HttpKernel] fixed locale management when exiting sub-requests - * 3933912: fixed HInclude renderer (closes #7113) - * 189fba6: Removed some leaking deprecation warning in the Form component - * d0e4b76: [HttpFoundation] fixed, overwritten CONTENT_TYPE - * 609636e: [Config] tweaked dumper to indent multi-line info - * 0eff68f: Fix REMOTE_ADDR for cached subrequests - * 54d7d25: [HttpKernel] hinclude fragment renderer must escape URIs properly to return valid html - * f842ae6: [FrameworkBundle] CSRF should be on by default - * cb319ac: [HttpKernel] added error display suppression when using the ErrorHandler (if not, errors are displayed twice, refs #6254) - * de0f7b7: [HttpFoundation] Added getter for httpMethodParameterOverride state diff --git a/CHANGELOG-2.3.md b/CHANGELOG-2.3.md deleted file mode 100644 index 2758f011f3cfd..0000000000000 --- a/CHANGELOG-2.3.md +++ /dev/null @@ -1,1056 +0,0 @@ -CHANGELOG for 2.3.x -=================== - -This changelog references the relevant changes (bug and security fixes) done -in 2.3 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.3.0...v2.3.1 - -* 2.3.42 (2016-05-30) - - * 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 #18864 [Console][DX] Fixed ambiguous error message when using a duplicate option shortcut (peterrehm) - * bug #18844 [Yaml] fix exception contexts (xabbuh) - * bug #18840 [Yaml] properly handle unindented collections (xabbuh) - * bug #18839 People - person singularization (Keeo) - * bug #18828 [Yaml] chomp newlines only at the end of YAML documents (xabbuh) - * bug #18635 [Console] Prevent fatal error when calling Command::getHelper without helperSet (chalasr) - * bug #18761 [Form] Modified iterator_to_array's 2nd parameter to false in ViolationMapper (issei-m) - -* 2.3.41 (2016-05-09) - - * security #18733 limited the maximum length of a submitted username (fabpot) - * bug #18709 [DependencyInjection] top-level anonymous services must be public (xabbuh) - -* 2.3.40 (2016-04-29) - - * bug #18246 [DependencyInjection] fix ambiguous services schema (backbone87) - * bug #18603 [PropertyAccess] ->getValue() should be read-only (nicolas-grekas) - * bug #18280 [Routing] add query param if value is different from default (Tobion) - * bug #18515 [Filesystem] Better error handling in remove() (nicolas-grekas) - * bug #18449 [PropertyAccess] Fix regression (nicolas-grekas) - * 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 #18265 Optimize ReplaceAliasByActualDefinitionPass (ajb-in) - * bug #18358 [Form] NumberToLocalizedStringTransformer should return floats when possible (nicolas-grekas) - * bug #17926 [DependencyInjection] Enable alias for service_container (hason) - * bug #18336 [Debug] Fix handling of php7 throwables (nicolas-grekas) - * bug #18312 [ClassLoader] Fix storing not-found classes in APC cache (nicolas-grekas) - * bug #18255 [HttpFoundation] Fix support of custom mime types with parameters (Ener-Getick) - * bug #18259 [PropertyAccess] Backport fixes from 2.7 (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 #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) - -* 2.3.39 (2016-03-13) - - * 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 #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 #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 #17894 [FrameworkBundle] Fix a regression in handling absolute template paths (jakzal) - * 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 #17976 [WebProfilerBundle] fix debug toolbar rendering by removing inadvertently added links (craue) - * bug #17971 Variadic controller params (NiR-, fabpot) - * bug #17925 [Bridge] The WebProcessor now forwards the client IP (magnetik) - -* 2.3.38 (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 #17542 ChoiceFormField of type "select" could be "disabled" (bouland) - * bug #17602 [HttpFoundation] Fix BinaryFileResponse incorrect behavior with if-range header (bburnichon) - * bug #17914 [Console] Fix escaping of trailing backslashes (nicolas-grekas) - * bug #17074 Fix constraint validator alias being required (Triiistan) - * 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 #17835 [Yaml] fix default timezone to be UTC (xabbuh) - * bug #17823 [DependencyInjection] fix dumped YAML string (xabbuh) - * 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 #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 #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 #17596 [Translation] Add resources from fallback locale to parent catalogue (c960657) - * bug #16956 [DependencyInjection] XmlFileLoader: enforce tags to have a name (xabbuh) - * bug #16265 [BrowserKit] Corrected HTTP_HOST logic (Naktibalda) - * bug #17555 [DependencyInjection] resolve aliases in factory services (xabbuh) - * 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 #16736 [Request] Ignore invalid IP addresses sent by proxies (GromNaN) - * 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 #17505 sort bundles in config:dump-reference command (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 #17423 [Process] Use stream based storage to avoid memory issues (romainneutron) - * bug #17373 [SecurityBundle] fix SecureRandom service constructor args (Tobion) - * 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.3.37 (2016-01-14) - - * security #17359 do not ship with a custom rng implementation (xabbuh, fabpot) - * bug #17326 [Console] Display console application name even when no version set (polc) - * 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 #17287 [HttpKernel] Forcing string comparison on query parameters sort in UriSigner (Tim van Densen) - * bug #17278 [FrameworkBundle] Add case in Kernel directory guess for PHPUnit (tgalopin) - * 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) - * bug #17177 [Process] Fix potential race condition leading to transient tests (nicolas-grekas) - -* 2.3.36 (2015-12-26) - - * bug #16864 [Yaml] fix indented line handling in folded blocks (xabbuh) - * bug #16826 Embedded identifier support (mihai-stancu) - * 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 #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 #17057 [FrameworkBundle][HttpKernel] the finder is required to discover bundle commands (xabbuh) - * bug #16915 [Process] Enhance compatiblity with --enable-sigchild (nicolas-grekas) - * bug #16829 [FrameworkBundle] prevent cache:clear creating too long paths (Tobion) - * bug #16870 [FrameworkBundle] Disable the server:run command when Process component is missing (gnugat, xabbuh) - * bug #16799 Improve error message for undefined DIC aliases (mpdude) - * 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 #16695 [SecurityBundle] disable the init:acl command if ACL is not used (Tobion) - * 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) - -* 2.3.35 (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 #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 #16462 [PropertyAccess] Fix dynamic property accessing. (dunglas) - * 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.3.34 (2015-10-27) - - * bug #16288 [Process] Inherit env vars by default in PhpProcess (nicolas-grekas) - * bug #16302 [DoctrineBridge] Fix required guess of boolean fields (enumag) - * bug #16177 [HttpFoundation] Fixes /0 subnet handling in IpUtils (ultrafez) - * 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 #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 #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 #16111 Throw exception if tempnam returns false in ProcessPipes (pierredup) - * 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 #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 #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 #15846 [FrameworkBundle] Advanced search templates of bundles (yethee) - * bug #15895 [Security] Allow user providers to be defined in many files (lyrixx) - -* 2.3.33 (2015-09-25) - - * bug #15821 [EventDispatcher] fix memory leak in getListeners (Tobion) - * bug #15826 [Finder] Optimize the hot-path (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) - * bug #13794 [DomCrawler] Invalid uri created from forms if base tag present (danez) - * bug #15637 Use ObjectManager interface instead of EntityManager (gnat42) - * 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 #15718 Fix that two DirectoryResources with different patterns would be deduplicated (mpdude) - * 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 #15729 [Kernel] Integer version constants (Tobion) - * bug #15527 [Translator][fallback catalogues] fixed circular reference. (aitboudad) - -* 2.3.32 (2015-09-01) - - * bug #15601 [console] Use the description when no help is available (Nicofuma) - * bug #15603 [HttpKernel] Do not normalize the kernel root directory path #15567 (leofeyer) - * bug #15428 Fix the validation of form resources to register the default theme (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 #15611 [Translation][Xliff Loader] Support omitting the node in an .xlf file. (leofeyer) - * 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 #15535 made Symfony compatible with both Twig 1.x and 2.x (fabpot) - * bug #14372 [DoctrineBridge][Form] fix EntityChoiceList when indexing by primary foreign key (giosh94mhz) - * 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 #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 #15380 do not dump leading backslashes in class names (xabbuh) - * bug #15376 [ClassMapGenerator] Skip ::class constant (WouterJ) - * bug #15170 [Config] type specific check for emptiness (xabbuh) - * bug #15411 Fix the handling of null as locale in the stub intl classes (stof) - * 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 #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 #15330 [Console] Fix console output with closed stdout (jakzal) - * bug #15326 [Security] fix check for empty usernames (xabbuh) - * 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) - -* 2.3.31 (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 #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 #15163 Update DateTimeToArrayTransformer.php (zhil) - * bug #15150 [Translation] Azerbaijani language pluralization rule is wrong (shehi) - * 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 #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 #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 #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 #14964 [bugfix][MonologBridge] WebProcessor: passing $extraFields to BaseWebProcessor (MacDada) - * 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 #14897 Allow new lines in Messages translated with transchoice() (replacement for #14867) (azine) - * bug #14895 [Form] Support DateTimeImmutable in transform() (c960657) - * 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) - -* 2.3.30 (2015-05-30) - - * bug #14262 [REVERTED] [TwigBundle] Refresh twig paths when resources change. (aitboudad) - -* 2.3.29 (2015-05-26) - - * security #14759 CVE-2015-4050 [HttpKernel] Do not call the FragmentListener if _controller is already defined (jakzal) - * 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 #14698 Fix HTML escaping of to-source links (nicolas-grekas) - * bug #14690 [HttpFoundation] IpUtils::checkIp4() should allow `/0` networks (zerkms) - * 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 #14633 [EventDispatcher] make listeners removable from an executed listener (xabbuh) - -* 2.3.28 (2015-05-10) - - * bug #14266 [HttpKernel] Check if "symfony/proxy-manager-bridge" package is installed (hason) - * bug #14501 [ProxyBridge] Fix proxy classnames generation (xphere) - * bug #14498 [FrameworkBundle] Added missing log in server:run command (lyrixx) - * 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 #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 #14402 [FrameworkBundle][Translation] Check for 'xlf' instead of 'xliff' (xelaris) - * bug #14272 [FrameworkBundle] Workaround php -S ignoring auto_prepend_file (nicolas-grekas) - * bug #14345 [FrameworkBundle] Fix Routing\DelegatingLoader resiliency to fatal errors (nicolas-grekas) - * 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 #14268 [Translator] Cache does not take fallback locales into consideration (sf2.3) (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) - -* 2.3.27 (2015-04-01) - - * security #14167 CVE-2015-2308 (nicolas-grekas) - * security #14166 CVE-2015-2309 (neclimdul) - * bug #14010 Replace GET parameters when changed in form (WouterJ) - * bug #13991 [Dependency Injection] Improve PhpDumper Performance for huge Containers (BattleRattle) - * bug #13997 [2.3+][Form][DoctrineBridge] Improved loading of entities and documents (guilhermeblanco) - * bug #13953 [Translation][MoFileLoader] fixed load empty translation. (aitboudad) - * bug #13912 [DependencyInjection] Highest precedence for user parameters (lyrixx) - -* 2.3.26 (2015-03-17) - - * bug #13927 Fixing wrong variable name from #13519 (weaverryan) - * bug #13519 [DependencyInjection] fixed service resolution for factories (fabpot) - * bug #13901 [Bundle] Fix charset config (nicolas-grekas, bamarni) - * bug #13911 [HttpFoundation] MongoDbSessionHandler::read() now checks for valid session age (bzikarsky) - * bug #13890 Fix XSS in Debug exception handler (fabpot) - * 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 #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 #13618 [PropertyAccess] Fixed invalid feedback -> foodback singularization (WouterJ) - * 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 #13607 [Console] Fixed output bug, if escaped string in a formatted string. (tronsha) - * 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 #13375 [YAML] Fix one-liners to work with multiple new lines (Alex Pott) - * bug #13545 fixxed order of usage (OskarStark) - * bug #13567 [Routing] make host matching case-insensitive (Tobion) - -* 2.3.25 (2015-01-30) - - * bug #13528 [Validator] reject ill-formed strings (nicolas-grekas) - * bug #13525 [Validator] UniqueEntityValidator - invalidValue fixed. (Dawid Sajdak) - * 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 #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 #13371 fix missing comma in YamlDumper (garak) - * bug #13365 [HttpFoundation] Make use of isEmpty() method (xelaris) - * 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) - -* 2.3.24 (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 #13145 [DomCrawler] Fix behaviour with tag (dkop, WouterJ) - * 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 #13075 [Config] fix error handler restoration in test (nicolas-grekas) - * 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 #12761 [Filesystem] symlink use RealPath instead LinkTarget (aitboudad) - * bug #12855 [DependencyInjection] Perf php dumper (nicolas-grekas) - * bug #12894 [FrameworkBundle][Template name] avoid error message for the shortcut n... (aitboudad) - * bug #12858 [ClassLoader] Fix undefined index in ClassCollectionLoader (szicsu) - -* 2.3.23 (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 #12137 [FrameworkBundle] cache:clear command fills *.php.meta files with wrong data (Strate) - -* 2.3.22 (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 #12393 [DependencyInjection] inlined factory not referenced (boekkooi) - * bug #12436 [Filesystem] Fixed case for empty folder (yosmanyga) - * 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 #12316 Break infinite loop while resolving aliases (chx) - * bug #12313 [Security][listener] change priority of switchuser (aitboudad) - -* 2.3.21 (2014-10-24) - - * bug #11696 [Form] Fix #11694 - Enforce options value type check in some form types (kix) - * 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 #11998 [Intl] Integrated ICU data into Intl component #2 (webmozart) - * bug #11920 [Intl] Integrated ICU data into Intl component #1 (webmozart) - -* 2.3.20 (2014-09-28) - - * bug #9453 [Form][DateTime] Propagate invalid_message & invalid_message_parameters to date & time (egeloen) - * 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 #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 #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 #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.3.19 (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 #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 #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.3.18 (2014-07-15) - - * [Security] Forced validate of locales passed to the translator - * 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.3.17 (2014-07-07) - - * 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.3.16 (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) - * 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.3.15 (2014-05-22) - - * reverted #10908 - -* 2.3.14 (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 #10937 [HttpKernel] Fix "absolute path" when we look to the cache directory (BenoitLeveque) - * bug #10908 [HttpFoundation] implement session locking for PDO (Tobion) - * 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 #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) - -* 2.3.13 (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 #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 #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 #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.3.12 (2014-04-03) - - * 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 #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 #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 #10351 [HttpKernel] fix stripComments() normalizing new-lines (sstok) - * bug #10348 Update FileLoader to fix issue #10339 (msumme) - -* 2.3.11 (2014-02-27) - - * 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) - -* 2.3.10 (2014-02-12) - - * 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) - * 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 #10119 [Validator] Minor fix in XmlFileLoader (florianv) - * bug #10078 [BrowserKit] add non-standard port to HTTP_HOST server param (kbond) - * bug #10091 [Translation] Update PluralizationRules.php (guilhermeblanco) - * bug #10053 [Form] fixed allow render 0 numeric input value (dczech) - * 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 #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 #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.3.9 (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 #9908 [HttpFoundation] Throw proper exception when invalid data is passed to JsonResponse class (stloyd) - * bug #9902 [Security] fixed pre/post authentication checks (fabpot) - * 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 #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) - * 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) - -* 2.3.8 (2013-12-16) - - * 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 #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) - * bug #9665 [Bridge/Doctrine] ORMQueryBuilderLoader - handled the scenario when no entity manager is passed with closure query builder (jakzal) - * 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 #9637 [Validator] Replaced inexistent interface (jakzal) - * bug #9605 Adjusting CacheClear Warmup method to namespaced kernels (rdohms) - * bug #9610 Container::camelize also takes backslashes into consideration (ondrejmirtes) - * 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) - * 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 #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) - -* 2.3.7 (2013-11-14) - - * bug #9499 Request::overrideGlobals() may call invalid ini value (denkiryokuhatsuden) - * 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 #9431 [DependencyInjection] fixed YamlDumper did not make services private. (realityking) - * 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 #9412 [HttpFoundation] added content length header to BinaryFileResponse (kbond) - * bug #9395 [HttpKernel] fixed memory limit display in MemoryDataCollector (hhamon) - * bug #9388 [Form] Fixed: The "data" option is taken into account even if it is NULL (bschussek) - * bug #9391 [Serializer] Fixed the error handling when decoding invalid XML to avoid a Warning (stof) - * 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 #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 #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 #9308 [DoctrineBridge] Loosened CollectionToArrayTransformer::transform() to accept arrays (bschussek) - * 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) - -* 2.3.6 (2013-10-10) - - * [Security] limited the password length passed to encoders - * 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 #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) - * bug #9190 Fix: duplicate usage of Symfony\Component\HttpFoundation\Response (realsim) - * bug #9188 [Form] add support for Length and Range constraint in ValidatorTypeGuesser (franek) - * bug #8809 [Form] enforce correct timezone (Burgov) - * bug #9169 Fixed client insulation when using the terminable event (fabpot) - * bug #9154 Fix problem with Windows file links (backslash in JavaScript string) (fabpot) - * bug #9153 [DependencyInjection] Prevented inlining of lazy loaded private service definitions (jakzal) - * bug #9103 [HttpFoundation] Header `HTTP_X_FORWARDED_PROTO` can contain various values (stloyd) - -* 2.3.5 (2013-09-27) - - * 8980954: bugix: CookieJar returns cookies with domain "domain.com" for domain "foodomain.com" - * bb59ac2: fixed HTML5 form attribute handling XPath query - * 3108c71: [Locale] added support for the position argument to NumberFormatter::parse() - * 0774c79: [Locale] added some more stubs for the number formatter - * e5282e8: [DomCrawler]Crawler guess charset from html - * 0e80d88: fixes RequestDataCollector bug, visible when used on Drupal8 - * c8d0342: [Console] fixed exception rendering when nested styles - * a47d663: [Console] fixed the formatter for single-char tags - * c6c35b3: [Console] Escape exception message during the rendering of an exception - * 04e730e: [DomCrawler] fixed HTML5 form attribute handling - * 0e437c5: [BrowserKit] Fixed the handling of parameters when redirecting - * d84df4c: [Process] Properly close pipes after a Process::stop call - * b3ae29d: fixed bytes conversion when used on 32-bits systems - * a273e79: [Form] Fixed: "required" attribute is not added to + - - - + + -
- {% block body %}{% endblock %} -
- + {% block body %}{% endblock %} + {{ include('@Twig/base_js.html.twig') }} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php index 4dfb100e34e84..417c83cdecc49 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php @@ -13,6 +13,7 @@ use Symfony\Bundle\TwigBundle\Controller\PreviewErrorController; use Symfony\Bundle\TwigBundle\Tests\TestCase; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -21,8 +22,6 @@ class PreviewErrorControllerTest extends TestCase { public function testForwardRequestToConfiguredController() { - $self = $this; - $request = Request::create('whatever'); $response = new Response(''); $code = 123; @@ -33,14 +32,13 @@ public function testForwardRequestToConfiguredController() ->expects($this->once()) ->method('handle') ->with( - $this->callback(function (Request $request) use ($self, $logicalControllerName, $code) { - $self->assertEquals($logicalControllerName, $request->attributes->get('_controller')); + $this->callback(function (Request $request) use ($logicalControllerName, $code) { + $this->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')); + $this->assertInstanceOf(FlattenException::class, $exception); + $this->assertEquals($code, $exception->getStatusCode()); + $this->assertFalse($request->attributes->get('showException')); return true; }), diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php index 23160c4071d1d..6ce77c54970c0 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php @@ -22,15 +22,20 @@ public function testProcessDoesNotDropExistingFileLoaderMethodCalls() { $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); + $container->setParameter('kernel.root_dir', __DIR__); $container->register('twig.app_variable', '\Symfony\Bridge\Twig\AppVariable'); $container->register('templating', '\Symfony\Bundle\TwigBundle\TwigEngine'); + $container->register('twig.extension.yaml'); + $container->register('twig.extension.debug.stopwatch'); + $container->register('twig.extension.expression'); $nativeTwigLoader = new Definition('\Twig\Loader\FilesystemLoader'); $nativeTwigLoader->addMethodCall('addPath', array()); $container->setDefinition('twig.loader.native_filesystem', $nativeTwigLoader); $filesystemLoader = new Definition('\Symfony\Bundle\TwigBundle\Loader\FilesystemLoader'); + $filesystemLoader->setArguments(array(null, null, null)); $filesystemLoader->addMethodCall('addPath', array()); $container->setDefinition('twig.loader.filesystem', $filesystemLoader); 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 deleted file mode 100644 index fbd2b83f133e3..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-form-resources-only.php +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index dc36daf8cf0c2..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/legacy-merge-form-resources-with-form-themes.php +++ /dev/null @@ -1,13 +0,0 @@ -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/legacy-form-resources-only.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-form-resources-only.xml deleted file mode 100644 index 9aa2486c5f04f..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-form-resources-only.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - 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 deleted file mode 100644 index efe83ea7b78e1..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/legacy-merge-form-resources-with-form-themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - 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/legacy-form-resources-only.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-form-resources-only.yml deleted file mode 100644 index bb1a75f603517..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-form-resources-only.yml +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 8fc9f92e869d5..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/legacy-merge-form-resources-with-form-themes.yml +++ /dev/null @@ -1,7 +0,0 @@ -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 f385ec04fd28c..cdd686029423a 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection; +use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass; use Symfony\Bundle\TwigBundle\DependencyInjection\TwigExtension; use Symfony\Bundle\TwigBundle\Tests\TestCase; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; @@ -23,42 +25,6 @@ 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(); @@ -66,7 +32,7 @@ public function testLoadEmptyConfiguration() $container->loadFromExtension('twig', array()); $this->compileContainer($container); - $this->assertEquals('Twig\Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file'); + $this->assertEquals('Twig\Environment', $container->getDefinition('twig')->getClass(), '->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'); @@ -87,7 +53,7 @@ public function testLoadFullConfiguration($format) $this->loadFromFile($container, 'full', $format); $this->compileContainer($container); - $this->assertEquals('Twig\Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file'); + $this->assertEquals('Twig\Environment', $container->getDefinition('twig')->getClass(), '->load() loads the twig.xml file'); // Form resources $resources = $container->getParameter('twig.form.resources'); @@ -98,17 +64,17 @@ public function testLoadFullConfiguration($format) $calls = $container->getDefinition('twig')->getMethodCalls(); $this->assertEquals('app', $calls[0][1][0], '->load() registers services as Twig globals'); $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'); - $this->assertEquals('@qux', $calls[2][1][1], '->load() allows escaping of service identifiers'); - $this->assertEquals('pi', $calls[3][1][0], '->load() registers variables as Twig globals'); - $this->assertEquals(3.14, $calls[3][1][1], '->load() registers variables as Twig globals'); + $this->assertEquals('foo', $calls[2][1][0], '->load() registers services as Twig globals'); + $this->assertEquals(new Reference('bar'), $calls[2][1][1], '->load() registers services as Twig globals'); + $this->assertEquals('baz', $calls[3][1][0], '->load() registers variables as Twig globals'); + $this->assertEquals('@qux', $calls[3][1][1], '->load() allows escaping of service identifiers'); + $this->assertEquals('pi', $calls[4][1][0], '->load() registers variables as Twig globals'); + $this->assertEquals(3.14, $calls[4][1][1], '->load() registers variables as Twig globals'); // Yaml and Php specific configs if (in_array($format, array('yml', 'php'))) { - $this->assertEquals('bad', $calls[4][1][0], '->load() registers variables as Twig globals'); - $this->assertEquals(array('key' => 'foo'), $calls[4][1][1], '->load() registers variables as Twig globals'); + $this->assertEquals('bad', $calls[5][1][0], '->load() registers variables as Twig globals'); + $this->assertEquals(array('key' => 'foo'), $calls[5][1][1], '->load() registers variables as Twig globals'); } // Twig options @@ -189,7 +155,7 @@ public function testGlobalsWithDifferentTypesAndValues() $this->compileContainer($container); $calls = $container->getDefinition('twig')->getMethodCalls(); - foreach (array_slice($calls, 1) as $call) { + foreach (array_slice($calls, 2) as $call) { $this->assertEquals(key($globals), $call[1][0]); $this->assertSame(current($globals), $call[1][1]); @@ -282,11 +248,36 @@ public function stopwatchExtensionAvailabilityProvider() ); } + public function testRuntimeLoader() + { + $container = $this->createContainer(); + $container->registerExtension(new TwigExtension()); + $container->loadFromExtension('twig', array()); + $container->setParameter('kernel.environment', 'test'); + $container->setParameter('debug.file_link_format', 'test'); + $container->setParameter('foo', 'FooClass'); + $container->register('http_kernel', 'FooClass'); + $container->register('templating.locator', 'FooClass'); + $container->register('templating.name_parser', 'FooClass'); + $container->register('foo', '%foo%')->addTag('twig.runtime'); + $container->addCompilerPass(new RuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->getCompilerPassConfig()->setRemovingPasses(array()); + $container->compile(); + + $loader = $container->getDefinition('twig.runtime_loader'); + $args = $container->getDefinition((string) $loader->getArgument(0))->getArgument(0); + $this->assertArrayHasKey('Symfony\Component\Form\FormRenderer', $args); + $this->assertArrayHasKey('FooClass', $args); + $this->assertEquals('twig.form.renderer', $args['Symfony\Component\Form\FormRenderer']->getValues()[0]); + $this->assertEquals('foo', $args['FooClass']->getValues()[0]); + } + private function createContainer() { $container = new ContainerBuilder(new ParameterBag(array( 'kernel.cache_dir' => __DIR__, 'kernel.root_dir' => __DIR__.'/Fixtures', + 'kernel.project_dir' => __DIR__, 'kernel.charset' => 'UTF-8', 'kernel.debug' => false, 'kernel.bundles' => array( diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php deleted file mode 100644 index 7ef380aaa6d56..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/Extension/LegacyAssetsExtensionTest.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * 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 -{ - protected function setUp() - { - if (!class_exists('Symfony\Component\Templating\Helper\CoreAssetsHelper')) { - $this->markTestSkipped('The CoreAssetsHelper class does only exist with symfony/templating < 3.0 installed.'); - } - } - - /** - * @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->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); - $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 index 12d2ddfbe7a37..83bedda6773c3 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php @@ -90,6 +90,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) $loader->load(function ($container) { $container->loadFromExtension('framework', array( 'secret' => '$ecret', + 'form' => array('enabled' => false), )); }); @@ -99,6 +100,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) 'secret' => '$ecret', 'templating' => array('engines' => array('twig')), 'router' => array('resource' => '%kernel.root_dir%/Resources/config/empty_routing.yml'), + 'form' => array('enabled' => false), )); }); } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php index d07e0e946b2e7..3cf0268203050 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php @@ -62,6 +62,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) $loader->load(function ($container) { $container->loadFromExtension('framework', array( 'secret' => '$ecret', + 'form' => array('enabled' => false), )); }); } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/TokenParser/LegacyRenderTokenParserTest.php b/src/Symfony/Bundle/TwigBundle/Tests/TokenParser/LegacyRenderTokenParserTest.php deleted file mode 100644 index 5cba98cd45ff9..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/TokenParser/LegacyRenderTokenParserTest.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\Tests\TokenParser; - -use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Bundle\TwigBundle\TokenParser\RenderTokenParser; -use Symfony\Bundle\TwigBundle\Node\RenderNode; -use Twig\Environment; -use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\ConstantExpression; -use Twig\Parser; -use Twig\Source; - -/** - * @group legacy - */ -class LegacyRenderTokenParserTest extends TestCase -{ - /** - * @dataProvider getTestsForRender - */ - public function testCompile($source, $expected) - { - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), array('cache' => false, 'autoescape' => false, 'optimizations' => 0)); - $env->addTokenParser(new RenderTokenParser()); - $stream = $env->tokenize(new Source($source, '')); - $parser = new Parser($env); - - $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)); - } - - public function getTestsForRender() - { - return array( - array( - '{% render "foo" %}', - new RenderNode( - new ConstantExpression('foo', 1), - new ArrayExpression(array(), 1), - 1, - 'render' - ), - ), - array( - '{% render "foo", {foo: 1} %}', - new RenderNode( - new ConstantExpression('foo', 1), - new ArrayExpression(array( - new ConstantExpression('foo', 1), - new ConstantExpression('1', 1), - ), 1), - 1, - 'render' - ), - ), - ); - } -} diff --git a/src/Symfony/Bundle/TwigBundle/TokenParser/RenderTokenParser.php b/src/Symfony/Bundle/TwigBundle/TokenParser/RenderTokenParser.php deleted file mode 100644 index cef083df3dc42..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/TokenParser/RenderTokenParser.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\TokenParser; - -use Symfony\Bundle\TwigBundle\Node\RenderNode; -use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Node; -use Twig\Token; -use Twig\TokenParser\AbstractTokenParser; - -/** - * Token Parser for the render tag. - * - * @author Fabien Potencier - * - * @deprecated since version 2.2, to be removed in 3.0. - */ -class RenderTokenParser extends AbstractTokenParser -{ - /** - * Parses a token and returns a node. - * - * @param Token $token - * - * @return Node - */ - public function parse(Token $token) - { - $expr = $this->parser->getExpressionParser()->parseExpression(); - - // options - if ($this->parser->getStream()->test(Token::PUNCTUATION_TYPE, ',')) { - $this->parser->getStream()->next(); - - $options = $this->parser->getExpressionParser()->parseExpression(); - } else { - $options = new ArrayExpression(array(), $token->getLine()); - } - - $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - - return new RenderNode($expr, $options, $token->getLine(), $this->getTag()); - } - - /** - * Gets the tag name associated with this token parser. - * - * @return string The tag name - */ - public function getTag() - { - return 'render'; - } -} diff --git a/src/Symfony/Bundle/TwigBundle/TwigBundle.php b/src/Symfony/Bundle/TwigBundle/TwigBundle.php index 7b4a2053f0f16..21a39a12373a9 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigBundle.php +++ b/src/Symfony/Bundle/TwigBundle/TwigBundle.php @@ -11,12 +11,15 @@ namespace Symfony\Bundle\TwigBundle; +use Symfony\Component\Console\Application; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\TwigEnvironmentPass; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\TwigLoaderPass; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExceptionListenerPass; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExtensionPass; +use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass; /** * Bundle. @@ -33,5 +36,11 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new TwigEnvironmentPass()); $container->addCompilerPass(new TwigLoaderPass()); $container->addCompilerPass(new ExceptionListenerPass()); + $container->addCompilerPass(new RuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING); + } + + public function registerCommands(Application $application) + { + // noop } } diff --git a/src/Symfony/Bundle/TwigBundle/TwigDefaultEscapingStrategy.php b/src/Symfony/Bundle/TwigBundle/TwigDefaultEscapingStrategy.php deleted file mode 100644 index c5429ee89a5b1..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/TwigDefaultEscapingStrategy.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -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 -{ - public static function guess($filename) - { - // remove .twig - $filename = substr($filename, 0, -5); - - // get the format - $format = substr($filename, strrpos($filename, '.') + 1); - - if ('js' === $format) { - return 'js'; - } - - if ('txt' === $format) { - return false; - } - - return 'html'; - } -} diff --git a/src/Symfony/Bundle/TwigBundle/TwigEngine.php b/src/Symfony/Bundle/TwigBundle/TwigEngine.php index 35e67b8735f72..cc13c280b10ff 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigEngine.php +++ b/src/Symfony/Bundle/TwigBundle/TwigEngine.php @@ -19,7 +19,6 @@ use Symfony\Component\Config\FileLocatorInterface; use Twig\Environment; use Twig\Error\Error; -use Twig\FileExtensionEscapingStrategy; /** * This engine renders Twig templates. @@ -37,28 +36,6 @@ public function __construct(Environment $environment, TemplateNameParserInterfac $this->locator = $locator; } - /** - * @deprecated since version 2.7, to be removed in 3.0. - * Inject the escaping strategy on Twig 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('Twig\Extension\EscaperExtension')->setDefaultStrategy($strategy); - } - - /** - * @deprecated since version 2.7, to be removed in 3.0. - * Use the 'name' strategy instead. - */ - public function guessDefaultEscapingStrategy($name) - { - @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 FileExtensionEscapingStrategy::guess($name); - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 193d4518521f2..ea7daf714f112 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -16,24 +16,30 @@ } ], "require": { - "php": ">=5.3.9", - "symfony/asset": "~2.7|~3.0.0", - "symfony/twig-bridge": "~2.7|~3.0.0", - "symfony/http-foundation": "~2.5|~3.0.0", - "symfony/http-kernel": "~2.7.23|^2.8.16", + "php": "^7.1.3", + "symfony/config": "~3.4|~4.0", + "symfony/twig-bridge": "~3.4|~4.0", + "symfony/http-foundation": "~3.4|~4.0", + "symfony/http-kernel": "~3.4|~4.0", "twig/twig": "~1.34|~2.4" }, "require-dev": { - "symfony/stopwatch": "~2.2|~3.0.0", - "symfony/dependency-injection": "^2.6.6|~3.0.0", - "symfony/expression-language": "~2.4|~3.0.0", - "symfony/config": "~2.8|~3.0.0", - "symfony/finder": "^2.0.5", - "symfony/routing": "~2.1|~3.0.0", - "symfony/templating": "~2.1|~3.0.0", - "symfony/yaml": "~2.3|~3.0.0", - "symfony/framework-bundle": "~2.7|~3.0.0", - "doctrine/annotations": "~1.0" + "symfony/asset": "~3.4|~4.0", + "symfony/stopwatch": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/finder": "~3.4|~4.0", + "symfony/form": "~3.4|~4.0", + "symfony/routing": "~3.4|~4.0", + "symfony/templating": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0", + "symfony/framework-bundle": "~3.4|~4.0", + "symfony/web-link": "~3.4|~4.0", + "doctrine/annotations": "~1.0", + "doctrine/cache": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" }, "autoload": { "psr-4": { "Symfony\\Bundle\\TwigBundle\\": "" }, @@ -44,7 +50,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index 5a319e7eff784..260dbdae05c67 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -1,6 +1,22 @@ CHANGELOG ========= +4.0.0 +----- + + * removed the `WebProfilerExtension::dumpValue()` method + * removed the `getTemplates()` method of the `TemplateManager` class in favor of the ``getNames()`` method + +3.1.0 +----- + + * added information about redirected and forwarded requests to the profiler + +3.0.0 +----- + + * removed profiler:import and profiler:export commands + 2.8.0 ----- diff --git a/src/Symfony/Bundle/WebProfilerBundle/Command/ExportCommand.php b/src/Symfony/Bundle/WebProfilerBundle/Command/ExportCommand.php deleted file mode 100644 index e405739581250..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Command/ExportCommand.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\WebProfilerBundle\Command; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\HttpKernel\Profiler\Profiler; - -/** - * Exports a profile. - * - * @deprecated since version 2.8, to be removed in 3.0. - * - * @author Fabien Potencier - */ -class ExportCommand extends Command -{ - private $profiler; - - public function __construct(Profiler $profiler = null) - { - $this->profiler = $profiler; - - parent::__construct(); - } - - /** - * {@inheritdoc} - */ - public function isEnabled() - { - if (null === $this->profiler) { - return false; - } - - return parent::isEnabled(); - } - - protected function configure() - { - $this - ->setName('profiler:export') - ->setDescription('[DEPRECATED] Exports a profile') - ->setDefinition(array( - new InputArgument('token', InputArgument::REQUIRED, 'The profile token'), - )) - ->setHelp(<<<'EOF' -The %command.name% command exports a profile to the standard output: - - php %command.full_name% profile_token -EOF - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $formatter = $this->getHelper('formatter'); - - $output->writeln($formatter->formatSection('warning', 'The profiler:export command is deprecated since version 2.8 and will be removed in 3.0', 'comment')); - - $token = $input->getArgument('token'); - - if (!$profile = $this->profiler->loadProfile($token)) { - throw new \LogicException(sprintf('Profile with token "%s" does not exist.', $token)); - } - - $output->writeln($this->profiler->export($profile), OutputInterface::OUTPUT_RAW); - } -} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Command/ImportCommand.php b/src/Symfony/Bundle/WebProfilerBundle/Command/ImportCommand.php deleted file mode 100644 index 1371a1b89ed58..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Command/ImportCommand.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\WebProfilerBundle\Command; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\HttpKernel\Profiler\Profiler; - -/** - * Imports a profile. - * - * @deprecated since version 2.8, to be removed in 3.0. - * - * @author Fabien Potencier - */ -class ImportCommand extends Command -{ - private $profiler; - - public function __construct(Profiler $profiler = null) - { - $this->profiler = $profiler; - - parent::__construct(); - } - - /** - * {@inheritdoc} - */ - public function isEnabled() - { - if (null === $this->profiler) { - return false; - } - - return parent::isEnabled(); - } - - protected function configure() - { - $this - ->setName('profiler:import') - ->setDescription('[DEPRECATED] Imports a profile') - ->setDefinition(array( - new InputArgument('filename', InputArgument::OPTIONAL, 'The profile path'), - )) - ->setHelp(<<<'EOF' -The %command.name% command imports a profile: - - php %command.full_name% profile_filepath - -You can also pipe the profile via STDIN: - - cat profile_file | php %command.full_name% -EOF - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $formatter = $this->getHelper('formatter'); - - $output->writeln($formatter->formatSection('warning', 'The profiler:import command is deprecated since version 2.8 and will be removed in 3.0', 'comment')); - - $data = ''; - if ($input->getArgument('filename')) { - $data = file_get_contents($input->getArgument('filename')); - } else { - if (0 !== ftell(STDIN)) { - throw new \RuntimeException('Please provide a filename or pipe the profile to STDIN.'); - } - - while (!feof(STDIN)) { - $data .= fread(STDIN, 1024); - } - } - - if (!$profile = $this->profiler->import($data)) { - throw new \LogicException('The profile already exists in the database.'); - } - - $output->writeln(sprintf('Profile "%s" has been successfully imported.', $profile->getToken())); - } -} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index 5b0e81a353ed3..7ebd04b574c22 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; +use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -34,23 +35,29 @@ class ProfilerController private $twig; private $templates; private $toolbarPosition; + private $cspHandler; + private $baseDir; /** * Constructor. * - * @param UrlGeneratorInterface $generator The URL Generator - * @param Profiler $profiler The profiler - * @param Environment $twig The twig environment - * @param array $templates The templates - * @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration) + * @param UrlGeneratorInterface $generator The URL Generator + * @param Profiler $profiler The profiler + * @param Environment $twig The twig environment + * @param array $templates The templates + * @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration) + * @param ContentSecurityPolicyHandler $cspHandler The Content-Security-Policy handler + * @param string $baseDir The project root directory */ - public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, Environment $twig, array $templates, $toolbarPosition = 'bottom') + public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, Environment $twig, array $templates, $toolbarPosition = 'bottom', ContentSecurityPolicyHandler $cspHandler = null, $baseDir = null) { $this->generator = $generator; $this->profiler = $profiler; $this->twig = $twig; $this->templates = $templates; $this->toolbarPosition = $toolbarPosition; + $this->cspHandler = $cspHandler; + $this->baseDir = $baseDir; } /** @@ -89,6 +96,10 @@ public function panelAction(Request $request, $token) $this->profiler->disable(); + if (null !== $this->cspHandler) { + $this->cspHandler->disableCsp(); + } + $panel = $request->query->get('panel', 'request'); $page = $request->query->get('page', 'home'); @@ -117,51 +128,6 @@ public function panelAction(Request $request, $token) )), 200, array('Content-Type' => 'text/html')); } - /** - * Purges all tokens. - * - * @return Response A Response instance - * - * @throws NotFoundHttpException - */ - public function purgeAction() - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - - if (null === $this->profiler) { - throw new NotFoundHttpException('The profiler must be enabled.'); - } - - $this->profiler->disable(); - $this->profiler->purge(); - - return new RedirectResponse($this->generator->generate('_profiler_info', array('about' => 'purge')), 302, array('Content-Type' => 'text/html')); - } - - /** - * Displays information page. - * - * @param Request $request The current HTTP Request - * @param string $about The about message - * - * @return Response A Response instance - * - * @throws NotFoundHttpException - */ - public function infoAction(Request $request, $about) - { - if (null === $this->profiler) { - throw new NotFoundHttpException('The profiler must be enabled.'); - } - - $this->profiler->disable(); - - return new Response($this->twig->render('@WebProfiler/Profiler/info.html.twig', array( - 'about' => $about, - 'request' => $request, - )), 200, array('Content-Type' => 'text/html')); - } - /** * Renders the Web Debug Toolbar. * @@ -207,7 +173,7 @@ public function toolbarAction(Request $request, $token) // the profiler is not enabled } - return new Response($this->twig->render('@WebProfiler/Profiler/toolbar.html.twig', array( + return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/toolbar.html.twig', array( 'request' => $request, 'position' => $position, 'profile' => $profile, @@ -215,7 +181,7 @@ public function toolbarAction(Request $request, $token) 'profiler_url' => $url, 'token' => $token, 'profiler_markup_version' => 2, // 1 = original toolbar, 2 = Symfony 2.8+ toolbar - )), 200, array('Content-Type' => 'text/html')); + )); } /** @@ -235,9 +201,14 @@ public function searchBarAction(Request $request) $this->profiler->disable(); + if (null !== $this->cspHandler) { + $this->cspHandler->disableCsp(); + } + if (null === $session = $request->getSession()) { $ip = $method = + $statusCode = $url = $start = $end = @@ -246,6 +217,7 @@ public function searchBarAction(Request $request) } else { $ip = $request->query->get('ip', $session->get('_profiler_search_ip')); $method = $request->query->get('method', $session->get('_profiler_search_method')); + $statusCode = $request->query->get('status_code', $session->get('_profiler_search_status_code')); $url = $request->query->get('url', $session->get('_profiler_search_url')); $start = $request->query->get('start', $session->get('_profiler_search_start')); $end = $request->query->get('end', $session->get('_profiler_search_end')); @@ -258,6 +230,7 @@ public function searchBarAction(Request $request) 'token' => $token, 'ip' => $ip, 'method' => $method, + 'status_code' => $statusCode, 'url' => $url, 'start' => $start, 'end' => $end, @@ -287,10 +260,15 @@ public function searchResultsAction(Request $request, $token) $this->profiler->disable(); + if (null !== $this->cspHandler) { + $this->cspHandler->disableCsp(); + } + $profile = $this->profiler->loadProfile($token); $ip = $request->query->get('ip'); $method = $request->query->get('method'); + $statusCode = $request->query->get('status_code'); $url = $request->query->get('url'); $start = $request->query->get('start', null); $end = $request->query->get('end', null); @@ -300,9 +278,10 @@ public function searchResultsAction(Request $request, $token) 'request' => $request, 'token' => $token, 'profile' => $profile, - 'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end), + 'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode), 'ip' => $ip, 'method' => $method, + 'status_code' => $statusCode, 'url' => $url, 'start' => $start, 'end' => $end, @@ -330,6 +309,7 @@ public function searchAction(Request $request) $ip = preg_replace('/[^:\d\.]/', '', $request->query->get('ip')); $method = $request->query->get('method'); + $statusCode = $request->query->get('status_code'); $url = $request->query->get('url'); $start = $request->query->get('start', null); $end = $request->query->get('end', null); @@ -339,6 +319,7 @@ public function searchAction(Request $request) if (null !== $session = $request->getSession()) { $session->set('_profiler_search_ip', $ip); $session->set('_profiler_search_method', $method); + $session->set('_profiler_search_status_code', $statusCode); $session->set('_profiler_search_url', $url); $session->set('_profiler_search_start', $start); $session->set('_profiler_search_end', $end); @@ -350,12 +331,13 @@ public function searchAction(Request $request) return new RedirectResponse($this->generator->generate('_profiler', array('token' => $token)), 302, array('Content-Type' => 'text/html')); } - $tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end); + $tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode); return new RedirectResponse($this->generator->generate('_profiler_search_results', array( 'token' => $tokens ? $tokens[0]['token'] : 'empty', 'ip' => $ip, 'method' => $method, + 'status_code' => $statusCode, 'url' => $url, 'start' => $start, 'end' => $end, @@ -378,6 +360,10 @@ public function phpinfoAction() $this->profiler->disable(); + if (null !== $this->cspHandler) { + $this->cspHandler->disableCsp(); + } + ob_start(); phpinfo(); $phpinfo = ob_get_clean(); @@ -385,6 +371,39 @@ public function phpinfoAction() return new Response($phpinfo, 200, array('Content-Type' => 'text/html')); } + /** + * Displays the source of a file. + * + * @return Response A Response instance + * + * @throws NotFoundHttpException + */ + public function openAction(Request $request) + { + if (null === $this->baseDir) { + throw new NotFoundHttpException('The base dir should be set.'); + } + + if ($this->profiler) { + $this->profiler->disable(); + } + + $file = $request->query->get('file'); + $line = $request->query->get('line'); + + $filename = $this->baseDir.DIRECTORY_SEPARATOR.$file; + + if (preg_match("'(^|[/\\\\])\.\.?([/\\\\]|$)'", $file) || !is_readable($filename)) { + throw new NotFoundHttpException(sprintf('The file "%s" cannot be opened.', $file)); + } + + return new Response($this->twig->render('@WebProfiler/Profiler/open.html.twig', array( + 'filename' => $filename, + 'file' => $file, + 'line' => $line, + )), 200, array('Content-Type' => 'text/html')); + } + /** * Gets the Template Manager. * @@ -398,4 +417,18 @@ protected function getTemplateManager() return $this->templateManager; } + + private function renderWithCspNonces(Request $request, $template, $variables, $code = 200, $headers = array('Content-Type' => 'text/html')) + { + $response = new Response('', $code, $headers); + + $nonces = $this->cspHandler ? $this->cspHandler->getNonces($request, $response) : array(); + + $variables['csp_script_nonce'] = isset($nonces['csp_script_nonce']) ? $nonces['csp_script_nonce'] : null; + $variables['csp_style_nonce'] = isset($nonces['csp_style_nonce']) ? $nonces['csp_style_nonce'] : null; + + $response->setContent($this->twig->render($template, $variables)); + + return $response; + } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php index c508346bf68a4..80946ac428c02 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php @@ -87,11 +87,11 @@ private function getTraces(RequestDataCollector $request, $method) { $traceRequest = Request::create( $request->getPathInfo(), - $request->getRequestServer()->get('REQUEST_METHOD'), + $request->getRequestServer(true)->get('REQUEST_METHOD'), array(), - $request->getRequestCookies()->all(), + $request->getRequestCookies(true)->all(), array(), - $request->getRequestServer()->all() + $request->getRequestServer(true)->all() ); $context = $this->matcher->getContext(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php b/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php new file mode 100644 index 0000000000000..782a393e6a978 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php @@ -0,0 +1,271 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Csp; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Handles Content-Security-Policy HTTP header for the WebProfiler Bundle. + * + * @author Romain Neutron + * + * @internal + */ +class ContentSecurityPolicyHandler +{ + private $nonceGenerator; + private $cspDisabled = false; + + public function __construct(NonceGenerator $nonceGenerator) + { + $this->nonceGenerator = $nonceGenerator; + } + + /** + * Returns an array of nonces to be used in Twig templates and Content-Security-Policy headers. + * + * Nonce can be provided by; + * - The request - In case HTML content is fetched via AJAX and inserted in DOM, it must use the same nonce as origin + * - The response - A call to getNonces() has already been done previously. Same nonce are returned + * - They are otherwise randomly generated + * + * @return array + */ + public function getNonces(Request $request, Response $response) + { + if ($request->headers->has('X-SymfonyProfiler-Script-Nonce') && $request->headers->has('X-SymfonyProfiler-Style-Nonce')) { + return array( + 'csp_script_nonce' => $request->headers->get('X-SymfonyProfiler-Script-Nonce'), + 'csp_style_nonce' => $request->headers->get('X-SymfonyProfiler-Style-Nonce'), + ); + } + + if ($response->headers->has('X-SymfonyProfiler-Script-Nonce') && $response->headers->has('X-SymfonyProfiler-Style-Nonce')) { + return array( + 'csp_script_nonce' => $response->headers->get('X-SymfonyProfiler-Script-Nonce'), + 'csp_style_nonce' => $response->headers->get('X-SymfonyProfiler-Style-Nonce'), + ); + } + + $nonces = array( + 'csp_script_nonce' => $this->generateNonce(), + 'csp_style_nonce' => $this->generateNonce(), + ); + + $response->headers->set('X-SymfonyProfiler-Script-Nonce', $nonces['csp_script_nonce']); + $response->headers->set('X-SymfonyProfiler-Style-Nonce', $nonces['csp_style_nonce']); + + return $nonces; + } + + /** + * Disables Content-Security-Policy. + * + * All related headers will be removed. + */ + public function disableCsp() + { + $this->cspDisabled = true; + } + + /** + * Cleanup temporary headers and updates Content-Security-Policy headers. + * + * @return array Nonces used by the bundle in Content-Security-Policy header + */ + public function updateResponseHeaders(Request $request, Response $response) + { + if ($this->cspDisabled) { + $this->removeCspHeaders($response); + + return array(); + } + + $nonces = $this->getNonces($request, $response); + $this->cleanHeaders($response); + $this->updateCspHeaders($response, $nonces); + + return $nonces; + } + + private function cleanHeaders(Response $response) + { + $response->headers->remove('X-SymfonyProfiler-Script-Nonce'); + $response->headers->remove('X-SymfonyProfiler-Style-Nonce'); + } + + private function removeCspHeaders(Response $response) + { + $response->headers->remove('X-Content-Security-Policy'); + $response->headers->remove('Content-Security-Policy'); + $response->headers->remove('Content-Security-Policy-Report-Only'); + } + + /** + * Updates Content-Security-Policy headers in a response. + * + * @return array + */ + private function updateCspHeaders(Response $response, array $nonces = array()) + { + $nonces = array_replace(array( + 'csp_script_nonce' => $this->generateNonce(), + 'csp_style_nonce' => $this->generateNonce(), + ), $nonces); + + $ruleIsSet = false; + + $headers = $this->getCspHeaders($response); + + foreach ($headers as $header => $directives) { + foreach (array('script-src' => 'csp_script_nonce', 'style-src' => 'csp_style_nonce') as $type => $tokenName) { + if ($this->authorizesInline($directives, $type)) { + continue; + } + if (!isset($headers[$header][$type])) { + if (isset($headers[$header]['default-src'])) { + $headers[$header][$type] = $headers[$header]['default-src']; + } else { + // If there is no script-src/style-src and no default-src, no additional rules required. + continue; + } + } + $ruleIsSet = true; + if (!in_array('\'unsafe-inline\'', $headers[$header][$type], true)) { + $headers[$header][$type][] = '\'unsafe-inline\''; + } + $headers[$header][$type][] = sprintf('\'nonce-%s\'', $nonces[$tokenName]); + } + } + + if (!$ruleIsSet) { + return $nonces; + } + + foreach ($headers as $header => $directives) { + $response->headers->set($header, $this->generateCspHeader($directives)); + } + + return $nonces; + } + + /** + * Generates a valid Content-Security-Policy nonce. + * + * @return string + */ + private function generateNonce() + { + return $this->nonceGenerator->generate(); + } + + /** + * Converts a directive set array into Content-Security-Policy header. + * + * @param array $directives The directive set + * + * @return string The Content-Security-Policy header + */ + private function generateCspHeader(array $directives) + { + return array_reduce(array_keys($directives), function ($res, $name) use ($directives) { + return ($res !== '' ? $res.'; ' : '').sprintf('%s %s', $name, implode(' ', $directives[$name])); + }, ''); + } + + /** + * Converts a Content-Security-Policy header value into a directive set array. + * + * @param string $header The header value + * + * @return array The directive set + */ + private function parseDirectives($header) + { + $directives = array(); + + foreach (explode(';', $header) as $directive) { + $parts = explode(' ', trim($directive)); + if (count($parts) < 1) { + continue; + } + $name = array_shift($parts); + $directives[$name] = $parts; + } + + return $directives; + } + + /** + * Detects if the 'unsafe-inline' is prevented for a directive within the directive set. + * + * @param array $directivesSet The directive set + * @param string $type The name of the directive to check + * + * @return bool + */ + private function authorizesInline(array $directivesSet, $type) + { + if (isset($directivesSet[$type])) { + $directives = $directivesSet[$type]; + } elseif (isset($directivesSet['default-src'])) { + $directives = $directivesSet['default-src']; + } else { + return false; + } + + return in_array('\'unsafe-inline\'', $directives, true) && !$this->hasHashOrNonce($directives); + } + + private function hasHashOrNonce(array $directives) + { + foreach ($directives as $directive) { + if ('\'' !== substr($directive, -1)) { + continue; + } + if ('\'nonce-' === substr($directive, 0, 7)) { + return true; + } + if (in_array(substr($directive, 0, 8), array('\'sha256-', '\'sha384-', '\'sha512-'), true)) { + return true; + } + } + + return false; + } + + /** + * Retrieves the Content-Security-Policy headers (either X-Content-Security-Policy or Content-Security-Policy) from + * a response. + * + * @return array An associative array of headers + */ + private function getCspHeaders(Response $response) + { + $headers = array(); + + if ($response->headers->has('Content-Security-Policy')) { + $headers['Content-Security-Policy'] = $this->parseDirectives($response->headers->get('Content-Security-Policy')); + } + + if ($response->headers->has('Content-Security-Policy-Report-Only')) { + $headers['Content-Security-Policy-Report-Only'] = $this->parseDirectives($response->headers->get('Content-Security-Policy-Report-Only')); + } + + if ($response->headers->has('X-Content-Security-Policy')) { + $headers['X-Content-Security-Policy'] = $this->parseDirectives($response->headers->get('X-Content-Security-Policy')); + } + + return $headers; + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php b/src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php new file mode 100644 index 0000000000000..728043551f3ee --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.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\WebProfilerBundle\Csp; + +/** + * Generates Content-Security-Policy nonce. + * + * @author Romain Neutron + * + * @internal + */ +class NonceGenerator +{ + public function generate() + { + return bin2hex(random_bytes(16)); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php index 8af7c63e64aaa..e507bf2d22b70 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php @@ -52,6 +52,24 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('web_profiler.debug_toolbar.intercept_redirects', $config['intercept_redirects']); $container->setParameter('web_profiler.debug_toolbar.mode', $config['toolbar'] ? WebDebugToolbarListener::ENABLED : WebDebugToolbarListener::DISABLED); } + + $baseDir = array(); + $rootDir = $container->getParameter('kernel.root_dir'); + $rootDir = explode(DIRECTORY_SEPARATOR, realpath($rootDir) ?: $rootDir); + $bundleDir = explode(DIRECTORY_SEPARATOR, __DIR__); + for ($i = 0; isset($rootDir[$i], $bundleDir[$i]); ++$i) { + if ($rootDir[$i] !== $bundleDir[$i]) { + break; + } + $baseDir[] = $rootDir[$i]; + } + $baseDir = implode(DIRECTORY_SEPARATOR, $baseDir); + + $profilerController = $container->getDefinition('web_profiler.controller.profiler'); + $profilerController->replaceArgument(6, $baseDir); + + $fileLinkFormatter = $container->getDefinition('debug.file_link_formatter'); + $fileLinkFormatter->replaceArgument(2, $baseDir); } /** diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index 5d020fe914bb0..6ffdbc7ad0764 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\EventListener; +use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; @@ -41,8 +42,9 @@ class WebDebugToolbarListener implements EventSubscriberInterface protected $mode; protected $position; protected $excludedAjaxPaths; + private $cspHandler; - public function __construct(Environment $twig, $interceptRedirects = false, $mode = self::ENABLED, $position = 'bottom', UrlGeneratorInterface $urlGenerator = null, $excludedAjaxPaths = '^/bundles|^/_wdt') + public function __construct(Environment $twig, $interceptRedirects = false, $mode = self::ENABLED, $position = 'bottom', UrlGeneratorInterface $urlGenerator = null, $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null) { $this->twig = $twig; $this->urlGenerator = $urlGenerator; @@ -50,6 +52,7 @@ public function __construct(Environment $twig, $interceptRedirects = false, $mod $this->mode = (int) $mode; $this->position = $position; $this->excludedAjaxPaths = $excludedAjaxPaths; + $this->cspHandler = $cspHandler; } public function isEnabled() @@ -77,6 +80,8 @@ public function onKernelResponse(FilterResponseEvent $event) return; } + $nonces = $this->cspHandler ? $this->cspHandler->updateResponseHeaders($request, $response) : array(); + // do not capture redirects or modify XML HTTP Requests if ($request->isXmlHttpRequest()) { return; @@ -104,13 +109,13 @@ public function onKernelResponse(FilterResponseEvent $event) return; } - $this->injectToolbar($response, $request); + $this->injectToolbar($response, $request, $nonces); } /** * Injects the web debug toolbar into the given Response. */ - protected function injectToolbar(Response $response, Request $request) + protected function injectToolbar(Response $response, Request $request, array $nonces) { $content = $response->getContent(); $pos = strripos($content, ''); @@ -123,6 +128,8 @@ protected function injectToolbar(Response $response, Request $request) 'excluded_ajax_paths' => $this->excludedAjaxPaths, 'token' => $response->headers->get('X-Debug-Token'), 'request' => $request, + 'csp_script_nonce' => isset($nonces['csp_script_nonce']) ? $nonces['csp_script_nonce'] : null, + 'csp_style_nonce' => isset($nonces['csp_style_nonce']) ? $nonces['csp_style_nonce'] : null, ) ))."\n"; $content = substr($content, 0, $pos).$toolbar.substr($content, $pos); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php index 29fe3a31c85d6..13590e33acd95 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php @@ -60,26 +60,6 @@ public function getName(Profile $profile, $panel) return $templates[$panel]; } - /** - * Gets the templates for a given profile. - * - * @param Profile $profile - * - * @return Template[] - * - * @deprecated not used anymore internally - */ - public function getTemplates(Profile $profile) - { - $templates = $this->getNames($profile); - - foreach ($templates as $name => $template) { - $templates[$name] = $this->twig->loadTemplate($template); - } - - return $templates; - } - /** * Gets template names of templates that are present in the viewed profile. * diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/bin/sync_assets.sh b/src/Symfony/Bundle/WebProfilerBundle/Resources/bin/sync_assets.sh deleted file mode 100755 index 6cff8d8276d94..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/bin/sync_assets.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -DIR=`php -r "echo realpath(dirname('$0'));"` - -cp $DIR/../../../FrameworkBundle/Resources/public/css/body.css $DIR/../views/Profiler/body.css.twig -cp $DIR/../../../FrameworkBundle/Resources/public/css/exception.css $DIR/../views/Collector/exception.css.twig diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml deleted file mode 100644 index d85b54d38db70..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/commands.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - Symfony\Bundle\WebProfilerBundle\Command\ImportCommand - Symfony\Bundle\WebProfilerBundle\Command\ExportCommand - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml index ed7e923f0d05d..4c659b628144d 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml @@ -4,36 +4,59 @@ 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\Bundle\WebProfilerBundle\Controller\ProfilerController - Symfony\Bundle\WebProfilerBundle\Controller\RouterController - Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController - Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension - - - + + + %data_collector.templates% %web_profiler.debug_toolbar.position% + + null - + - + %kernel.debug% - + + + + + + + + + + null + %kernel.charset% + Symfony\Component\VarDumper\Dumper\HtmlDumper::DUMP_LIGHT_ARRAY + + + 4096 + + + + + + + + + %debug.file_link_format% + + null + /_profiler/open?file=%%f&line=%%l#line%%l diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml index d9708f9c50743..0be717b19d6e7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml @@ -16,14 +16,6 @@ web_profiler.controller.profiler:searchBarAction - - web_profiler.controller.profiler:purgeAction - - - - web_profiler.controller.profiler:infoAction - - web_profiler.controller.profiler:phpinfoAction @@ -32,6 +24,10 @@ web_profiler.controller.profiler:searchResultsAction + + web_profiler.controller.profiler:openAction + + web_profiler.controller.profiler:panelAction diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml index d72c28532c334..25f49fc2cd069 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/toolbar.xml @@ -4,12 +4,10 @@ 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\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener - - - + + + %web_profiler.debug_toolbar.intercept_redirects% @@ -17,6 +15,7 @@ %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 index fe73b662bd311..5df0d9ea9bd0f 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/ajax.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/ajax.html.twig @@ -3,7 +3,7 @@ {% block toolbar %} {% set icon %} {{ include('@WebProfiler/Icon/ajax.svg') }} - 0 + 0 {% endset %} {% set text %} @@ -15,6 +15,8 @@ Method + Type + Status URL Time Profile diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig new file mode 100644 index 0000000000000..cbc705f51cb0e --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig @@ -0,0 +1,153 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% if collector.totals.calls > 0 %} + {% set icon %} + {{ include('@WebProfiler/Icon/cache.svg') }} + {{ collector.totals.calls }} + + in + {{ '%0.2f'|format(collector.totals.time * 1000) }} + ms + + {% endset %} + {% set text %} +
+ Cache Calls + {{ collector.totals.calls }} +
+
+ Total time + {{ '%0.2f'|format(collector.totals.time * 1000) }} ms +
+
+ Cache hits + {{ collector.totals.hits }} / {{ collector.totals.reads }}{% if collector.totals.hit_read_ratio is not null %} ({{ collector.totals.hit_read_ratio }}%){% endif %} +
+
+ Cache writes + {{ collector.totals.writes }} +
+ {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} + {% endif %} +{% endblock %} + +{% block menu %} + + + {{ include('@WebProfiler/Icon/cache.svg') }} + + Cache + +{% endblock %} + +{% block panel %} +

Cache

+ + {% if collector.totals.calls == 0 %} +
+

No cache calls were made.

+
+ {% else %} +
+
+ {{ collector.totals.calls }} + Total calls +
+
+ {{ '%0.2f'|format(collector.totals.time * 1000) }} ms + Total time +
+
+
+ {{ collector.totals.reads }} + Total reads +
+
+ {{ collector.totals.writes }} + Total writes +
+
+ {{ collector.totals.deletes }} + Total deletes +
+
+
+ {{ collector.totals.hits }} + Total hits +
+
+ {{ collector.totals.misses }} + Total misses +
+
+ + {{ collector.totals.hit_read_ratio ?? 0 }} % + + Hits/reads +
+
+ +

Pools

+
+ {% for name, calls in collector.calls %} +
+

{{ name }} {{ collector.statistics[name].calls }}

+ +
+ {% if calls|length == 0 %} +
+

No calls were made for {{ name }} pool.

+
+ {% else %} +

Metrics

+
+ {% for key, value in collector.statistics[name] %} +
+ + {% if key == 'time' %} + {{ '%0.2f'|format(1000 * value.value) }} ms + {% elseif key == 'hit_read_ratio' %} + {{ value.value ?? 0 }} % + {% else %} + {{ value }} + {% endif %} + + {{ key == 'hit_read_ratio' ? 'Hits/reads' : key|capitalize }} +
+ {% if key == 'time' or key == 'deletes' %} +
+ {% endif %} + {% endfor %} +
+ +

Calls

+ + + + + + + + + + + {% for call in calls %} + + + + + + + {% endfor %} + +
#TimeCallHit
{{ loop.index }}{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms{{ call.name }}(){{ profiler_dump(call.value.result, maxDepth=2) }}
+ {% endif %} +
+
+ {% endfor %} +
+ {% endif %} +{% 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 6b84aaf65663a..99e12f0f00fb8 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig @@ -75,7 +75,7 @@
PHP version - + {{ collector.phpversion }}   View phpinfo() @@ -84,7 +84,8 @@
PHP Extensions xdebug - accel + APCu + OPcache
@@ -109,6 +110,14 @@ {% endif %}
+ {% endif %}
{% endset %} @@ -173,46 +182,71 @@
{% endif %} + + {% set symfony_status = { dev: 'Unstable Version', stable: 'Stable Version', eom: 'Maintenance Ended', eol: 'Version Expired' } %} + {% set symfony_status_class = { dev: 'warning', stable: 'success', eom: 'warning', eol: 'error' } %} + + + + + + + + + + + + + + + + + +
Symfony StatusBugs {{ collector.symfonystate in ['eom', 'eol'] ? 'were' : 'are' }} fixed untilSecurity issues {{ collector.symfonystate == 'eol' ? 'were' : 'are' }} fixed until
+ {{ symfony_status[collector.symfonystate]|upper }} + {{ collector.symfonyeom }}{{ collector.symfonyeol }} + View roadmap +
{% endif %}

PHP Configuration

- {{ collector.phpversion }} + {{ collector.phpversion }}{% if collector.phpversionextra %} {{ collector.phpversionextra }}{% endif %} PHP version
- {{ include('@WebProfiler/Icon/' ~ (collector.hasaccelerator ? 'yes' : 'no') ~ '.svg') }} - PHP acceleration + {{ collector.phparchitecture }} bits + Architecture
- {{ include('@WebProfiler/Icon/' ~ (collector.hasxdebug ? 'yes' : 'no') ~ '.svg') }} - Xdebug + {{ collector.phpintllocale }} + Intl locale
-
-
- {{ include('@WebProfiler/Icon/' ~ (collector.haszendopcache ? 'yes' : 'no') ~ '.svg') }} - OPcache + {{ collector.phptimezone }} + Timezone
+
+
- {{ include('@WebProfiler/Icon/' ~ (collector.hasapc ? 'yes' : 'no') ~ '.svg') }} - APC + {{ include('@WebProfiler/Icon/' ~ (collector.haszendopcache ? 'yes' : 'no') ~ '.svg') }} + OPcache
- {{ include('@WebProfiler/Icon/' ~ (collector.hasxcache ? 'yes' : 'no') ~ '.svg') }} - XCache + {{ include('@WebProfiler/Icon/' ~ (collector.hasapcu ? 'yes' : 'no') ~ '.svg') }} + APCu
- {{ include('@WebProfiler/Icon/' ~ (collector.haseaccelerator ? 'yes' : 'no') ~ '.svg') }} - EAccelerator + {{ include('@WebProfiler/Icon/' ~ (collector.hasxdebug ? 'yes' : 'no') ~ '.svg') }} + Xdebug
@@ -233,7 +267,7 @@ {% for name in collector.bundles|keys|sort %} {{ name }} - {{ collector.bundles[name] }} + {{ profiler_dump(collector.bundles[name]) }} {% endfor %} 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 24a1e3fff99a8..238096157acc3 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/events.html.twig @@ -75,38 +75,7 @@ {{ listener.priority|default('-') }} - - {% if listener.type == 'Closure' %} - - Closure - (there is no class or file information) - - {% elseif listener.type == 'Function' %} - - {% set link = listener.file|file_link(listener.line) %} - {% if link %} - {{ listener.function }}() - ({{ listener.file }}) - {% else %} - {{ listener.function }}() - {{ listener.file }} (line {{ listener.line }}) - {% endif %} - - {% elseif listener.type == "Method" %} - - {% set link = listener.file|file_link(listener.line) %} - {% set class_namespace = listener.class|split('\\', -1)|join('\\') %} - - {% if link %} - {{ listener.class|abbr_class|striptags }}::{{ listener.method }}() - ({{ listener.class }}) - {% else %} - {{ class_namespace }}\{{ listener.class|abbr_class|striptags }}::{{ listener.method }}() - {{ listener.file }} (line {{ listener.line }}) - {% endif %} - - {% endif %} - + {{ profiler_dump(listener.stub) }} {% if loop.last %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig index 0d057b98a4334..c849cb29666ff 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig @@ -1,96 +1,34 @@ -.sf-reset .traces { - padding: 0 0 1em 1.5em; -} -.sf-reset .traces a { - font-size: 14px; -} -.sf-reset .traces abbr { - border-bottom-color: #AAA; - padding-bottom: 2px; -} -.sf-reset .traces li { - color: #222; - font-size: 14px; - padding: 5px 0; - list-style-type: decimal; - margin: 0 0 0 1em; -} -.sf-reset .traces li.selected { - background: rgba(255, 255, 153, 0.5); -} +{{ include('@Twig/exception.css.twig') }} -.sf-reset .traces ol li { - font-size: 12px; - color: #777; -} -.sf-reset #logs .traces li.error { - color: #AA3333; +.container { + max-width: auto; + margin: 0; + padding: 0; } -.sf-reset #logs .traces li.warning { - background: #FFCC00; +.container .container { + padding: 0; } -.sf-reset .trace { - border: 1px solid #DDD; + +.exception-summary { background: #FFF; - padding: 10px; - overflow: auto; + border: 1px solid #E0E0E0; + box-shadow: 0 0 1px rgba(128, 128, 128, .2); margin: 1em 0; + padding: 10px; } -.sf-reset .trace code, -#traces-text pre { - font-size: 13px; -} -.sf-reset .block-exception { - margin-bottom: 2em; - background-color: #FFF; - border: 1px solid #EEE; - padding: 28px; - word-wrap: break-word; - overflow: hidden; -} -.sf-reset .block-exception h1 { - font-size: 21px; - font-weight: normal; - margin: 0 0 12px; -} -.sf-reset .block-exception .linked { - margin-top: 1em; +.exception-summary.exception-without-message { + display: none; } -.sf-reset .block { - margin-bottom: 2em; -} -.sf-reset .block h2 { - font-size: 16px; -} -.sf-reset .block-exception div { - font-size: 14px; -} -.sf-reset .block-exception-detected .illustration-exception, -.sf-reset .block-exception-detected .text-exception { - float: left; -} -.sf-reset .block-exception-detected .illustration-exception { - width: 110px; +.exception-message { + color: #B0413E; } -.sf-reset .block-exception-detected .text-exception { - width: 650px; - margin-left: 20px; - padding: 30px 44px 24px 46px; - position: relative; -} -.sf-reset .text-exception .open-quote, -.sf-reset .text-exception .close-quote { - position: absolute; -} -.sf-reset .open-quote { - top: 0; - left: 0; -} -.sf-reset .close-quote { - bottom: 0; - right: 50px; + +.exception-metadata, +.exception-illustration { + display: none; } -.sf-reset .toggle { - vertical-align: middle; + +.exception-message-wrapper .container { + min-height: auto; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index fc4aada9ef3e2..02b77319ac249 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -151,9 +151,6 @@ margin-top: -9px; margin-left: 6px; } - .form-type { - color: #999; - } .badge-error { float: right; background: #B0413E; @@ -163,6 +160,9 @@ font-weight: bold; vertical-align: middle; } + .has-error { + color: #B0413E; + } .errors h3 { color: #B0413E; } @@ -194,7 +194,7 @@
{% for formName, formData in collector.data.forms %} - {{ form_tree_details(formName, formData, collector.data.forms_by_hash) }} + {{ form_tree_details(formName, formData, collector.data.forms_by_hash, loop.first) }} {% endfor %}
{% else %} @@ -422,11 +422,12 @@ {% endblock %} -{% macro form_tree_entry(name, data, expanded) %} +{% macro form_tree_entry(name, data, is_root) %} {% import _self as tree %} + {% set has_error = data.errors is defined and data.errors|length > 0 %}
  • - {% if data.errors is defined and data.errors|length > 0 %} + {% if has_error %}
    {{ data.errors|length }}
    {% endif %} @@ -436,11 +437,13 @@
    {% endif %} - {{ name|default('(no name)') }} {% if data.type_class is defined and data.type is defined %}[{{ data.type|split('\\')|last }}]{% endif %} + + {{ name|default('(no name)') }} +
    {% if data.children is not empty %} -
      +
        {% for childName, childData in data.children %} {{ tree.form_tree_entry(childName, childData, false) }} {% endfor %} @@ -449,14 +452,11 @@ {% endmacro %} -{% macro form_tree_details(name, data, forms_by_hash) %} +{% macro form_tree_details(name, data, forms_by_hash, show) %} {% import _self as tree %} -
        -

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

        + {{ name|default('(no name)') }} {% if data.type_class is defined %}({{ profiler_dump(data.type_class) }}){% endif %}

        {% if data.errors is defined and data.errors|length > 0 %} @@ -489,29 +489,14 @@ {% 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 %} + {% if error.trace %} + Caused by: + {% for stacked in error.trace %} + {{ profiler_dump(stacked) }} + {% endfor %} {% else %} Unknown. - {% endfor %} + {% endif %} {% endfor %} @@ -540,7 +525,7 @@ Model Format {% if data.default_data.model is defined %} - {{ data.default_data.model }} + {{ profiler_dump(data.default_data.seek('model')) }} {% else %} same as normalized format {% endif %} @@ -548,13 +533,13 @@ Normalized Format - {{ data.default_data.norm }} + {{ profiler_dump(data.default_data.seek('norm')) }} View Format {% if data.default_data.view is defined %} - {{ data.default_data.view }} + {{ profiler_dump(data.default_data.seek('view')) }} {% else %} same as normalized format {% endif %} @@ -586,7 +571,7 @@ View Format {% if data.submitted_data.view is defined %} - {{ data.submitted_data.view }} + {{ profiler_dump(data.submitted_data.seek('view')) }} {% else %} same as normalized format {% endif %} @@ -594,13 +579,13 @@ Normalized Format - {{ data.submitted_data.norm }} + {{ profiler_dump(data.submitted_data.seek('norm')) }} Model Format {% if data.submitted_data.model is defined %} - {{ data.submitted_data.model }} + {{ profiler_dump(data.submitted_data.seek('model')) }} {% else %} same as normalized format {% endif %} @@ -637,12 +622,15 @@ {% for option, value in data.passed_options %} {{ option }} - {{ value }} + {{ profiler_dump(value) }} - {% if data.resolved_options[option] is same as(value) %} + {# values can be stubs #} + {% set option_value = value.value|default(value) %} + {% set resolved_option_value = data.resolved_options[option].value|default(data.resolved_options[option]) %} + {% if resolved_option_value == option_value %} same as passed value {% else %} - {{ data.resolved_options[option] }} + {{ profiler_dump(data.resolved_options.seek(option)) }} {% endif %} @@ -676,7 +664,7 @@ {% for option, value in data.resolved_options %} {{ option }} - {{ value }} + {{ profiler_dump(value) }} {% endfor %} @@ -703,7 +691,7 @@ {% for variable, value in data.view_vars %} {{ variable }} - {{ value }} + {{ profiler_dump(value) }} {% endfor %} 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 c25a5c54d6cd2..6e92022a441cf 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -3,12 +3,11 @@ {% import _self as helper %} {% block toolbar %} - {% if collector.counterrors or collector.countdeprecations or collector.countscreams %} + {% if collector.counterrors or collector.countdeprecations or collector.countwarnings %} {% set icon %} - {% set status_color = collector.counterrors ? 'red' : collector.countdeprecations ? 'yellow' : '' %} - {% set error_count = collector.counterrors + collector.countdeprecations + collector.countscreams %} + {% set status_color = collector.counterrors ? 'red' : 'yellow' %} {{ include('@WebProfiler/Icon/logger.svg') }} - {{ error_count }} + {{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }} {% endset %} {% set text %} @@ -18,13 +17,13 @@
        - Deprecated Calls - {{ collector.countdeprecations|default(0) }} + Warnings + {{ collector.countwarnings|default(0) }}
        - Silenced Errors - {{ collector.countscreams|default(0) }} + Deprecations + {{ collector.countdeprecations|default(0) }}
        {% endset %} @@ -33,12 +32,12 @@ {% endblock %} {% block menu %} - + {{ include('@WebProfiler/Icon/logger.svg') }} Logs - {% if collector.counterrors or collector.countdeprecations %} + {% if collector.counterrors or collector.countdeprecations or collector.countwarnings %} - {{ collector.counterrors ?: collector.countdeprecations }} + {{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }} {% endif %} @@ -55,9 +54,9 @@ {# sort collected logs in groups #} {% set deprecation_logs, debug_logs, info_and_error_logs, silenced_logs = [], [], [], [] %} {% for log in collector.logs %} - {% if log.context.level is defined and log.context.type is defined and log.context.type in [constant('E_DEPRECATED'), constant('E_USER_DEPRECATED')] %} + {% if log.scream is defined and not log.scream %} {% set deprecation_logs = deprecation_logs|merge([log]) %} - {% elseif log.context.scream is defined and log.context.scream == true %} + {% elseif log.scream is defined and log.scream %} {% set silenced_logs = silenced_logs|merge([log]) %} {% elseif log.priorityName == 'DEBUG' %} {% set debug_logs = debug_logs|merge([log]) %} @@ -68,7 +67,8 @@
        -

        Info. & Errors {{ info_and_error_logs|length }}

        +

        Info. & Errors {{ collector.counterrors ?: info_and_error_logs|length }}

        +

        Informational and error log messages generated during the execution of the application.

        {% if info_and_error_logs is empty %} @@ -76,7 +76,7 @@

        There are no log messages of this level.

        {% else %} - {{ helper.render_table(info_and_error_logs, true) }} + {{ helper.render_table(info_and_error_logs, 'info', true) }} {% endif %}
        @@ -84,7 +84,8 @@
        {# 'deprecation_logs|length' is not used because deprecations are now grouped and the group count doesn't match the message count #} -

        Deprecations {{ collector.countdeprecations|default(0) }}

        +

        Deprecations {{ collector.countdeprecations|default(0) }}

        +

        Log messages generated by using features marked as deprecated.

        {% if deprecation_logs is empty %} @@ -92,13 +93,14 @@

        There are no log messages about deprecated features.

        {% else %} - {{ helper.render_table(deprecation_logs, false, true) }} + {{ helper.render_table(deprecation_logs, 'deprecation', false, true) }} {% endif %}

        Debug {{ debug_logs|length }}

        +

        Unimportant log messages generated during the execution of the application.

        {% if debug_logs is empty %} @@ -106,13 +108,14 @@

        There are no log messages of this level.

        {% else %} - {{ helper.render_table(debug_logs) }} + {{ helper.render_table(debug_logs, 'debug') }} {% endif %}
        -

        Silenced Errors {{ collector.countscreams|default(0) }}

        +

        PHP Notices {{ collector.countscreams|default(0) }}

        +

        Log messages generated by PHP notices silenced with the @ operator.

        {% if silenced_logs is empty %} @@ -120,7 +123,54 @@

        There are no log messages of this level.

        {% else %} - {{ helper.render_table(silenced_logs) }} + {{ helper.render_table(silenced_logs, 'silenced') }} + {% endif %} +
        + + + {% set compilerLogTotal = 0 %} + {% for logs in collector.compilerLogs %} + {% set compilerLogTotal = compilerLogTotal + logs|length %} + {% endfor %} +
        +

        Container {{ compilerLogTotal }}

        +

        Log messages generated during the compilation of the service container.

        + +
        + {% if collector.compilerLogs is empty %} +
        +

        There are no compiler log messages.

        +
        + {% else %} + + + + + + + + + + {% for class, logs in collector.compilerLogs %} + + + + + {% endfor %} + +
        ClassMessages
        + {% set context_id = 'context-compiler-' ~ loop.index %} + + {{ class }} + +
        +
          + {% for log in logs %} +
        • {{ profiler_dump_log(log.message) }}
        • + {% endfor %} +
        +
        +
        {{ logs|length }}
        {% endif %}
        @@ -129,7 +179,7 @@ {% endif %} {% endblock %} -{% macro render_table(logs, show_level = false, is_deprecation = false) %} +{% macro render_table(logs, category = '', show_level = false, is_deprecation = false) %} {% import _self as helper %} {% set channel_is_defined = (logs|first).channel is defined %} @@ -138,7 +188,7 @@ {{ show_level ? 'Level' : 'Time' }} {% if channel_is_defined %}Channel{% endif %} - Message + Message @@ -146,77 +196,61 @@ {% for log in logs %} {% set css_class = is_deprecation ? '' : log.priorityName in ['CRITICAL', 'ERROR', 'ALERT', 'EMERGENCY'] ? 'status-error' - : log.priorityName in ['NOTICE', 'WARNING'] ? 'status-warning' + : log.priorityName == 'WARNING' ? 'status-warning' %} - + {% if show_level %} - {{ log.priorityName }} + {{ log.priorityName }} {% endif %} - {{ log.timestamp|date('H:i:s') }} + {{ log.timestamp|date('H:i:s') }} {% if channel_is_defined %} - {{ log.channel }} + + {{ log.channel }} + {% if log.errorCount is defined and log.errorCount > 1 %} + ({{ log.errorCount }} times) + {% endif %} + + {% endif %} - {{ helper.render_log_message(loop.index, log, is_deprecation) }} + {{ helper.render_log_message(category, loop.index, log) }} {% endfor %} {% endmacro %} -{% macro render_log_message(log_index, log, is_deprecation = false) %} - {{ log.message }} - - {% if is_deprecation %} - {% set stack = log.context.stack|default([]) %} - {% set id = 'sf-call-stack-' ~ log_index %} +{% macro render_log_message(category, log_index, log) %} + {% set has_context = log.context is defined and log.context is not empty %} + {% set has_trace = log.context.exception.trace is defined %} - {% if log.context.errorCount is defined %} - ({{ log.context.errorCount }} times) - {% endif %} + {% if not has_context %} + {{ profiler_dump_log(log.message) }} + {% else %} + {{ profiler_dump_log(log.message, log.context) }} - {% if stack %} - - {% endif %} +
        + {% set context_id = 'context-' ~ category ~ '-' ~ log_index %} + Show context - {% for index, call in stack if index > 1 %} - {% if index == 2 %} -
        - {% if call.class is defined %} - {% set from = call.class|abbr_class ~ '::' ~ call.function|abbr_method() %} - {% elseif call.function is defined %} - {% set from = call.function|abbr_method %} - {% elseif call.file is defined %} - {% set from = call.file %} - {% else %} - {% set from = '-' %} - {% endif %} - - {% set file_name = (call.file is defined and call.line is defined) ? call.file|replace({'\\': '/'})|split('/')|last %} - -
      • - {{ from|raw }} - {% if file_name %} - (called from {{ call.file|format_file(call.line, file_name)|raw }}) - {% endif %} -
      • +
        + {{ profiler_dump(log.context, maxDepth=1) }} +
        - {% if index == stack|length - 1 %} -
      - {% endif %} - {% endfor %} - {% else %} - {% if log.context is defined and log.context is not empty %} - + {% if has_trace %} +
      + {{ profiler_dump(log.context.exception.trace, maxDepth=1) }} +
      {% endif %} {% endif %} {% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig index db842c5e0e64d..4fc6a82c58298 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig @@ -2,59 +2,99 @@ {% block toolbar %} {% set request_handler %} - {% if collector.controller.class is defined %} - {% set link = collector.controller.file|file_link(collector.controller.line) %} - {% if link %}{% else %}{% endif %} - - {{ collector.controller.class|abbr_class|striptags }} + {% import _self as helper %} + {{ helper.set_handler(collector.controller) }} + {% endset %} - {%- if collector.controller.method -%} -  :: {{ collector.controller.method }} - {%- endif -%} + {% if collector.redirect %} + {% set redirect_handler %} + {% import _self as helper %} + {{ helper.set_handler(collector.redirect.controller, collector.redirect.route, 'GET' != collector.redirect.method ? collector.redirect.method) }} + {% endset %} + {% endif %} - {% if link %}{% else %}{% endif %} - {% else %} - {{ collector.controller }} - {% endif %} - {% endset %} + {% if collector.forward|default(false) %} + {% set forward_handler %} + {% import _self as helper %} + {{ helper.set_handler(collector.forward.controller) }} + {% endset %} + {% endif %} {% set request_status_code_color = (collector.statuscode >= 400) ? 'red' : (collector.statuscode >= 300) ? 'yellow' : 'green' %} {% set icon %} {{ collector.statuscode }} {% if collector.route %} - @ + {% if collector.redirect %}{{ include('@WebProfiler/Icon/redirect.svg') }}{% endif %} + {% if collector.forward|default(false) %}{{ include('@WebProfiler/Icon/forward.svg') }}{% endif %} + {{ 'GET' != collector.method ? collector.method }} @ {{ collector.route }} {% endif %} {% endset %} {% set text %} -
      - HTTP status - {{ collector.statuscode }} {{ collector.statustext }} -
      +
      +
      + HTTP status + {{ collector.statuscode }} {{ collector.statustext }} +
      -
      - Controller - {{ request_handler }} -
      + {% if 'GET' != collector.method -%} +
      + Method + {{ collector.method }} +
      + {%- endif %} - {% if collector.controller.class is defined %}
      - Controller class - {{ collector.controller.class }} + Controller + {{ request_handler }}
      - {% endif %} -
      - Route name - {{ collector.route|default('NONE') }} -
      + {% if collector.controller.class is defined -%} +
      + Controller class + {{ collector.controller.class }} +
      + {%- endif %} + +
      + Route name + {{ collector.route|default('NONE') }} +
      -
      - Has session - {% if collector.sessionmetadata|length %}yes{% else %}no{% endif %} +
      + Has session + {% if collector.sessionmetadata|length %}yes{% else %}no{% endif %} +
      + + {% if redirect_handler is defined -%} +
      +
      + + {{ collector.redirect.status_code }} + Redirect from + + + {{ redirect_handler }} + ({{ collector.redirect.token }}) + +
      +
      + {% endif %} + + {% if forward_handler is defined %} +
      +
      + Forwarded to + + {{ forward_handler }} + ({{ collector.forward.token }}) + +
      +
      + {% endif %} {% endset %} {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} @@ -80,7 +120,7 @@

      No GET parameters

      {% else %} - {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestquery }, with_context = false) }} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestquery, maxDepth: 1 }, with_context = false) }} {% endif %}

      POST Parameters

      @@ -90,7 +130,7 @@

      No POST parameters

      {% else %} - {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestrequest }, with_context = false) }} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestrequest, maxDepth: 1 }, with_context = false) }} {% endif %}

      Request Attributes

      @@ -103,18 +143,8 @@ {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestattributes }, with_context = false) }} {% endif %} -

      Cookies

      - - {% if collector.requestcookies.all is empty %} -
      -

      No cookies

      -
      - {% else %} - {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestcookies }, with_context = false) }} - {% endif %} -

      Request Headers

      - {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestheaders, labels: ['Header', 'Value'] }, with_context = false) }} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }}

      Request Content

      @@ -143,7 +173,33 @@

      Response Headers

      - {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responseheaders, labels: ['Header', 'Value'] }, with_context = false) }} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responseheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }} +
      + + +
      +

      Cookies

      + +
      +

      Request Cookies

      + + {% if collector.requestcookies.all is empty %} +
      +

      No request cookies

      +
      + {% else %} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestcookies }, with_context = false) }} + {% endif %} + +

      Response Cookies

      + + {% if collector.responsecookies.all is empty %} +
      +

      No response cookies

      +
      + {% else %} + {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responsecookies }, with_context = true) }} + {% endif %}
      @@ -212,7 +268,7 @@ {% for child in profile.children %}

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

      @@ -224,3 +280,22 @@ {% endif %} {% endblock %} + +{% macro set_handler(controller, route, method) %} + {% if controller.class is defined -%} + {%- if method|default(false) %}{{ method }}{% endif -%} + {%- set link = controller.file|file_link(controller.line) %} + {%- if link %}{% else %}{% endif %} + + {%- if route|default(false) -%} + @{{ route }} + {%- else -%} + {{- controller.class|abbr_class|striptags -}} + {{- controller.method ? ' :: ' ~ controller.method -}} + {%- endif -%} + + {%- if link %}{% else %}{% endif %} + {%- else -%} + {{ route|default(controller) }} + {%- endif %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig index 6860230243106..28e3a3c8385af 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig @@ -107,8 +107,8 @@ {% if profile.parent %} -

      - Sub-Request {{ profile.getcollector('request').requestattributes.get('_controller') }} +

      + Sub-Request {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }} {{ collector.events.__section__.duration }} ms Return to parent request @@ -130,7 +130,7 @@ {% for child in profile.children %} {% set events = child.getcollector('time').events %}

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

      diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig index da55ef9190323..31881953d8139 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig @@ -8,7 +8,7 @@ {{ include('@WebProfiler/Icon/translation.svg') }} {% set status_color = collector.countMissings ? 'red' : collector.countFallbacks ? 'yellow' %} {% set error_count = collector.countMissings + collector.countFallbacks %} - {{ error_count ?: collector.countdefines }} + {{ error_count ?: collector.countDefines }} {% endset %} {% set text %} @@ -28,7 +28,7 @@
      Defined messages - {{ collector.countdefines }} + {{ collector.countDefines }}
      {% endset %} @@ -65,7 +65,7 @@
      - {{ collector.countdefines }} + {{ collector.countDefines }} Defined messages
      @@ -96,7 +96,7 @@
      -

      Defined {{ messages_defined|length }}

      +

      Defined {{ collector.countDefines }}

      @@ -114,7 +114,7 @@

      -

      Fallback {{ messages_fallback|length }}

      +

      Fallback {{ collector.countFallbacks }}

      @@ -133,7 +133,7 @@

      -

      Missing {{ messages_missing|length }}

      +

      Missing {{ collector.countMissings }}

      @@ -183,8 +183,7 @@

      {% endif %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig new file mode 100644 index 0000000000000..6153637026261 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig @@ -0,0 +1,98 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% if collector.violationsCount > 0 or collector.calls|length %} + {% set status_color = collector.violationsCount ? 'red' : '' %} + {% set icon %} + {{ include('@WebProfiler/Icon/validator.svg') }} + + {{ collector.violationsCount ?: collector.calls|length }} + + {% endset %} + + {% set text %} +
      + Validator calls + {{ collector.calls|length }} +
      +
      + Number of violations + {{ collector.violationsCount }} +
      + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} + {% endif %} +{% endblock %} + +{% block menu %} + + {{ include('@WebProfiler/Icon/validator.svg') }} + Validator + {% if collector.violationsCount > 0 %} + + {{ collector.violationsCount }} + + {% endif %} + +{% endblock %} + +{% block panel %} +

      Validator calls

      + + {% for call in collector.calls %} +
      + + + + + + + {% if call.violations|length %} + + + + + + + + + + {% for violation in call.violations %} + + + + + + + {% endfor %} +
      PathMessageInvalid valueViolation
      {{ violation.propertyPath }}{{ violation.message }}{{ profiler_dump(violation.seek('invalidValue')) }}{{ profiler_dump(violation) }}
      + {% else %} + No violations + {% endif %} +
      + {% else %} +
      +

      No calls to the validator were collected during this request.

      +
      + {% endfor %} +{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg new file mode 100644 index 0000000000000..5b36ae37e0158 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg new file mode 100644 index 0000000000000..96a5bd7d22e02 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/forward.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg index 51870801cf284..3c863393bc1ab 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/menu.svg @@ -1,3 +1,3 @@ - + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg new file mode 100644 index 0000000000000..54ff4187a4c81 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/redirect.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/validator.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/validator.svg new file mode 100644 index 0000000000000..0b60184f9def5 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/validator.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/bag.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/bag.html.twig index bfcef9440329b..4df5ccf1c6975 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/bag.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/bag.html.twig @@ -9,7 +9,7 @@ {% for key in bag.keys|sort %}
      {{ key }} - {{ profiler_dump(bag.get(key)) }} + {{ profiler_dump(bag.get(key), maxDepth=maxDepth|default(0)) }}
      {% else %}
      diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index cb4fbeed7e2cb..b5f08f869168d 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -1,4 +1,6 @@ - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ExportCommandTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ExportCommandTest.php deleted file mode 100644 index b77baf8d1ef17..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ExportCommandTest.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\WebProfilerBundle\Tests\Command; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\WebProfilerBundle\Command\ExportCommand; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\HttpKernel\Profiler\Profile; - -/** - * @group legacy - */ -class ExportCommandTest extends TestCase -{ - /** - * @expectedException \LogicException - */ - public function testExecuteWithUnknownToken() - { - $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock() - ; - - $helperSet = new HelperSet(); - $helper = $this->getMockBuilder('Symfony\Component\Console\Helper\FormatterHelper')->getMock(); - $helper->expects($this->any())->method('formatSection'); - $helperSet->set($helper, 'formatter'); - - $command = new ExportCommand($profiler); - $command->setHelperSet($helperSet); - - $commandTester = new CommandTester($command); - $commandTester->execute(array('token' => 'TOKEN')); - } - - public function testExecuteWithToken() - { - $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock() - ; - - $profile = new Profile('TOKEN'); - $profiler->expects($this->once())->method('loadProfile')->with('TOKEN')->will($this->returnValue($profile)); - - $helperSet = new HelperSet(); - $helper = $this->getMockBuilder('Symfony\Component\Console\Helper\FormatterHelper')->getMock(); - $helper->expects($this->any())->method('formatSection'); - $helperSet->set($helper, 'formatter'); - - $command = new ExportCommand($profiler); - $command->setHelperSet($helperSet); - - $commandTester = new CommandTester($command); - $commandTester->execute(array('token' => 'TOKEN')); - $this->assertEquals($profiler->export($profile), $commandTester->getDisplay()); - } -} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ImportCommandTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ImportCommandTest.php deleted file mode 100644 index f8e94d9f9f32f..0000000000000 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Command/ImportCommandTest.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\WebProfilerBundle\Tests\Command; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\WebProfilerBundle\Command\ImportCommand; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\HttpKernel\Profiler\Profile; - -/** - * @group legacy - */ -class ImportCommandTest extends TestCase -{ - public function testExecute() - { - $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock() - ; - - $profiler->expects($this->once())->method('import')->will($this->returnValue(new Profile('TOKEN'))); - - $helperSet = new HelperSet(); - $helper = $this->getMockBuilder('Symfony\Component\Console\Helper\FormatterHelper')->getMock(); - $helper->expects($this->any())->method('formatSection'); - $helperSet->set($helper, 'formatter'); - - $command = new ImportCommand($profiler); - $command->setHelperSet($helperSet); - - $commandTester = new CommandTester($command); - $commandTester->execute(array('filename' => __DIR__.'/../Fixtures/profile.data')); - $this->assertRegExp('/Profile "TOKEN" has been successfully imported\./', $commandTester->getDisplay()); - } -} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php index 52845c37d4a85..839c9f21d9fe9 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController; +use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\HttpFoundation\Request; @@ -45,17 +46,17 @@ public function getEmptyTokenCases() ); } - public function testReturns404onTokenNotFound() + /** + * @dataProvider provideCspVariants + */ + public function testReturns404onTokenNotFound($withCsp) { - $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); $profiler = $this ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') ->disableOriginalConstructor() ->getMock(); - $controller = new ProfilerController($urlGenerator, $profiler, $twig, array()); - $profiler ->expects($this->exactly(2)) ->method('loadProfile') @@ -66,6 +67,8 @@ public function testReturns404onTokenNotFound() })) ; + $controller = $this->createController($profiler, $twig, $withCsp); + $response = $controller->toolbarAction(Request::create('/_wdt/found'), 'found'); $this->assertEquals(200, $response->getStatusCode()); @@ -73,16 +76,18 @@ public function testReturns404onTokenNotFound() $this->assertEquals(404, $response->getStatusCode()); } - public function testSearchResult() + /** + * @dataProvider provideCspVariants + */ + public function testSearchResult($withCsp) { - $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); $profiler = $this ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') ->disableOriginalConstructor() ->getMock(); - $controller = new ProfilerController($urlGenerator, $profiler, $twig, array()); + $controller = $this->createController($profiler, $twig, $withCsp); $tokens = array( array( @@ -110,10 +115,10 @@ public function testSearchResult() ->will($this->returnValue($tokens)); $request = Request::create('/_profiler/empty/search/results', 'GET', array( - 'limit' => 2, - 'ip' => '127.0.0.1', - 'method' => 'GET', - 'url' => 'http://example.com/', + 'limit' => 2, + 'ip' => '127.0.0.1', + 'method' => 'GET', + 'url' => 'http://example.com/', )); $twig->expects($this->once()) @@ -124,6 +129,7 @@ public function testSearchResult() 'tokens' => $tokens, 'ip' => '127.0.0.1', 'method' => 'GET', + 'status_code' => null, 'url' => 'http://example.com/', 'start' => null, 'end' => null, @@ -135,4 +141,25 @@ public function testSearchResult() $response = $controller->searchResultsAction($request, 'empty'); $this->assertEquals(200, $response->getStatusCode()); } + + public function provideCspVariants() + { + return array( + array(true), + array(false), + ); + } + + private function createController($profiler, $twig, $withCSP) + { + $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + + if ($withCSP) { + $nonceGenerator = $this->getMockBuilder('Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator')->getMock(); + + return new ProfilerController($urlGenerator, $profiler, $twig, array(), 'bottom', new ContentSecurityPolicyHandler($nonceGenerator)); + } + + return new ProfilerController($urlGenerator, $profiler, $twig, array()); + } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php new file mode 100644 index 0000000000000..51397783699ea --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.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\WebProfilerBundle\Tests\Csp; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class ContentSecurityPolicyHandlerTest extends TestCase +{ + /** + * @dataProvider provideRequestAndResponses + */ + public function testGetNonces($nonce, $expectedNonce, Request $request, Response $response) + { + $cspHandler = new ContentSecurityPolicyHandler($this->mockNonceGenerator($nonce)); + + $this->assertSame($expectedNonce, $cspHandler->getNonces($request, $response)); + } + + /** + * @dataProvider provideRequestAndResponsesForOnKernelResponse + */ + public function testOnKernelResponse($nonce, $expectedNonce, Request $request, Response $response, array $expectedCsp) + { + $cspHandler = new ContentSecurityPolicyHandler($this->mockNonceGenerator($nonce)); + + $this->assertSame($expectedNonce, $cspHandler->updateResponseHeaders($request, $response)); + + $this->assertFalse($response->headers->has('X-SymfonyProfiler-Script-Nonce')); + $this->assertFalse($response->headers->has('X-SymfonyProfiler-Style-Nonce')); + + foreach ($expectedCsp as $header => $value) { + $this->assertSame($value, $response->headers->get($header)); + } + } + + public function provideRequestAndResponses() + { + $nonce = bin2hex(random_bytes(16)); + + $requestScriptNonce = 'request-with-headers-script-nonce'; + $requestStyleNonce = 'request-with-headers-style-nonce'; + + $responseScriptNonce = 'response-with-headers-script-nonce'; + $responseStyleNonce = 'response-with-headers-style-nonce'; + + $requestNonceHeaders = array( + 'X-SymfonyProfiler-Script-Nonce' => $requestScriptNonce, + 'X-SymfonyProfiler-Style-Nonce' => $requestStyleNonce, + ); + $responseNonceHeaders = array( + 'X-SymfonyProfiler-Script-Nonce' => $responseScriptNonce, + 'X-SymfonyProfiler-Style-Nonce' => $responseStyleNonce, + ); + + return array( + array($nonce, array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), $this->createRequest(), $this->createResponse()), + array($nonce, array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce), $this->createRequest($requestNonceHeaders), $this->createResponse($responseNonceHeaders)), + array($nonce, array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce), $this->createRequest($requestNonceHeaders), $this->createResponse()), + array($nonce, array('csp_script_nonce' => $responseScriptNonce, 'csp_style_nonce' => $responseStyleNonce), $this->createRequest(), $this->createResponse($responseNonceHeaders)), + ); + } + + public function provideRequestAndResponsesForOnKernelResponse() + { + $nonce = bin2hex(random_bytes(16)); + + $requestScriptNonce = 'request-with-headers-script-nonce'; + $requestStyleNonce = 'request-with-headers-style-nonce'; + + $responseScriptNonce = 'response-with-headers-script-nonce'; + $responseStyleNonce = 'response-with-headers-style-nonce'; + + $requestNonceHeaders = array( + 'X-SymfonyProfiler-Script-Nonce' => $requestScriptNonce, + 'X-SymfonyProfiler-Style-Nonce' => $requestStyleNonce, + ); + $responseNonceHeaders = array( + 'X-SymfonyProfiler-Script-Nonce' => $responseScriptNonce, + 'X-SymfonyProfiler-Style-Nonce' => $responseStyleNonce, + ); + + return array( + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(), + array('Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null), + ), + array( + $nonce, array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce), + $this->createRequest($requestNonceHeaders), + $this->createResponse($responseNonceHeaders), + array('Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce), + $this->createRequest($requestNonceHeaders), + $this->createResponse(), + array('Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $responseScriptNonce, 'csp_style_nonce' => $responseStyleNonce), + $this->createRequest(), + $this->createResponse($responseNonceHeaders), + array('Content-Security-Policy' => null, 'Content-Security-Policy-Report-Only' => null, 'X-Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('Content-Security-Policy' => 'frame-ancestors https: ; form-action: https:', 'Content-Security-Policy-Report-Only' => 'frame-ancestors http: ; form-action: http:')), + array('Content-Security-Policy' => 'frame-ancestors https: ; form-action: https:', 'Content-Security-Policy-Report-Only' => 'frame-ancestors http: ; form-action: http:', 'X-Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'')), + array('Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain-report-only.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'')), + array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'', 'X-Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'')), + array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'')), + array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'', 'Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('X-Content-Security-Policy' => 'script-src \'self\'')), + array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'sha384-LALALALALAAL\'')), + array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'sha384-LALALALALAAL\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy' => null), + ), + array( + $nonce, + array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), + $this->createRequest(), + $this->createResponse(array('Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'', 'X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'self\'')), + array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\''), + ), + ); + } + + private function createRequest(array $headers = array()) + { + $request = new Request(); + $request->headers->add($headers); + + return $request; + } + + private function createResponse(array $headers = array()) + { + $response = new Response(); + $response->headers->add($headers); + + return $response; + } + + private function mockNonceGenerator($value) + { + $generator = $this->getMockBuilder('Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator')->getMock(); + + $generator->expects($this->any()) + ->method('generate') + ->will($this->returnValue($value)); + + return $generator; + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php index 951c40b36b53f..44ca1ed2cc038 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php @@ -27,10 +27,13 @@ class WebProfilerExtensionTest extends TestCase */ private $container; - public static function assertSaneContainer(Container $container, $message = '') + public static function assertSaneContainer(Container $container, $message = '', $knownPrivates = array()) { $errors = array(); foreach ($container->getServiceIds() as $id) { + if (in_array($id, $knownPrivates, true)) { // for BC with 3.4 + continue; + } try { $container->get($id); } catch (\Exception $e) { @@ -56,6 +59,8 @@ protected function setUp() $this->container->setParameter('kernel.cache_dir', __DIR__); $this->container->setParameter('kernel.debug', false); $this->container->setParameter('kernel.root_dir', __DIR__); + $this->container->setParameter('kernel.charset', 'UTF-8'); + $this->container->setParameter('debug.file_link_format', null); $this->container->setParameter('profiler.class', array('Symfony\\Component\\HttpKernel\\Profiler\\Profiler')); $this->container->register('profiler', $this->getMockClass('Symfony\\Component\\HttpKernel\\Profiler\\Profiler')) ->addArgument(new Definition($this->getMockClass('Symfony\\Component\\HttpKernel\\Profiler\\ProfilerStorageInterface'))); @@ -96,11 +101,11 @@ public function testToolbarConfig($toolbarEnabled, $interceptRedirects, $listene $this->assertSame($listenerInjected, $this->container->has('web_profiler.debug_toolbar')); + $this->assertSaneContainer($this->getDumpedContainer(), '', array('web_profiler.csp.handler')); + if ($listenerInjected) { $this->assertSame($listenerEnabled, $this->container->get('web_profiler.debug_toolbar')->isEnabled()); } - - $this->assertSaneContainer($this->getDumpedContainer()); } public function getDebugModes() diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php index 6f32b2101341d..5e5e7a21fe76b 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; +use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; @@ -32,7 +33,7 @@ public function testInjectToolbar($content, $expected) $response = new Response($content); - $m->invoke($listener, $response, Request::create('/')); + $m->invoke($listener, $response, Request::create('/'), array('csp_script_nonce' => 'scripto', 'csp_style_nonce' => 'stylo')); $this->assertEquals($expected, $response->getContent()); } @@ -276,6 +277,8 @@ protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'h ->method('getRequestFormat') ->will($this->returnValue($requestFormat)); + $request->headers = new HeaderBag(); + if ($hasSession) { $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session')->disableOriginalConstructor()->getMock(); $request->expects($this->any()) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php index 7b8f9a8df51c0..ee0ba44fa74d9 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php @@ -79,28 +79,6 @@ public function testGetNameValidTemplate() $this->assertEquals('FooBundle:Collector:foo.html.twig', $this->templateManager->getName($profile, 'foo')); } - /** - * template should be loaded for 'foo' because other collectors are - * missing in profile or in profiler. - */ - public function testGetTemplates() - { - $profile = $this->mockProfile(); - $profile->expects($this->any()) - ->method('hasCollector') - ->will($this->returnCallback(array($this, 'profilerHasCallback'))); - - $this->profiler->expects($this->any()) - ->method('has') - ->withAnyParameters() - ->will($this->returnCallback(array($this, 'profileHasCollectorCallback'))); - - $result = $this->templateManager->getTemplates($profile); - $this->assertArrayHasKey('foo', $result); - $this->assertArrayNotHasKey('bar', $result); - $this->assertArrayNotHasKey('baz', $result); - } - public function profilerHasCallback($panel) { switch ($panel) { diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php new file mode 100644 index 0000000000000..040d4003f5c54 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.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\WebProfilerBundle\Tests\Resources; + +use PHPUnit\Framework\TestCase; + +class IconTest extends TestCase +{ + /** + * @dataProvider provideIconFilePaths + */ + public function testIconFileContents($iconFilePath) + { + $this->assertRegExp('~.*~s', file_get_contents($iconFilePath), sprintf('The SVG metadata of the %s icon is different than expected (use the same as the other icons).', $iconFilePath)); + } + + public function provideIconFilePaths() + { + return array_map(function ($filePath) { return (array) $filePath; }, glob(__DIR__.'/../../Resources/views/Icon/*.svg')); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php index 8fc6b592deb42..c714ff0642472 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php @@ -11,8 +11,11 @@ namespace Symfony\Bundle\WebProfilerBundle\Twig; -use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; -use Twig\Extension\AbstractExtension; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; +use Twig\Environment; +use Twig\Extension\ProfilerExtension; +use Twig\Profiler\Profile; use Twig\TwigFunction; /** @@ -20,12 +23,40 @@ * * @author Fabien Potencier */ -class WebProfilerExtension extends AbstractExtension +class WebProfilerExtension extends ProfilerExtension { /** - * @var ValueExporter + * @var HtmlDumper */ - private $valueExporter; + private $dumper; + + /** + * @var resource + */ + private $output; + + /** + * @var int + */ + private $stackLevel = 0; + + public function __construct(HtmlDumper $dumper = null) + { + $this->dumper = $dumper ?: new HtmlDumper(); + $this->dumper->setOutput($this->output = fopen('php://memory', 'r+b')); + } + + public function enter(Profile $profile) + { + ++$this->stackLevel; + } + + public function leave(Profile $profile) + { + if (0 === --$this->stackLevel) { + $this->dumper->setOutput($this->output = fopen('php://memory', 'r+b')); + } + } /** * {@inheritdoc} @@ -33,17 +64,41 @@ class WebProfilerExtension extends AbstractExtension public function getFunctions() { return array( - new TwigFunction('profiler_dump', array($this, 'dumpValue')), + new TwigFunction('profiler_dump', array($this, 'dumpData'), array('is_safe' => array('html'), 'needs_environment' => true)), + new TwigFunction('profiler_dump_log', array($this, 'dumpLog'), array('is_safe' => array('html'), 'needs_environment' => true)), ); } - public function dumpValue($value) + public function dumpData(Environment $env, Data $data, $maxDepth = 0) + { + $this->dumper->setCharset($env->getCharset()); + $this->dumper->dump($data, null, array( + 'maxDepth' => $maxDepth, + )); + + $dump = stream_get_contents($this->output, -1, 0); + rewind($this->output); + ftruncate($this->output, 0); + + return str_replace("\nvalueExporter) { - $this->valueExporter = new ValueExporter(); + $message = twig_escape_filter($env, $message); + $message = preg_replace('/"(.*?)"/', '"
      $1"', $message); + + if (null === $context || false === strpos($message, '{')) { + return ''.$message.''; + } + + $replacements = array(); + foreach ($context as $k => $v) { + $k = '{'.twig_escape_filter($env, $k).'}'; + $replacements['"'.$k.'"'] = $replacements['"'.$k.'"'] = $replacements[$k] = $this->dumpData($env, $v); } - return $this->valueExporter->exportValue($value); + return ''.strtr($message, $replacements).''; } /** diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index c0395198481ef..0c626a29dabb5 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -16,17 +16,23 @@ } ], "require": { - "php": ">=5.3.9", - "symfony/http-kernel": "~2.4|~3.0.0", - "symfony/routing": "~2.2|~3.0.0", - "symfony/twig-bridge": "~2.7|~3.0.0", + "php": "^7.1.3", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/routing": "~3.4|~4.0", + "symfony/twig-bridge": "~3.4|~4.0", + "symfony/var-dumper": "~3.4|~4.0", "twig/twig": "~1.34|~2.4" }, "require-dev": { - "symfony/config": "~2.2|~3.0.0", - "symfony/console": "~2.3|~3.0.0", - "symfony/dependency-injection": "~2.2|~3.0.0", - "symfony/stopwatch": "~2.2|~3.0.0" + "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/stopwatch": "~3.4|~4.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/event-dispatcher": "<3.4", + "symfony/var-dumper": "<3.4" }, "autoload": { "psr-4": { "Symfony\\Bundle\\WebProfilerBundle\\": "" }, @@ -37,7 +43,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md new file mode 100644 index 0000000000000..af709a0ee45e3 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md @@ -0,0 +1,12 @@ +CHANGELOG +========= + +3.4.0 +----- + + * WebServer can now use '*' as a wildcard to bind to 0.0.0.0 (INADDR_ANY) + +3.3.0 +----- + + * Added bundle diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php new file mode 100644 index 0000000000000..eed32c5a2055f --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; +use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +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\ExpressionLanguage\ExpressionLanguage; + +/** + * @author Grégoire Pineau + */ +class ServerLogCommand extends Command +{ + private static $bgColor = array('black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow'); + + private $el; + private $handler; + + public function isEnabled() + { + if (!class_exists(ConsoleFormatter::class)) { + return false; + } + + return parent::isEnabled(); + } + + protected function configure() + { + $this->setName('server:log'); + + if (!class_exists(ConsoleFormatter::class)) { + return; + } + + $this + ->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0.0.0.0:9911') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT) + ->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE) + ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"') + ->setDescription('Starts a log server that displays logs in real time') + ->setHelp(<<<'EOF' +%command.name% starts a log server to display in real time the log +messages generated by your application: + + php %command.full_name% + +To get the information as a machine readable format, use the +--filter option: + +php %command.full_name% --filter=port +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $filter = $input->getOption('filter'); + if ($filter) { + if (!class_exists(ExpressionLanguage::class)) { + throw new \LogicException('Package "symfony/expression-language" is required to use the "filter" option.'); + } + $this->el = new ExpressionLanguage(); + } + + $this->handler = new ConsoleHandler($output); + + $this->handler->setFormatter(new ConsoleFormatter(array( + 'format' => str_replace('\n', "\n", $input->getOption('format')), + 'date_format' => $input->getOption('date-format'), + 'colors' => $output->isDecorated(), + 'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(), + ))); + + if (false === strpos($host = $input->getOption('host'), '://')) { + $host = 'tcp://'.$host; + } + + if (!$socket = stream_socket_server($host, $errno, $errstr)) { + throw new \RuntimeException(sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno)); + } + + foreach ($this->getLogs($socket) as $clientId => $message) { + $record = unserialize(base64_decode($message)); + + // Impossible to decode the message, give up. + if (false === $record) { + continue; + } + + if ($filter && !$this->el->evaluate($filter, $record)) { + continue; + } + + $this->displayLog($input, $output, $clientId, $record); + } + } + + private function getLogs($socket) + { + $sockets = array((int) $socket => $socket); + $write = array(); + + while (true) { + $read = $sockets; + stream_select($read, $write, $write, null); + + foreach ($read as $stream) { + if ($socket === $stream) { + $stream = stream_socket_accept($socket); + $sockets[(int) $stream] = $stream; + } elseif (feof($stream)) { + unset($sockets[(int) $stream]); + fclose($stream); + } else { + yield (int) $stream => fgets($stream); + } + } + } + } + + private function displayLog(InputInterface $input, OutputInterface $output, $clientId, array $record) + { + if ($this->handler->isHandling($record)) { + if (isset($record['log_id'])) { + $clientId = unpack('H*', $record['log_id'])[1]; + } + $logBlock = sprintf(' ', self::$bgColor[$clientId % 8]); + $output->write($logBlock); + } + + $this->handler->handle($record); + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php new file mode 100644 index 0000000000000..b3139755b965e --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bundle\WebServerBundle\WebServer; +use Symfony\Bundle\WebServerBundle\WebServerConfig; +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; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Process\Process; + +/** + * Runs Symfony application using a local web server. + * + * @author Michał Pipa + */ +class ServerRunCommand extends Command +{ + private $documentRoot; + private $environment; + + public function __construct($documentRoot = null, $environment = null) + { + $this->documentRoot = $documentRoot; + $this->environment = $environment; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('addressport', InputArgument::OPTIONAL, 'The address to listen to (can be address:port, address, or port)'), + new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root, usually where your front controllers are stored'), + new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), + )) + ->setName('server:run') + ->setDescription('Runs a local web server') + ->setHelp(<<<'EOF' +%command.name% runs a local web server: By default, the server +listens on 127.0.0.1 address and the port number is automatically selected +as the first free port starting from 8000: + + %command.full_name% + +This command blocks the console. If you want to run other commands, stop it by +pressing Control+C or use the non-blocking server:start +command instead. + +Change the default address and port by passing them as an argument: + + %command.full_name% 127.0.0.1:8080 + +Use the --docroot option to change the default docroot directory: + + %command.full_name% --docroot=htdocs/ + +Specify your own router script via the --router option: + + %command.full_name% --router=app/config/router.php + +See also: http://www.php.net/manual/en/features.commandline.webserver.php +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + if (null === $documentRoot = $input->getOption('docroot')) { + if (!$this->documentRoot) { + $io->error('The document root directory must be either passed as first argument of the constructor or through the "--docroot" input option.'); + + return 1; + } + $documentRoot = $this->documentRoot; + } + + if (!$env = $this->environment) { + if ($input->hasOption('env') && !$env = $input->getOption('env')) { + $io->error('The environment must be either passed as second argument of the constructor or through the "--env" input option.'); + + return 1; + } else { + $io->error('The environment must be passed as second argument of the constructor.'); + + return 1; + } + } + + if ('prod' === $env) { + $io->error('Running this server in production environment is NOT recommended!'); + } + + $callback = null; + $disableOutput = false; + if ($output->isQuiet()) { + $disableOutput = true; + } else { + $callback = function ($type, $buffer) use ($output) { + if (Process::ERR === $type && $output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + $output->write($buffer, false, OutputInterface::OUTPUT_RAW); + }; + } + + try { + $server = new WebServer(); + $config = new WebServerConfig($documentRoot, $env, $input->getArgument('addressport'), $input->getOption('router')); + + $io->success(sprintf('Server listening on http://%s', $config->getAddress())); + $io->comment('Quit the server with CONTROL-C.'); + + $exitCode = $server->run($config, $disableOutput, $callback); + } catch (\Exception $e) { + $io->error($e->getMessage()); + + return 1; + } + + return $exitCode; + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php new file mode 100644 index 0000000000000..1b1d3070e7fc8 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bundle\WebServerBundle\WebServer; +use Symfony\Bundle\WebServerBundle\WebServerConfig; +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\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Runs a local web server in a background process. + * + * @author Christian Flothmann + */ +class ServerStartCommand extends Command +{ + private $documentRoot; + private $environment; + + public function __construct($documentRoot = null, $environment = null) + { + $this->documentRoot = $documentRoot; + $this->environment = $environment; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('server:start') + ->setDefinition(array( + new InputArgument('addressport', InputArgument::OPTIONAL, 'The address to listen to (can be address:port, address, or port)'), + new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root'), + new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), + new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), + )) + ->setDescription('Starts a local web server in the background') + ->setHelp(<<<'EOF' +%command.name% runs a local web server: By default, the server +listens on 127.0.0.1 address and the port number is automatically selected +as the first free port starting from 8000: + + php %command.full_name% + +The server is run in the background and you can keep executing other commands. +Execute server:stop to stop it. + +Change the default address and port by passing them as an argument: + + php %command.full_name% 127.0.0.1:8080 + +Use the --docroot option to change the default docroot directory: + + php %command.full_name% --docroot=htdocs/ + +Specify your own router script via the --router option: + + php %command.full_name% --router=app/config/router.php + +See also: http://www.php.net/manual/en/features.commandline.webserver.php +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + if (!extension_loaded('pcntl')) { + $io->error(array( + 'This command needs the pcntl extension to run.', + 'You can either install it or use the "server:run" command instead.', + )); + + if ($io->confirm('Do you want to execute server:run immediately?', false)) { + return $this->getApplication()->find('server:run')->run($input, $output); + } + + return 1; + } + + if (null === $documentRoot = $input->getOption('docroot')) { + if (!$this->documentRoot) { + $io->error('The document root directory must be either passed as first argument of the constructor or through the "docroot" input option.'); + + return 1; + } + $documentRoot = $this->documentRoot; + } + + if (!$env = $this->environment) { + if ($input->hasOption('env') && !$env = $input->getOption('env')) { + $io->error('The environment must be either passed as second argument of the constructor or through the "--env" input option.'); + + return 1; + } else { + $io->error('The environment must be passed as second argument of the constructor.'); + + return 1; + } + } + + if ('prod' === $env) { + $io->error('Running this server in production environment is NOT recommended!'); + } + + try { + $server = new WebServer(); + if ($server->isRunning($input->getOption('pidfile'))) { + $io->error(sprintf('The web server is already running (listening on http://%s).', $server->getAddress($input->getOption('pidfile')))); + + return 1; + } + + $config = new WebServerConfig($documentRoot, $env, $input->getArgument('addressport'), $input->getOption('router')); + + if (WebServer::STARTED === $server->start($config, $input->getOption('pidfile'))) { + $io->success(sprintf('Server listening on http://%s', $config->getAddress())); + } + } catch (\Exception $e) { + $io->error($e->getMessage()); + + return 1; + } + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php new file mode 100644 index 0000000000000..7c9f6980e8bb5 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\Command; + +use Symfony\Bundle\WebServerBundle\WebServer; +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\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Shows the status of a process that is running PHP's built-in web server in + * the background. + * + * @author Christian Flothmann + */ +class ServerStatusCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('server:status') + ->setDefinition(array( + new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), + new InputOption('filter', null, InputOption::VALUE_REQUIRED, 'The value to display (one of port, host, or address)'), + )) + ->setDescription('Outputs the status of the local web server for the given address') + ->setHelp(<<<'EOF' +%command.name% shows the details of the given local web +server, such as the address and port where it is listening to: + + php %command.full_name% + +To get the information as a machine readable format, use the +--filter option: + +php %command.full_name% --filter=port + +Supported values are port, host, and address. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + $server = new WebServer(); + if ($filter = $input->getOption('filter')) { + if ($server->isRunning($input->getOption('pidfile'))) { + list($host, $port) = explode(':', $address = $server->getAddress($input->getOption('pidfile'))); + if ('address' === $filter) { + $output->write($address); + } elseif ('host' === $filter) { + $output->write($host); + } elseif ('port' === $filter) { + $output->write($port); + } else { + throw new \InvalidArgumentException(sprintf('"%s" is not a valid filter.', $filter)); + } + } else { + return 1; + } + } else { + if ($server->isRunning($input->getOption('pidfile'))) { + $io->success(sprintf('Web server still listening on http://%s', $server->getAddress($input->getOption('pidfile')))); + } else { + $io->warning('No web server is listening.'); + + return 1; + } + } + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php new file mode 100644 index 0000000000000..fc5e2fd563dfe --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.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\WebServerBundle\Command; + +use Symfony\Bundle\WebServerBundle\WebServer; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Stops a background process running a local web server. + * + * @author Christian Flothmann + */ +class ServerStopCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('server:stop') + ->setDefinition(array( + new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), + )) + ->setDescription('Stops the local web server that was started with the server:start command') + ->setHelp(<<<'EOF' +%command.name% stops the local web server: + + php %command.full_name% +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + try { + $server = new WebServer(); + $server->stop($input->getOption('pidfile')); + $io->success('Stopped the web server.'); + } catch (\Exception $e) { + $io->error($e->getMessage()); + + return 1; + } + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php b/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php new file mode 100644 index 0000000000000..b26236bcccef9 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Config\FileLocator; + +/** + * @author Robin Chalas + */ +class WebServerExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container) + { + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('webserver.xml'); + } +} diff --git a/src/Symfony/Bridge/Swiftmailer/LICENSE b/src/Symfony/Bundle/WebServerBundle/LICENSE similarity index 100% rename from src/Symfony/Bridge/Swiftmailer/LICENSE rename to src/Symfony/Bundle/WebServerBundle/LICENSE diff --git a/src/Symfony/Bundle/WebServerBundle/README.md b/src/Symfony/Bundle/WebServerBundle/README.md new file mode 100644 index 0000000000000..09e514dcb809d --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/README.md @@ -0,0 +1,15 @@ +WebServerBundle +=============== + +WebServerBundle provides commands for running applications using the PHP +built-in web server. It simplifies your local development setup because you +don't have to configure a proper web server such as Apache or Nginx to run your +application. + +Resources +--------- + + * [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/Bundle/WebServerBundle/Resources/config/webserver.xml b/src/Symfony/Bundle/WebServerBundle/Resources/config/webserver.xml new file mode 100644 index 0000000000000..1aef987ceb6d5 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Resources/config/webserver.xml @@ -0,0 +1,30 @@ + + + + + + + + + %kernel.project_dir%/public + %kernel.environment% + + + + + %kernel.project_dir%/public + %kernel.environment% + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/WebServerBundle/Resources/router.php b/src/Symfony/Bundle/WebServerBundle/Resources/router.php new file mode 100644 index 0000000000000..187be0b8366ac --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/Resources/router.php @@ -0,0 +1,44 @@ + + * + * 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 + */ + +// Workaround https://bugs.php.net/64566 +if (ini_get('auto_prepend_file') && !in_array(realpath(ini_get('auto_prepend_file')), get_included_files(), true)) { + require ini_get('auto_prepend_file'); +} + +if (is_file($_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { + return false; +} + +$script = getenv('APP_FRONT_CONTROLLER') ?: 'index.php'; + +$_SERVER = array_merge($_SERVER, $_ENV); +$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$script; + +// Since we are rewriting to app_dev.php, adjust SCRIPT_NAME and PHP_SELF accordingly +$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR.$script; +$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR.$script; + +require $script; + +error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4); diff --git a/src/Symfony/Bundle/WebServerBundle/WebServer.php b/src/Symfony/Bundle/WebServerBundle/WebServer.php new file mode 100644 index 0000000000000..8edbe2bf56ec2 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/WebServer.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\WebServerBundle; + +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * Manages a local HTTP web server. + * + * @author Fabien Potencier + */ +class WebServer +{ + const STARTED = 0; + const STOPPED = 1; + + public function run(WebServerConfig $config, $disableOutput = true, callable $callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException(sprintf('A process is already listening on http://%s.', $config->getAddress())); + } + + $process = $this->createServerProcess($config); + if ($disableOutput) { + $process->disableOutput(); + $callback = null; + } else { + try { + $process->setTty(true); + $callback = null; + } catch (RuntimeException $e) { + } + } + + $process->run($callback); + + if (!$process->isSuccessful()) { + $error = 'Server terminated unexpectedly.'; + if ($process->isOutputDisabled()) { + $error .= ' Run the command again with -v option for more details.'; + } + + throw new \RuntimeException($error); + } + } + + public function start(WebServerConfig $config, $pidFile = null) + { + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + if ($this->isRunning($pidFile)) { + throw new \RuntimeException(sprintf('A process is already listening on http://%s.', $config->getAddress())); + } + + $pid = pcntl_fork(); + + if ($pid < 0) { + throw new \RuntimeException('Unable to start the server process.'); + } + + if ($pid > 0) { + return self::STARTED; + } + + if (posix_setsid() < 0) { + throw new \RuntimeException('Unable to set the child process as session leader.'); + } + + $process = $this->createServerProcess($config); + $process->disableOutput(); + $process->start(); + + if (!$process->isRunning()) { + throw new \RuntimeException('Unable to start the server process.'); + } + + file_put_contents($pidFile, $config->getAddress()); + + // stop the web server when the lock file is removed + while ($process->isRunning()) { + if (!file_exists($pidFile)) { + $process->stop(); + } + + sleep(1); + } + + return self::STOPPED; + } + + public function stop($pidFile = null) + { + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + if (!file_exists($pidFile)) { + throw new \RuntimeException('No web server is listening.'); + } + + unlink($pidFile); + } + + public function getAddress($pidFile = null) + { + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + if (!file_exists($pidFile)) { + return false; + } + + return file_get_contents($pidFile); + } + + public function isRunning($pidFile = null) + { + $pidFile = $pidFile ?: $this->getDefaultPidFile(); + if (!file_exists($pidFile)) { + return false; + } + + $address = file_get_contents($pidFile); + $pos = strrpos($address, ':'); + $hostname = substr($address, 0, $pos); + $port = substr($address, $pos + 1); + if (false !== $fp = @fsockopen($hostname, $port, $errno, $errstr, 1)) { + fclose($fp); + + return true; + } + + unlink($pidFile); + + return false; + } + + /** + * @return Process The process + */ + private function createServerProcess(WebServerConfig $config) + { + $finder = new PhpExecutableFinder(); + if (false === $binary = $finder->find()) { + throw new \RuntimeException('Unable to find the PHP binary.'); + } + + $process = new Process(array($binary, '-S', $config->getAddress(), $config->getRouter())); + $process->setWorkingDirectory($config->getDocumentRoot()); + $process->setTimeout(null); + + return $process; + } + + private function getDefaultPidFile() + { + return getcwd().'/.web-server-pid'; + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php b/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php new file mode 100644 index 0000000000000..3e3f41f45c978 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class WebServerBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php new file mode 100644 index 0000000000000..6c17d110feb01 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebServerBundle; + +/** + * @author Fabien Potencier + */ +class WebServerConfig +{ + private $hostname; + private $port; + private $documentRoot; + private $env; + private $router; + + public function __construct($documentRoot, $env, $address = null, $router = null) + { + if (!is_dir($documentRoot)) { + throw new \InvalidArgumentException(sprintf('The document root directory "%s" does not exist.', $documentRoot)); + } + + if (null === $file = $this->findFrontController($documentRoot, $env)) { + throw new \InvalidArgumentException(sprintf('Unable to find the front controller under "%s" (none of these files exist: %s).', $documentRoot, implode(', ', $this->getFrontControllerFileNames($env)))); + } + + putenv('APP_FRONT_CONTROLLER='.$file); + + $this->documentRoot = $documentRoot; + $this->env = $env; + + if (null !== $router) { + $absoluteRouterPath = realpath($router); + + if (false === $absoluteRouterPath) { + throw new \InvalidArgumentException(sprintf('Router script "%s" does not exist.', $router)); + } + + $this->router = $absoluteRouterPath; + } else { + $this->router = __DIR__.'/Resources/router.php'; + } + + if (null === $address) { + $this->hostname = '127.0.0.1'; + $this->port = $this->findBestPort(); + } elseif (false !== $pos = strrpos($address, ':')) { + $this->hostname = substr($address, 0, $pos); + if ('*' === $this->hostname) { + $this->hostname = '0.0.0.0'; + } + $this->port = substr($address, $pos + 1); + } elseif (ctype_digit($address)) { + $this->hostname = '127.0.0.1'; + $this->port = $address; + } else { + $this->hostname = $address; + $this->port = $this->findBestPort(); + } + + if (!ctype_digit($this->port)) { + throw new \InvalidArgumentException(sprintf('Port "%s" is not valid.', $this->port)); + } + } + + public function getDocumentRoot() + { + return $this->documentRoot; + } + + public function getEnv() + { + return $this->env; + } + + public function getRouter() + { + return $this->router; + } + + public function getHostname() + { + return $this->hostname; + } + + public function getPort() + { + return $this->port; + } + + public function getAddress() + { + return $this->hostname.':'.$this->port; + } + + private function findFrontController($documentRoot, $env) + { + $fileNames = $this->getFrontControllerFileNames($env); + + foreach ($fileNames as $fileName) { + if (file_exists($documentRoot.'/'.$fileName)) { + return $fileName; + } + } + } + + private function getFrontControllerFileNames($env) + { + return array('app_'.$env.'.php', 'app.php', 'index_'.$env.'.php', 'index.php'); + } + + private function findBestPort() + { + $port = 8000; + while (false !== $fp = @fsockopen($this->hostname, $port, $errno, $errstr, 1)) { + fclose($fp); + if ($port++ >= 8100) { + throw new \RuntimeException('Unable to find a port available to run the web server.'); + } + } + + return $port; + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/composer.json b/src/Symfony/Bundle/WebServerBundle/composer.json new file mode 100644 index 0000000000000..dda1c9245ffee --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/composer.json @@ -0,0 +1,39 @@ +{ + "name": "symfony/web-server-bundle", + "type": "symfony-bundle", + "description": "Symfony WebServerBundle", + "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": "^7.1.3", + "symfony/console": "~3.4|~4.0", + "symfony/http-kernel": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0" + }, + "autoload": { + "psr-4": { "Symfony\\Bundle\\WebServerBundle\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "conflict": { + "symfony/dependency-injection": "<3.4" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + } +} diff --git a/src/Symfony/Bundle/WebServerBundle/phpunit.xml.dist b/src/Symfony/Bundle/WebServerBundle/phpunit.xml.dist new file mode 100644 index 0000000000000..9d4d021172823 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Asset/CHANGELOG.md b/src/Symfony/Component/Asset/CHANGELOG.md index 619a423402ada..72030a9d65b16 100644 --- a/src/Symfony/Component/Asset/CHANGELOG.md +++ b/src/Symfony/Component/Asset/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.3.0 +----- + * Added `JsonManifestVersionStrategy` as a way to read final, + versioned paths from a JSON manifest file. + 2.7.0 ----- diff --git a/src/Symfony/Component/Asset/PathPackage.php b/src/Symfony/Component/Asset/PathPackage.php index 5621986fb58b6..31cbc5df507f2 100644 --- a/src/Symfony/Component/Asset/PathPackage.php +++ b/src/Symfony/Component/Asset/PathPackage.php @@ -31,6 +31,7 @@ class PathPackage extends Package /** * @param string $basePath The base path to be prepended to relative paths * @param VersionStrategyInterface $versionStrategy The version strategy + * @param ContextInterface|null $context The context */ public function __construct($basePath, VersionStrategyInterface $versionStrategy, ContextInterface $context = null) { diff --git a/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php b/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php new file mode 100644 index 0000000000000..9da2b4ada2856 --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Tests\VersionStrategy; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; + +class JsonManifestVersionStrategyTest extends TestCase +{ + public function testGetVersion() + { + $strategy = $this->createStrategy('manifest-valid.json'); + + $this->assertEquals('main.123abc.js', $strategy->getVersion('main.js')); + } + + public function testApplyVersion() + { + $strategy = $this->createStrategy('manifest-valid.json'); + + $this->assertEquals('css/styles.555def.css', $strategy->getVersion('css/styles.css')); + } + + public function testApplyVersionWhenKeyDoesNotExistInManifest() + { + $strategy = $this->createStrategy('manifest-valid.json'); + + $this->assertEquals('css/other.css', $strategy->getVersion('css/other.css')); + } + + /** + * @expectedException \RuntimeException + */ + public function testMissingManifestFileThrowsException() + { + $strategy = $this->createStrategy('non-existent-file.json'); + $strategy->getVersion('main.js'); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Error parsing JSON + */ + public function testManifestFileWithBadJSONThrowsException() + { + $strategy = $this->createStrategy('manifest-invalid.json'); + $strategy->getVersion('main.js'); + } + + private function createStrategy($manifestFilename) + { + return new JsonManifestVersionStrategy(__DIR__.'/../fixtures/'.$manifestFilename); + } +} diff --git a/src/Symfony/Component/Asset/Tests/fixtures/manifest-invalid.json b/src/Symfony/Component/Asset/Tests/fixtures/manifest-invalid.json new file mode 100644 index 0000000000000..feed937ea1215 --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/fixtures/manifest-invalid.json @@ -0,0 +1,4 @@ +{ + "main.js": main.123abc.js", + "css/styles.css": "css/styles.555def.css" +} diff --git a/src/Symfony/Component/Asset/Tests/fixtures/manifest-valid.json b/src/Symfony/Component/Asset/Tests/fixtures/manifest-valid.json new file mode 100644 index 0000000000000..546a0066d31ee --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/fixtures/manifest-valid.json @@ -0,0 +1,4 @@ +{ + "main.js": "main.123abc.js", + "css/styles.css": "css/styles.555def.css" +} diff --git a/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php new file mode 100644 index 0000000000000..378ad54346d7f --- /dev/null +++ b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\VersionStrategy; + +/** + * Reads the versioned path of an asset from a JSON manifest file. + * + * For example, the manifest file might look like this: + * { + * "main.js": "main.abc123.js", + * "css/styles.css": "css/styles.555abc.css" + * } + * + * You could then ask for the version of "main.js" or "css/styles.css". + */ +class JsonManifestVersionStrategy implements VersionStrategyInterface +{ + private $manifestPath; + private $manifestData; + + /** + * @param string $manifestPath Absolute path to the manifest file + */ + public function __construct($manifestPath) + { + $this->manifestPath = $manifestPath; + } + + /** + * With a manifest, we don't really know or care about what + * the version is. Instead, this returns the path to the + * versioned file. + */ + public function getVersion($path) + { + return $this->applyVersion($path); + } + + public function applyVersion($path) + { + return $this->getManifestPath($path) ?: $path; + } + + private function getManifestPath($path) + { + if (null === $this->manifestData) { + if (!file_exists($this->manifestPath)) { + throw new \RuntimeException(sprintf('Asset manifest file "%s" does not exist.', $this->manifestPath)); + } + + $this->manifestData = json_decode(file_get_contents($this->manifestPath), true); + if (0 < json_last_error()) { + throw new \RuntimeException(sprintf('Error parsing JSON from asset manifest file "%s" - %s', $this->manifestPath, json_last_error_msg())); + } + } + + return isset($this->manifestData[$path]) ? $this->manifestData[$path] : null; + } +} diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json index 6c6b17a10f970..e60d306d6d62b 100644 --- a/src/Symfony/Component/Asset/composer.json +++ b/src/Symfony/Component/Asset/composer.json @@ -16,13 +16,14 @@ } ], "require": { - "php": ">=5.3.9" + "php": "^7.1.3" }, "suggest": { "symfony/http-foundation": "" }, "require-dev": { - "symfony/http-foundation": "~2.4" + "symfony/http-foundation": "~3.4|~4.0", + "symfony/http-kernel": "~3.4|~4.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Asset\\": "" }, @@ -33,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Symfony/Component/BrowserKit/CHANGELOG.md b/src/Symfony/Component/BrowserKit/CHANGELOG.md index d2b1074029fd1..47c847d0617e5 100644 --- a/src/Symfony/Component/BrowserKit/CHANGELOG.md +++ b/src/Symfony/Component/BrowserKit/CHANGELOG.md @@ -1,6 +1,23 @@ CHANGELOG ========= +3.4.0 +----- + + * [BC BREAK] Client will skip redirects during history navigation + (back and forward calls) according to W3C Browsers recommendation + +3.3.0 +----- + + * [BC BREAK] The request method is dropped from POST to GET when the response + status code is 301. + +3.2.0 +----- + + * Client HTTP user agent has been changed to 'Symfony BrowserKit' + 2.3.0 ----- diff --git a/src/Symfony/Component/BrowserKit/Client.php b/src/Symfony/Component/BrowserKit/Client.php index 9b9e31829694e..5ceabb569a254 100644 --- a/src/Symfony/Component/BrowserKit/Client.php +++ b/src/Symfony/Component/BrowserKit/Client.php @@ -42,6 +42,7 @@ abstract class Client private $maxRedirects = -1; private $redirectCount = 0; + private $redirects = array(); private $isMainRequest = true; /** @@ -123,7 +124,7 @@ public function insulate($insulated = true) public function setServerParameters(array $server) { $this->server = array_merge(array( - 'HTTP_USER_AGENT' => 'Symfony2 BrowserKit', + 'HTTP_USER_AGENT' => 'Symfony BrowserKit', ), $server); } @@ -328,6 +329,8 @@ public function request($method, $uri, array $parameters = array(), array $files } if ($this->followRedirects && $this->redirect) { + $this->redirects[serialize($this->history->current())] = true; + return $this->crawler = $this->followRedirect(); } @@ -430,7 +433,11 @@ protected function createCrawlerFromContent($uri, $content, $type) */ public function back() { - return $this->requestFromRequest($this->history->back(), false); + do { + $request = $this->history->back(); + } while (array_key_exists(serialize($request), $this->redirects)); + + return $this->requestFromRequest($request, false); } /** @@ -440,7 +447,11 @@ public function back() */ public function forward() { - return $this->requestFromRequest($this->history->forward(), false); + do { + $request = $this->history->forward(); + } while (array_key_exists(serialize($request), $this->redirects)); + + return $this->requestFromRequest($request, false); } /** @@ -475,7 +486,7 @@ public function followRedirect() $request = $this->internalRequest; - if (in_array($this->internalResponse->getStatus(), array(302, 303))) { + if (in_array($this->internalResponse->getStatus(), array(301, 302, 303))) { $method = 'GET'; $files = array(); $content = null; diff --git a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php index b48c40a3d7faa..9c7267e83b721 100644 --- a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php @@ -433,7 +433,7 @@ public function testFollowRedirectWithHeaders() { $headers = array( 'HTTP_HOST' => 'www.example.com', - 'HTTP_USER_AGENT' => 'Symfony2 BrowserKit', + 'HTTP_USER_AGENT' => 'Symfony BrowserKit', 'CONTENT_TYPE' => 'application/vnd.custom+xml', 'HTTPS' => false, ); @@ -460,7 +460,7 @@ public function testFollowRedirectWithPort() { $headers = array( 'HTTP_HOST' => 'www.example.com:8080', - 'HTTP_USER_AGENT' => 'Symfony2 BrowserKit', + 'HTTP_USER_AGENT' => 'Symfony BrowserKit', 'HTTPS' => false, 'HTTP_REFERER' => 'http://www.example.com:8080/', ); @@ -510,6 +510,28 @@ public function testFollowRedirectWithPostMethod() $this->assertEquals('POST', $client->getRequest()->getMethod(), '->followRedirect() keeps request method'); } + public function testFollowRedirectDropPostMethod() + { + $parameters = array('foo' => 'bar'); + $files = array('myfile.foo' => 'baz'); + $server = array('X_TEST_FOO' => 'bazbar'); + $content = 'foobarbaz'; + + $client = new TestClient(); + + foreach (array(301, 302, 303) as $code) { + $client->setNextResponse(new Response('', $code, array('Location' => 'http://www.example.com/redirected'))); + $client->request('POST', 'http://www.example.com/foo/foobar', $parameters, $files, $server, $content); + + $this->assertEquals('http://www.example.com/redirected', $client->getRequest()->getUri(), '->followRedirect() follows a redirect with POST method on response code: '.$code.'.'); + $this->assertEmpty($client->getRequest()->getParameters(), '->followRedirect() drops parameters with POST method on response code: '.$code.'.'); + $this->assertEmpty($client->getRequest()->getFiles(), '->followRedirect() drops files with POST method on response code: '.$code.'.'); + $this->assertArrayHasKey('X_TEST_FOO', $client->getRequest()->getServer(), '->followRedirect() keeps $_SERVER with POST method on response code: '.$code.'.'); + $this->assertEmpty($client->getRequest()->getContent(), '->followRedirect() drops content with POST method on response code: '.$code.'.'); + $this->assertEquals('GET', $client->getRequest()->getMethod(), '->followRedirect() drops request method to GET on response code: '.$code.'.'); + } + } + public function testBack() { $client = new TestClient(); @@ -551,6 +573,25 @@ public function testForward() $this->assertEquals($content, $client->getRequest()->getContent(), '->forward() keeps content'); } + public function testBackAndFrowardWithRedirects() + { + $client = new TestClient(); + + $client->request('GET', 'http://www.example.com/foo'); + $client->setNextResponse(new Response('', 301, array('Location' => 'http://www.example.com/redirected'))); + $client->request('GET', 'http://www.example.com/bar'); + + $this->assertEquals('http://www.example.com/redirected', $client->getRequest()->getUri(), 'client followed redirect'); + + $client->back(); + + $this->assertEquals('http://www.example.com/foo', $client->getRequest()->getUri(), '->back() goes back in the history skipping redirects'); + + $client->forward(); + + $this->assertEquals('http://www.example.com/redirected', $client->getRequest()->getUri(), '->forward() goes forward in the history skipping redirects'); + } + public function testReload() { $client = new TestClient(); @@ -603,7 +644,7 @@ public function testGetServerParameter() { $client = new TestClient(); $this->assertEquals('', $client->getServerParameter('HTTP_HOST')); - $this->assertEquals('Symfony2 BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); + $this->assertEquals('Symfony BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); $this->assertEquals('testvalue', $client->getServerParameter('testkey', 'testvalue')); } @@ -612,7 +653,7 @@ public function testSetServerParameter() $client = new TestClient(); $this->assertEquals('', $client->getServerParameter('HTTP_HOST')); - $this->assertEquals('Symfony2 BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); + $this->assertEquals('Symfony BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); $client->setServerParameter('HTTP_HOST', 'testhost'); $this->assertEquals('testhost', $client->getServerParameter('HTTP_HOST')); @@ -626,7 +667,7 @@ public function testSetServerParameterInRequest() $client = new TestClient(); $this->assertEquals('', $client->getServerParameter('HTTP_HOST')); - $this->assertEquals('Symfony2 BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); + $this->assertEquals('Symfony BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); $client->request('GET', 'https://www.example.com/https/www.example.com', array(), array(), array( 'HTTP_HOST' => 'testhost', @@ -636,7 +677,7 @@ public function testSetServerParameterInRequest() )); $this->assertEquals('', $client->getServerParameter('HTTP_HOST')); - $this->assertEquals('Symfony2 BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); + $this->assertEquals('Symfony BrowserKit', $client->getServerParameter('HTTP_USER_AGENT')); $this->assertEquals('http://www.example.com/https/www.example.com', $client->getRequest()->getUri()); diff --git a/src/Symfony/Component/BrowserKit/composer.json b/src/Symfony/Component/BrowserKit/composer.json index 64bfb0eefaf54..eda8a9d7c3acd 100644 --- a/src/Symfony/Component/BrowserKit/composer.json +++ b/src/Symfony/Component/BrowserKit/composer.json @@ -16,12 +16,12 @@ } ], "require": { - "php": ">=5.3.9", - "symfony/dom-crawler": "~2.1|~3.0.0" + "php": "^7.1.3", + "symfony/dom-crawler": "~3.4|~4.0" }, "require-dev": { - "symfony/process": "~2.3.34|^2.7.6|~3.0.0", - "symfony/css-selector": "^2.0.5|~3.0.0" + "symfony/process": "~3.4|~4.0", + "symfony/css-selector": "~3.4|~4.0" }, "suggest": { "symfony/process": "" @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Symfony/Component/Cache/.gitignore b/src/Symfony/Component/Cache/.gitignore new file mode 100644 index 0000000000000..5414c2c655e72 --- /dev/null +++ b/src/Symfony/Component/Cache/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor/ diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php new file mode 100644 index 0000000000000..9bc05fd2b30fd --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -0,0 +1,301 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\AbstractTrait; + +/** + * @author Nicolas Grekas + */ +abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface +{ + use AbstractTrait; + + private static $apcuSupported; + private static $phpFilesSupported; + + private $createCacheItem; + private $mergeByLifetime; + + /** + * @param string $namespace + * @param int $defaultLifetime + */ + protected function __construct($namespace = '', $defaultLifetime = 0) + { + $this->namespace = '' === $namespace ? '' : $this->getId($namespace).':'; + if (null !== $this->maxIdLength && strlen($namespace) > $this->maxIdLength - 24) { + throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, strlen($namespace), $namespace)); + } + $this->createCacheItem = \Closure::bind( + function ($key, $value, $isHit) use ($defaultLifetime) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + $item->defaultLifetime = $defaultLifetime; + + return $item; + }, + null, + CacheItem::class + ); + $getId = function ($key) { return $this->getId((string) $key); }; + $this->mergeByLifetime = \Closure::bind( + function ($deferred, $namespace, &$expiredIds) use ($getId) { + $byLifetime = array(); + $now = time(); + $expiredIds = array(); + + foreach ($deferred as $key => $item) { + if (null === $item->expiry) { + $byLifetime[0 < $item->defaultLifetime ? $item->defaultLifetime : 0][$getId($key)] = $item->value; + } elseif ($item->expiry > $now) { + $byLifetime[$item->expiry - $now][$getId($key)] = $item->value; + } else { + $expiredIds[] = $getId($key); + } + } + + return $byLifetime; + }, + null, + CacheItem::class + ); + } + + /** + * @param string $namespace + * @param int $defaultLifetime + * @param string $version + * @param string $directory + * @param LoggerInterface|null $logger + * + * @return AdapterInterface + */ + public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null) + { + if (null === self::$apcuSupported) { + self::$apcuSupported = ApcuAdapter::isSupported(); + } + + if (!self::$apcuSupported && null === self::$phpFilesSupported) { + self::$phpFilesSupported = PhpFilesAdapter::isSupported(); + } + + if (self::$phpFilesSupported) { + $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory); + if (null !== $logger) { + $opcache->setLogger($logger); + } + + return $opcache; + } + + $fs = new FilesystemAdapter($namespace, $defaultLifetime, $directory); + if (null !== $logger) { + $fs->setLogger($logger); + } + if (!self::$apcuSupported) { + return $fs; + } + + $apcu = new ApcuAdapter($namespace, (int) $defaultLifetime / 5, $version); + if ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { + $apcu->setLogger(new NullLogger()); + } elseif (null !== $logger) { + $apcu->setLogger($logger); + } + + return new ChainAdapter(array($apcu, $fs)); + } + + public static function createConnection($dsn, array $options = array()) + { + if (!is_string($dsn)) { + throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, gettype($dsn))); + } + if (0 === strpos($dsn, 'redis://')) { + return RedisAdapter::createConnection($dsn, $options); + } + if (0 === strpos($dsn, 'memcached://')) { + return MemcachedAdapter::createConnection($dsn, $options); + } + + throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn)); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + if ($this->deferred) { + $this->commit(); + } + $id = $this->getId($key); + + $f = $this->createCacheItem; + $isHit = false; + $value = null; + + try { + foreach ($this->doFetch(array($id)) as $value) { + $isHit = true; + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch key "{key}"', array('key' => $key, 'exception' => $e)); + } + + return $f($key, $value, $isHit); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + if ($this->deferred) { + $this->commit(); + } + $ids = array(); + + foreach ($keys as $key) { + $ids[] = $this->getId($key); + } + try { + $items = $this->doFetch($ids); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch requested items', array('keys' => $keys, 'exception' => $e)); + $items = array(); + } + $ids = array_combine($ids, $keys); + + return $this->generateItems($items, $ids); + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return $this->commit(); + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return true; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + $ok = true; + $byLifetime = $this->mergeByLifetime; + $byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds); + $retry = $this->deferred = array(); + + if ($expiredIds) { + $this->doDelete($expiredIds); + } + foreach ($byLifetime as $lifetime => $values) { + try { + $e = $this->doSave($values, $lifetime); + } catch (\Exception $e) { + } + if (true === $e || array() === $e) { + continue; + } + if (is_array($e) || 1 === count($values)) { + foreach (is_array($e) ? $e : array_keys($values) as $id) { + $ok = false; + $v = $values[$id]; + $type = is_object($v) ? get_class($v) : gettype($v); + CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', array('key' => substr($id, strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null)); + } + } else { + foreach ($values as $id => $v) { + $retry[$lifetime][] = $id; + } + } + } + + // When bulk-save failed, retry each item individually + foreach ($retry as $lifetime => $ids) { + foreach ($ids as $id) { + try { + $v = $byLifetime[$lifetime][$id]; + $e = $this->doSave(array($id => $v), $lifetime); + } catch (\Exception $e) { + } + if (true === $e || array() === $e) { + continue; + } + $ok = false; + $type = is_object($v) ? get_class($v) : gettype($v); + CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', array('key' => substr($id, strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null)); + } + } + + return $ok; + } + + public function __destruct() + { + if ($this->deferred) { + $this->commit(); + } + } + + private function generateItems($items, &$keys) + { + $f = $this->createCacheItem; + + try { + foreach ($items as $id => $value) { + if (!isset($keys[$id])) { + $id = key($keys); + } + $key = $keys[$id]; + unset($keys[$id]); + yield $key => $f($key, $value, true); + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch requested items', array('keys' => array_values($keys), 'exception' => $e)); + } + + foreach ($keys as $key) { + yield $key => $f($key, null, false); + } + } +} diff --git a/src/Symfony/Component/Cache/Adapter/AdapterInterface.php b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php new file mode 100644 index 0000000000000..274ebec1ef445 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; + +/** + * Interface for adapters managing instances of Symfony's CacheItem. + * + * @author Kévin Dunglas + */ +interface AdapterInterface extends CacheItemPoolInterface +{ + /** + * {@inheritdoc} + * + * @return CacheItem + */ + public function getItem($key); + + /** + * {@inheritdoc} + * + * return \Traversable|CacheItem[] + */ + public function getItems(array $keys = array()); +} diff --git a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php new file mode 100644 index 0000000000000..50554ed688309 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Traits\ApcuTrait; + +class ApcuAdapter extends AbstractAdapter +{ + use ApcuTrait; + + /** + * @param string $namespace + * @param int $defaultLifetime + * @param string|null $version + * + * @throws CacheException if APCu is not enabled + */ + public function __construct($namespace = '', $defaultLifetime = 0, $version = null) + { + $this->init($namespace, $defaultLifetime, $version); + } +} diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php new file mode 100644 index 0000000000000..45c19c7a6c7af --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerAwareInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Traits\ArrayTrait; + +/** + * @author Nicolas Grekas + */ +class ArrayAdapter implements AdapterInterface, LoggerAwareInterface +{ + use ArrayTrait; + + private $createCacheItem; + + /** + * @param int $defaultLifetime + * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise + */ + public function __construct($defaultLifetime = 0, $storeSerialized = true) + { + $this->storeSerialized = $storeSerialized; + $this->createCacheItem = \Closure::bind( + function ($key, $value, $isHit) use ($defaultLifetime) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + $item->defaultLifetime = $defaultLifetime; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $isHit = $this->hasItem($key); + try { + if (!$isHit) { + $this->values[$key] = $value = null; + } elseif (!$this->storeSerialized) { + $value = $this->values[$key]; + } elseif ('b:0;' === $value = $this->values[$key]) { + $value = false; + } elseif (false === $value = unserialize($value)) { + $this->values[$key] = $value = null; + $isHit = false; + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e)); + $this->values[$key] = $value = null; + $isHit = false; + } + $f = $this->createCacheItem; + + return $f($key, $value, $isHit); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + foreach ($keys as $key) { + CacheItem::validateKey($key); + } + + return $this->generateItems($keys, time(), $this->createCacheItem); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + foreach ($keys as $key) { + $this->deleteItem($key); + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $item = (array) $item; + $key = $item["\0*\0key"]; + $value = $item["\0*\0value"]; + $expiry = $item["\0*\0expiry"]; + + if (null !== $expiry && $expiry <= time()) { + $this->deleteItem($key); + + return true; + } + if ($this->storeSerialized) { + try { + $value = serialize($value); + } catch (\Exception $e) { + $type = is_object($value) ? get_class($value) : gettype($value); + CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', array('key' => $key, 'type' => $type, 'exception' => $e)); + + return false; + } + } + if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) { + $expiry = time() + $item["\0*\0defaultLifetime"]; + } + + $this->values[$key] = $value; + $this->expiries[$key] = null !== $expiry ? $expiry : PHP_INT_MAX; + + return true; + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + return $this->save($item); + } + + /** + * {@inheritdoc} + */ + public function commit() + { + return true; + } +} diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php new file mode 100644 index 0000000000000..7512472150631 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -0,0 +1,234 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * Chains several adapters together. + * + * Cached items are fetched from the first adapter having them in its data store. + * They are saved and deleted in all adapters at once. + * + * @author Kévin Dunglas + */ +class ChainAdapter implements AdapterInterface +{ + private $adapters = array(); + private $adapterCount; + private $saveUp; + + /** + * @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items + * @param int $maxLifetime The max lifetime of items propagated from lower adapters to upper ones + */ + public function __construct(array $adapters, $maxLifetime = 0) + { + if (!$adapters) { + throw new InvalidArgumentException('At least one adapter must be specified.'); + } + + foreach ($adapters as $adapter) { + if (!$adapter instanceof CacheItemPoolInterface) { + throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_class($adapter), CacheItemPoolInterface::class)); + } + + if ($adapter instanceof AdapterInterface) { + $this->adapters[] = $adapter; + } else { + $this->adapters[] = new ProxyAdapter($adapter); + } + } + $this->adapterCount = count($this->adapters); + + $this->saveUp = \Closure::bind( + function ($adapter, $item) use ($maxLifetime) { + $origDefaultLifetime = $item->defaultLifetime; + + if (0 < $maxLifetime && ($origDefaultLifetime <= 0 || $maxLifetime < $origDefaultLifetime)) { + $item->defaultLifetime = $maxLifetime; + } + + $adapter->save($item); + $item->defaultLifetime = $origDefaultLifetime; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $saveUp = $this->saveUp; + + foreach ($this->adapters as $i => $adapter) { + $item = $adapter->getItem($key); + + if ($item->isHit()) { + while (0 <= --$i) { + $saveUp($this->adapters[$i], $item); + } + + return $item; + } + } + + return $item; + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + return $this->generateItems($this->adapters[0]->getItems($keys), 0); + } + + private function generateItems($items, $adapterIndex) + { + $missing = array(); + $nextAdapterIndex = $adapterIndex + 1; + $nextAdapter = isset($this->adapters[$nextAdapterIndex]) ? $this->adapters[$nextAdapterIndex] : null; + + foreach ($items as $k => $item) { + if (!$nextAdapter || $item->isHit()) { + yield $k => $item; + } else { + $missing[] = $k; + } + } + + if ($missing) { + $saveUp = $this->saveUp; + $adapter = $this->adapters[$adapterIndex]; + $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex); + + foreach ($items as $k => $item) { + if ($item->isHit()) { + $saveUp($adapter, $item); + } + + yield $k => $item; + } + } + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + foreach ($this->adapters as $adapter) { + if ($adapter->hasItem($key)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $cleared = true; + $i = $this->adapterCount; + + while ($i--) { + $cleared = $this->adapters[$i]->clear() && $cleared; + } + + return $cleared; + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + $deleted = true; + $i = $this->adapterCount; + + while ($i--) { + $deleted = $this->adapters[$i]->deleteItem($key) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + $deleted = true; + $i = $this->adapterCount; + + while ($i--) { + $deleted = $this->adapters[$i]->deleteItems($keys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + $saved = true; + $i = $this->adapterCount; + + while ($i--) { + $saved = $this->adapters[$i]->save($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + $saved = true; + $i = $this->adapterCount; + + while ($i--) { + $saved = $this->adapters[$i]->saveDeferred($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + $committed = true; + $i = $this->adapterCount; + + while ($i--) { + $committed = $this->adapters[$i]->commit() && $committed; + } + + return $committed; + } +} diff --git a/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php b/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php new file mode 100644 index 0000000000000..972d2b41545ef --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Doctrine\Common\Cache\CacheProvider; +use Symfony\Component\Cache\Traits\DoctrineTrait; + +class DoctrineAdapter extends AbstractAdapter +{ + use DoctrineTrait; + + /** + * @param CacheProvider $provider + * @param string $namespace + * @param int $defaultLifetime + */ + public function __construct(CacheProvider $provider, $namespace = '', $defaultLifetime = 0) + { + parent::__construct('', $defaultLifetime); + $this->provider = $provider; + $provider->setNamespace($namespace); + } +} diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php b/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php new file mode 100644 index 0000000000000..d071964ec2c5c --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemTrait; + +class FilesystemAdapter extends AbstractAdapter implements PruneableInterface +{ + use FilesystemTrait; + + /** + * @param string $namespace + * @param int $defaultLifetime + * @param string|null $directory + */ + public function __construct($namespace = '', $defaultLifetime = 0, $directory = null) + { + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + } +} diff --git a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php new file mode 100644 index 0000000000000..3acf8bdb86c6a --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Traits\MemcachedTrait; + +class MemcachedAdapter extends AbstractAdapter +{ + use MemcachedTrait; + + protected $maxIdLength = 250; + + /** + * Constructor. + * + * Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged. + * Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that: + * - the Memcached::OPT_BINARY_PROTOCOL must be enabled + * (that's the default when using MemcachedAdapter::createConnection()); + * - tags eviction by Memcached's LRU algorithm will break by-tags invalidation; + * your Memcached memory should be large enough to never trigger LRU. + * + * Using a MemcachedAdapter as a pure items store is fine. + */ + public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0) + { + $this->init($client, $namespace, $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Adapter/NullAdapter.php b/src/Symfony/Component/Cache/Adapter/NullAdapter.php new file mode 100644 index 0000000000000..f58f81e5b8960 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/NullAdapter.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheItem; + +/** + * @author Titouan Galopin + */ +class NullAdapter implements AdapterInterface +{ + private $createCacheItem; + + public function __construct() + { + $this->createCacheItem = \Closure::bind( + function ($key) { + $item = new CacheItem(); + $item->key = $key; + $item->isHit = false; + + return $item; + }, + $this, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $f = $this->createCacheItem; + + return $f($key); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + return $this->generateItems($keys); + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + return false; + } + + private function generateItems(array $keys) + { + $f = $this->createCacheItem; + + foreach ($keys as $key) { + yield $key => $f($key); + } + } +} diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php new file mode 100644 index 0000000000000..832185629b053 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Traits\PdoTrait; + +class PdoAdapter extends AbstractAdapter +{ + use PdoTrait; + + protected $maxIdLength = 255; + + /** + * Constructor. + * + * You can either pass an existing database connection as PDO instance or + * a Doctrine DBAL Connection or a DSN string that will be used to + * lazy-connect to the database when the cache is actually used. + * + * List of available options: + * * db_table: The name of the table [default: cache_items] + * * db_id_col: The column where to store the cache id [default: item_id] + * * db_data_col: The column where to store the cache data [default: item_data] + * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime] + * * db_time_col: The column where to store the timestamp [default: item_time] + * * db_username: The username when lazy-connect [default: ''] + * * db_password: The password when lazy-connect [default: ''] + * * db_connection_options: An array of driver-specific connection options [default: array()] + * + * @param \PDO|Connection|string $connOrDsn A \PDO or Connection instance or DSN string or null + * @param string $namespace + * @param int $defaultLifetime + * @param array $options An associative array of options + * + * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string + * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + * @throws InvalidArgumentException When namespace contains invalid characters + */ + public function __construct($connOrDsn, $namespace = '', $defaultLifetime = 0, array $options = array()) + { + $this->init($connOrDsn, $namespace, $defaultLifetime, $options); + } +} diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php new file mode 100644 index 0000000000000..e62ed9b575f8f --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -0,0 +1,302 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\PhpArrayTrait; + +/** + * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0. + * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter. + * + * @author Titouan Galopin + * @author Nicolas Grekas + */ +class PhpArrayAdapter implements AdapterInterface +{ + use PhpArrayTrait; + + private $createCacheItem; + + /** + * @param string $file The PHP file were values are cached + * @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit + */ + public function __construct($file, AdapterInterface $fallbackPool) + { + $this->file = $file; + $this->fallbackPool = $fallbackPool; + $this->createCacheItem = \Closure::bind( + function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * This adapter takes advantage of how PHP stores arrays in its latest versions. + * + * @param string $file The PHP file were values are cached + * @param CacheItemPoolInterface $fallbackPool Fallback when opcache is disabled + * + * @return CacheItemPoolInterface + */ + public static function create($file, CacheItemPoolInterface $fallbackPool) + { + // Shared memory is available in PHP 7.0+ with OPCache enabled + if (ini_get('opcache.enable')) { + if (!$fallbackPool instanceof AdapterInterface) { + $fallbackPool = new ProxyAdapter($fallbackPool); + } + + return new static($file, $fallbackPool); + } + + return $fallbackPool; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + if (null === $this->values) { + $this->initialize(); + } + if (!isset($this->values[$key])) { + return $this->fallbackPool->getItem($key); + } + + $value = $this->values[$key]; + $isHit = true; + + if ('N;' === $value) { + $value = null; + } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) { + try { + $e = null; + $value = unserialize($value); + } catch (\Error $e) { + } catch (\Exception $e) { + } + if (null !== $e) { + $value = null; + $isHit = false; + } + } + + $f = $this->createCacheItem; + + return $f($key, $value, $isHit); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + foreach ($keys as $key) { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + } + if (null === $this->values) { + $this->initialize(); + } + + return $this->generateItems($keys); + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return isset($this->values[$key]) || $this->fallbackPool->hasItem($key); + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->values[$key]) && $this->fallbackPool->deleteItem($key); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + $deleted = true; + $fallbackKeys = array(); + + foreach ($keys as $key) { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + + if (isset($this->values[$key])) { + $deleted = false; + } else { + $fallbackKeys[] = $key; + } + } + if (null === $this->values) { + $this->initialize(); + } + + if ($fallbackKeys) { + $deleted = $this->fallbackPool->deleteItems($fallbackKeys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->values[$item->getKey()]) && $this->fallbackPool->save($item); + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->values[$item->getKey()]) && $this->fallbackPool->saveDeferred($item); + } + + /** + * {@inheritdoc} + */ + public function commit() + { + return $this->fallbackPool->commit(); + } + + /** + * Generator for items. + * + * @param array $keys + * + * @return \Generator + */ + private function generateItems(array $keys) + { + $f = $this->createCacheItem; + $fallbackKeys = array(); + + foreach ($keys as $key) { + if (isset($this->values[$key])) { + $value = $this->values[$key]; + + if ('N;' === $value) { + yield $key => $f($key, null, true); + } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) { + try { + yield $key => $f($key, unserialize($value), true); + } catch (\Error $e) { + yield $key => $f($key, null, false); + } catch (\Exception $e) { + yield $key => $f($key, null, false); + } + } else { + yield $key => $f($key, $value, true); + } + } else { + $fallbackKeys[] = $key; + } + } + + if ($fallbackKeys) { + foreach ($this->fallbackPool->getItems($fallbackKeys) as $key => $item) { + yield $key => $item; + } + } + } + + /** + * @throws \ReflectionException When $class is not found and is required + * + * @internal + */ + public static function throwOnRequiredClass($class) + { + $e = new \ReflectionException("Class $class does not exist"); + $trace = $e->getTrace(); + $autoloadFrame = array( + 'function' => 'spl_autoload_call', + 'args' => array($class), + ); + $i = 1 + array_search($autoloadFrame, $trace, true); + + if (isset($trace[$i]['function']) && !isset($trace[$i]['class'])) { + switch ($trace[$i]['function']) { + case 'get_class_methods': + case 'get_class_vars': + case 'get_parent_class': + case 'is_a': + case 'is_subclass_of': + case 'class_exists': + case 'class_implements': + case 'class_parents': + case 'trait_exists': + case 'defined': + case 'interface_exists': + case 'method_exists': + case 'property_exists': + case 'is_callable': + return; + } + } + + throw $e; + } +} diff --git a/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php new file mode 100644 index 0000000000000..3ce1ac7d13ab7 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\PhpFilesTrait; + +class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface +{ + use PhpFilesTrait; + + /** + * @param string $namespace + * @param int $defaultLifetime + * @param string|null $directory + * + * @throws CacheException if OPcache is not enabled + */ + public function __construct($namespace = '', $defaultLifetime = 0, $directory = null) + { + if (!static::isSupported()) { + throw new CacheException('OPcache is not enabled'); + } + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + + $e = new \Exception(); + $this->includeHandler = function () use ($e) { throw $e; }; + } +} diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php new file mode 100644 index 0000000000000..4f37ffd731931 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; + +/** + * @author Nicolas Grekas + */ +class ProxyAdapter implements AdapterInterface +{ + private $pool; + private $namespace; + private $namespaceLen; + private $createCacheItem; + private $poolHash; + + /** + * @param CacheItemPoolInterface $pool + * @param string $namespace + * @param int $defaultLifetime + */ + public function __construct(CacheItemPoolInterface $pool, $namespace = '', $defaultLifetime = 0) + { + $this->pool = $pool; + $this->poolHash = $poolHash = spl_object_hash($pool); + $this->namespace = '' === $namespace ? '' : $this->getId($namespace); + $this->namespaceLen = strlen($namespace); + $this->createCacheItem = \Closure::bind( + function ($key, $innerItem) use ($defaultLifetime, $poolHash) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $innerItem->get(); + $item->isHit = $innerItem->isHit(); + $item->defaultLifetime = $defaultLifetime; + $item->innerItem = $innerItem; + $item->poolHash = $poolHash; + $innerItem->set(null); + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $f = $this->createCacheItem; + $item = $this->pool->getItem($this->getId($key)); + + return $f($key, $item); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + if ($this->namespaceLen) { + foreach ($keys as $i => $key) { + $keys[$i] = $this->getId($key); + } + } + + return $this->generateItems($this->pool->getItems($keys)); + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + return $this->pool->hasItem($this->getId($key)); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + return $this->pool->deleteItem($this->getId($key)); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + if ($this->namespaceLen) { + foreach ($keys as $i => $key) { + $keys[$i] = $this->getId($key); + } + } + + return $this->pool->deleteItems($keys); + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + return $this->doSave($item, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + return $this->doSave($item, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function commit() + { + return $this->pool->commit(); + } + + private function doSave(CacheItemInterface $item, $method) + { + if (!$item instanceof CacheItem) { + return false; + } + $item = (array) $item; + $expiry = $item["\0*\0expiry"]; + if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) { + $expiry = time() + $item["\0*\0defaultLifetime"]; + } + $innerItem = $item["\0*\0poolHash"] === $this->poolHash ? $item["\0*\0innerItem"] : $this->pool->getItem($this->namespace.$item["\0*\0key"]); + $innerItem->set($item["\0*\0value"]); + $innerItem->expiresAt(null !== $expiry ? \DateTime::createFromFormat('U', $expiry) : null); + + return $this->pool->$method($innerItem); + } + + private function generateItems($items) + { + $f = $this->createCacheItem; + + foreach ($items as $key => $item) { + if ($this->namespaceLen) { + $key = substr($key, $this->namespaceLen); + } + + yield $key => $f($key, $item); + } + } + + private function getId($key) + { + CacheItem::validateKey($key); + + return $this->namespace.$key; + } +} diff --git a/src/Symfony/Component/Cache/Adapter/RedisAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php new file mode 100644 index 0000000000000..c1e17997fb557 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/RedisAdapter.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Traits\RedisTrait; + +class RedisAdapter extends AbstractAdapter +{ + use RedisTrait; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient The redis client + * @param string $namespace The default namespace + * @param int $defaultLifetime The default lifetime + */ + public function __construct($redisClient, $namespace = '', $defaultLifetime = 0) + { + $this->init($redisClient, $namespace, $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php b/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php new file mode 100644 index 0000000000000..f17662441041b --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/SimpleCacheAdapter.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\SimpleCache\CacheInterface; + +/** + * @author Nicolas Grekas + */ +class SimpleCacheAdapter extends AbstractAdapter +{ + private $pool; + private $miss; + + public function __construct(CacheInterface $pool, $namespace = '', $defaultLifetime = 0) + { + parent::__construct($namespace, $defaultLifetime); + + $this->pool = $pool; + $this->miss = new \stdClass(); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) { + if ($this->miss !== $value) { + yield $key => $value; + } + } + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + return $this->pool->has($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + return $this->pool->deleteMultiple($ids); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime); + } +} diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php new file mode 100644 index 0000000000000..1e0617ebe271d --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -0,0 +1,337 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; +use Symfony\Component\Cache\CacheItem; + +/** + * @author Nicolas Grekas + */ +class TagAwareAdapter implements TagAwareAdapterInterface +{ + const TAGS_PREFIX = "\0tags\0"; + + private $itemsAdapter; + private $deferred = array(); + private $createCacheItem; + private $setCacheItemTags; + private $getTagsByKey; + private $invalidateTags; + private $tagsAdapter; + + public function __construct(AdapterInterface $itemsAdapter, AdapterInterface $tagsAdapter = null) + { + $this->itemsAdapter = $itemsAdapter; + $this->tagsAdapter = $tagsAdapter ?: $itemsAdapter; + $this->createCacheItem = \Closure::bind( + function ($key, $value, CacheItem $protoItem) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->defaultLifetime = $protoItem->defaultLifetime; + $item->expiry = $protoItem->expiry; + $item->innerItem = $protoItem->innerItem; + $item->poolHash = $protoItem->poolHash; + + return $item; + }, + null, + CacheItem::class + ); + $this->setCacheItemTags = \Closure::bind( + function (CacheItem $item, $key, array &$itemTags) { + if (!$item->isHit) { + return $item; + } + if (isset($itemTags[$key])) { + foreach ($itemTags[$key] as $tag => $version) { + $item->prevTags[$tag] = $tag; + } + unset($itemTags[$key]); + } else { + $item->value = null; + $item->isHit = false; + } + + return $item; + }, + null, + CacheItem::class + ); + $this->getTagsByKey = \Closure::bind( + function ($deferred) { + $tagsByKey = array(); + foreach ($deferred as $key => $item) { + $tagsByKey[$key] = $item->tags; + } + + return $tagsByKey; + }, + null, + CacheItem::class + ); + $this->invalidateTags = \Closure::bind( + function (AdapterInterface $tagsAdapter, array $tags) { + foreach ($tagsAdapter->getItems($tags) as $v) { + $v->set(1 + (int) $v->get()); + $v->defaultLifetime = 0; + $v->expiry = null; + $tagsAdapter->saveDeferred($v); + } + + return $tagsAdapter->commit(); + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + foreach ($tags as $k => $tag) { + if ('' !== $tag && is_string($tag)) { + $tags[$k] = $tag.static::TAGS_PREFIX; + } + } + $f = $this->invalidateTags; + + return $f($this->tagsAdapter, $tags); + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + if ($this->deferred) { + $this->commit(); + } + if (!$this->itemsAdapter->hasItem($key)) { + return false; + } + if (!$itemTags = $this->itemsAdapter->getItem(static::TAGS_PREFIX.$key)->get()) { + return true; + } + + foreach ($this->getTagVersions(array($itemTags)) as $tag => $version) { + if ($itemTags[$tag] !== $version) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + foreach ($this->getItems(array($key)) as $item) { + return $item; + } + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + if ($this->deferred) { + $this->commit(); + } + $tagKeys = array(); + + foreach ($keys as $key) { + if ('' !== $key && is_string($key)) { + $key = static::TAGS_PREFIX.$key; + $tagKeys[$key] = $key; + } + } + + try { + $items = $this->itemsAdapter->getItems($tagKeys + $keys); + } catch (InvalidArgumentException $e) { + $this->itemsAdapter->getItems($keys); // Should throw an exception + + throw $e; + } + + return $this->generateItems($items, $tagKeys); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->deferred = array(); + + return $this->itemsAdapter->clear(); + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + return $this->deleteItems(array($key)); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + foreach ($keys as $key) { + if ('' !== $key && is_string($key)) { + $keys[] = static::TAGS_PREFIX.$key; + } + } + + return $this->itemsAdapter->deleteItems($keys); + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return $this->commit(); + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return true; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + $ok = true; + + if ($this->deferred) { + $items = $this->deferred; + foreach ($items as $key => $item) { + if (!$this->itemsAdapter->saveDeferred($item)) { + unset($this->deferred[$key]); + $ok = false; + } + } + + $f = $this->getTagsByKey; + $tagsByKey = $f($items); + $deletedTags = $this->deferred = array(); + $tagVersions = $this->getTagVersions($tagsByKey); + $f = $this->createCacheItem; + + foreach ($tagsByKey as $key => $tags) { + if ($tags) { + $this->itemsAdapter->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key])); + } else { + $deletedTags[] = static::TAGS_PREFIX.$key; + } + } + if ($deletedTags) { + $this->itemsAdapter->deleteItems($deletedTags); + } + } + + return $this->itemsAdapter->commit() && $ok; + } + + public function __destruct() + { + $this->commit(); + } + + private function generateItems($items, array $tagKeys) + { + $bufferedItems = $itemTags = array(); + $f = $this->setCacheItemTags; + + foreach ($items as $key => $item) { + if (!$tagKeys) { + yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); + continue; + } + if (!isset($tagKeys[$key])) { + $bufferedItems[$key] = $item; + continue; + } + + unset($tagKeys[$key]); + $itemTags[$key] = $item->get() ?: array(); + + if (!$tagKeys) { + $tagVersions = $this->getTagVersions($itemTags); + + foreach ($itemTags as $key => $tags) { + foreach ($tags as $tag => $version) { + if ($tagVersions[$tag] !== $version) { + unset($itemTags[$key]); + continue 2; + } + } + } + $tagVersions = $tagKeys = null; + + foreach ($bufferedItems as $key => $item) { + yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); + } + $bufferedItems = null; + } + } + } + + private function getTagVersions(array $tagsByKey) + { + $tagVersions = array(); + + foreach ($tagsByKey as $tags) { + $tagVersions += $tags; + } + + if ($tagVersions) { + $tags = array(); + foreach ($tagVersions as $tag => $version) { + $tagVersions[$tag] = $tag.static::TAGS_PREFIX; + $tags[$tag.static::TAGS_PREFIX] = $tag; + } + foreach ($this->tagsAdapter->getItems($tagVersions) as $tag => $version) { + $tagVersions[$tags[$tag]] = $version->get() ?: 0; + } + } + + return $tagVersions; + } +} diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapterInterface.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapterInterface.php new file mode 100644 index 0000000000000..340048c100021 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapterInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\InvalidArgumentException; + +/** + * Interface for invalidating cached items using tags. + * + * @author Nicolas Grekas + */ +interface TagAwareAdapterInterface extends AdapterInterface +{ + /** + * Invalidates cached items using tags. + * + * @param string[] $tags An array of tags to invalidate + * + * @return bool True on success + * + * @throws InvalidArgumentException When $tags is not valid + */ + public function invalidateTags(array $tags); +} diff --git a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php new file mode 100644 index 0000000000000..9959199f67ccc --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php @@ -0,0 +1,198 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; + +/** + * An adapter that collects data about all cache calls. + * + * @author Aaron Scherer + * @author Tobias Nyholm + * @author Nicolas Grekas + */ +class TraceableAdapter implements AdapterInterface +{ + protected $pool; + private $calls = array(); + + public function __construct(AdapterInterface $pool) + { + $this->pool = $pool; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $event = $this->start(__FUNCTION__); + try { + $item = $this->pool->getItem($key); + } finally { + $event->end = microtime(true); + } + if ($event->result[$key] = $item->isHit()) { + ++$event->hits; + } else { + ++$event->misses; + } + + return $item; + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->hasItem($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->deleteItem($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$item->getKey()] = $this->pool->save($item); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$item->getKey()] = $this->pool->saveDeferred($item); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + $event = $this->start(__FUNCTION__); + try { + $result = $this->pool->getItems($keys); + } finally { + $event->end = microtime(true); + } + $f = function () use ($result, $event) { + $event->result = array(); + foreach ($result as $key => $item) { + if ($event->result[$key] = $item->isHit()) { + ++$event->hits; + } else { + ++$event->misses; + } + yield $key => $item; + } + }; + + return $f(); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->clear(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + $event = $this->start(__FUNCTION__); + $event->result['keys'] = $keys; + try { + return $event->result['result'] = $this->pool->deleteItems($keys); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function commit() + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->commit(); + } finally { + $event->end = microtime(true); + } + } + + public function getCalls() + { + try { + return $this->calls; + } finally { + $this->calls = array(); + } + } + + protected function start($name) + { + $this->calls[] = $event = new TraceableAdapterEvent(); + $event->name = $name; + $event->start = microtime(true); + + return $event; + } +} + +class TraceableAdapterEvent +{ + public $name; + public $start; + public $end; + public $result; + public $hits = 0; + public $misses = 0; +} diff --git a/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php new file mode 100644 index 0000000000000..de68955d8e56d --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +/** + * @author Robin Chalas + */ +class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface +{ + public function __construct(TagAwareAdapterInterface $pool) + { + parent::__construct($pool); + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->invalidateTags($tags); + } finally { + $event->end = microtime(true); + } + } +} diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md new file mode 100644 index 0000000000000..e8172d94988d4 --- /dev/null +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -0,0 +1,35 @@ +CHANGELOG +========= + +3.4.0 +----- + + * added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning + * added FilesystemTrait::prune() and PhpFilesTrait::prune() implementations + * now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, and PhpFilesCache implement PruneableInterface and support + manual stale cache pruning + +3.3.0 +----- + + * [EXPERIMENTAL] added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any + * added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters + * added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16 + * added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16) + * added TraceableAdapter (PSR-6) and TraceableCache (PSR-16) + +3.2.0 +----- + + * added TagAwareAdapter for tags-based invalidation + * added PdoAdapter with PDO and Doctrine DBAL support + * added PhpArrayAdapter and PhpFilesAdapter for OPcache-backed shared memory storage (PHP 7+ only) + * added NullAdapter + +3.1.0 +----- + + * added the component with strict PSR-6 implementations + * added ApcuAdapter, ArrayAdapter, FilesystemAdapter and RedisAdapter + * added AbstractAdapter, ChainAdapter and ProxyAdapter + * added DoctrineAdapter and DoctrineProvider for bidirectional interoperability with Doctrine Cache diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php new file mode 100644 index 0000000000000..55e25de9a9513 --- /dev/null +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + */ +final class CacheItem implements CacheItemInterface +{ + protected $key; + protected $value; + protected $isHit = false; + protected $expiry; + protected $defaultLifetime; + protected $tags = array(); + protected $prevTags = array(); + protected $innerItem; + protected $poolHash; + + /** + * {@inheritdoc} + */ + public function getKey() + { + return $this->key; + } + + /** + * {@inheritdoc} + */ + public function get() + { + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function isHit() + { + return $this->isHit; + } + + /** + * {@inheritdoc} + */ + public function set($value) + { + $this->value = $value; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function expiresAt($expiration) + { + if (null === $expiration) { + $this->expiry = $this->defaultLifetime > 0 ? time() + $this->defaultLifetime : null; + } elseif ($expiration instanceof \DateTimeInterface) { + $this->expiry = (int) $expiration->format('U'); + } else { + throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given', is_object($expiration) ? get_class($expiration) : gettype($expiration))); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function expiresAfter($time) + { + if (null === $time) { + $this->expiry = $this->defaultLifetime > 0 ? time() + $this->defaultLifetime : null; + } elseif ($time instanceof \DateInterval) { + $this->expiry = (int) \DateTime::createFromFormat('U', time())->add($time)->format('U'); + } elseif (is_int($time)) { + $this->expiry = $time + time(); + } else { + throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given', is_object($time) ? get_class($time) : gettype($time))); + } + + return $this; + } + + /** + * Adds a tag to a cache item. + * + * @param string|string[] $tags A tag or array of tags + * + * @return static + * + * @throws InvalidArgumentException When $tag is not valid + */ + public function tag($tags) + { + if (!is_array($tags)) { + $tags = array($tags); + } + foreach ($tags as $tag) { + if (!is_string($tag)) { + throw new InvalidArgumentException(sprintf('Cache tag must be string, "%s" given', is_object($tag) ? get_class($tag) : gettype($tag))); + } + if (isset($this->tags[$tag])) { + continue; + } + if (!isset($tag[0])) { + throw new InvalidArgumentException('Cache tag length must be greater than zero'); + } + if (false !== strpbrk($tag, '{}()/\@:')) { + throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters {}()/\@:', $tag)); + } + $this->tags[$tag] = $tag; + } + + return $this; + } + + /** + * Returns the list of tags bound to the value coming from the pool storage if any. + * + * @return array + * + * @experimental in version 3.3 + */ + public function getPreviousTags() + { + return $this->prevTags; + } + + /** + * Validates a cache key according to PSR-6. + * + * @param string $key The key to validate + * + * @throws InvalidArgumentException When $key is not valid + */ + public static function validateKey($key) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given', is_object($key) ? get_class($key) : gettype($key))); + } + if (!isset($key[0])) { + throw new InvalidArgumentException('Cache key length must be greater than zero'); + } + if (false !== strpbrk($key, '{}()/\@:')) { + throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters {}()/\@:', $key)); + } + } + + /** + * Internal logging helper. + * + * @internal + */ + public static function log(LoggerInterface $logger = null, $message, $context = array()) + { + if ($logger) { + $logger->warning($message, $context); + } else { + $replace = array(); + foreach ($context as $k => $v) { + if (is_scalar($v)) { + $replace['{'.$k.'}'] = $v; + } + } + @trigger_error(strtr($message, $replace), E_USER_WARNING); + } + } +} diff --git a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php new file mode 100644 index 0000000000000..07ddaf3f4790a --- /dev/null +++ b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DataCollector; + +use Symfony\Component\Cache\Adapter\TraceableAdapter; +use Symfony\Component\Cache\Adapter\TraceableAdapterEvent; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; + +/** + * @author Aaron Scherer + * @author Tobias Nyholm + */ +class CacheDataCollector extends DataCollector implements LateDataCollectorInterface +{ + /** + * @var TraceableAdapter[] + */ + private $instances = array(); + + /** + * @param string $name + * @param TraceableAdapter $instance + */ + public function addInstance($name, TraceableAdapter $instance) + { + $this->instances[$name] = $instance; + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + $empty = array('calls' => array(), 'config' => array(), 'options' => array(), 'statistics' => array()); + $this->data = array('instances' => $empty, 'total' => $empty); + foreach ($this->instances as $name => $instance) { + $this->data['instances']['calls'][$name] = $instance->getCalls(); + } + + $this->data['instances']['statistics'] = $this->calculateStatistics(); + $this->data['total']['statistics'] = $this->calculateTotalStatistics(); + } + + public function lateCollect() + { + $this->data = $this->cloneVar($this->data); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'cache'; + } + + /** + * Method returns amount of logged Cache reads: "get" calls. + * + * @return array + */ + public function getStatistics() + { + return $this->data['instances']['statistics']; + } + + /** + * Method returns the statistic totals. + * + * @return array + */ + public function getTotals() + { + return $this->data['total']['statistics']; + } + + /** + * Method returns all logged Cache call objects. + * + * @return mixed + */ + public function getCalls() + { + return $this->data['instances']['calls']; + } + + /** + * @return array + */ + private function calculateStatistics() + { + $statistics = array(); + foreach ($this->data['instances']['calls'] as $name => $calls) { + $statistics[$name] = array( + 'calls' => 0, + 'time' => 0, + 'reads' => 0, + 'writes' => 0, + 'deletes' => 0, + 'hits' => 0, + 'misses' => 0, + ); + /** @var TraceableAdapterEvent $call */ + foreach ($calls as $call) { + $statistics[$name]['calls'] += 1; + $statistics[$name]['time'] += $call->end - $call->start; + if ('getItem' === $call->name) { + $statistics[$name]['reads'] += 1; + if ($call->hits) { + $statistics[$name]['hits'] += 1; + } else { + $statistics[$name]['misses'] += 1; + } + } elseif ('getItems' === $call->name) { + $count = $call->hits + $call->misses; + $statistics[$name]['reads'] += $count; + $statistics[$name]['hits'] += $call->hits; + $statistics[$name]['misses'] += $count - $call->misses; + } elseif ('hasItem' === $call->name) { + $statistics[$name]['reads'] += 1; + if (false === $call->result) { + $statistics[$name]['misses'] += 1; + } else { + $statistics[$name]['hits'] += 1; + } + } elseif ('save' === $call->name) { + $statistics[$name]['writes'] += 1; + } elseif ('deleteItem' === $call->name) { + $statistics[$name]['deletes'] += 1; + } + } + if ($statistics[$name]['reads']) { + $statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2); + } else { + $statistics[$name]['hit_read_ratio'] = null; + } + } + + return $statistics; + } + + /** + * @return array + */ + private function calculateTotalStatistics() + { + $statistics = $this->getStatistics(); + $totals = array( + 'calls' => 0, + 'time' => 0, + 'reads' => 0, + 'writes' => 0, + 'deletes' => 0, + 'hits' => 0, + 'misses' => 0, + ); + foreach ($statistics as $name => $values) { + foreach ($totals as $key => $value) { + $totals[$key] += $statistics[$name][$key]; + } + } + if ($totals['reads']) { + $totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2); + } else { + $totals['hit_read_ratio'] = null; + } + + return $totals; + } +} diff --git a/src/Symfony/Component/Cache/DoctrineProvider.php b/src/Symfony/Component/Cache/DoctrineProvider.php new file mode 100644 index 0000000000000..5d9c2faed7187 --- /dev/null +++ b/src/Symfony/Component/Cache/DoctrineProvider.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Doctrine\Common\Cache\CacheProvider; +use Psr\Cache\CacheItemPoolInterface; + +/** + * @author Nicolas Grekas + */ +class DoctrineProvider extends CacheProvider +{ + private $pool; + + public function __construct(CacheItemPoolInterface $pool) + { + $this->pool = $pool; + } + + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + $item = $this->pool->getItem(rawurlencode($id)); + + return $item->isHit() ? $item->get() : false; + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return $this->pool->hasItem(rawurlencode($id)); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + $item = $this->pool->getItem(rawurlencode($id)); + + if (0 < $lifeTime) { + $item->expiresAfter($lifeTime); + } + + return $this->pool->save($item->set($data)); + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return $this->pool->deleteItem(rawurlencode($id)); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + $this->pool->clear(); + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + } +} diff --git a/src/Symfony/Component/Cache/Exception/CacheException.php b/src/Symfony/Component/Cache/Exception/CacheException.php new file mode 100644 index 0000000000000..e87b2db8fe733 --- /dev/null +++ b/src/Symfony/Component/Cache/Exception/CacheException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\CacheException as Psr6CacheInterface; +use Psr\SimpleCache\CacheException as SimpleCacheInterface; + +class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface +{ +} diff --git a/src/Symfony/Component/Cache/Exception/InvalidArgumentException.php b/src/Symfony/Component/Cache/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..828bf3ed77999 --- /dev/null +++ b/src/Symfony/Component/Cache/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\InvalidArgumentException as Psr6CacheInterface; +use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface; + +class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface +{ +} diff --git a/src/Symfony/Component/Cache/LICENSE b/src/Symfony/Component/Cache/LICENSE new file mode 100644 index 0000000000000..ce39894f6a9a2 --- /dev/null +++ b/src/Symfony/Component/Cache/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016-2017 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Cache/PruneableInterface.php b/src/Symfony/Component/Cache/PruneableInterface.php new file mode 100644 index 0000000000000..cd366adb55290 --- /dev/null +++ b/src/Symfony/Component/Cache/PruneableInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +/** + * Interface for adapters and simple cache implementations that allow pruning expired items. + */ +interface PruneableInterface +{ + /** + * @return bool + */ + public function prune(); +} diff --git a/src/Symfony/Component/Cache/README.md b/src/Symfony/Component/Cache/README.md new file mode 100644 index 0000000000000..c4ab7520f451e --- /dev/null +++ b/src/Symfony/Component/Cache/README.md @@ -0,0 +1,18 @@ +Symfony PSR-6 implementation for caching +======================================== + +This component provides an extended [PSR-6](http://www.php-fig.org/psr/psr-6/) +implementation for adding cache to your applications. It is designed to have a +low overhead so that caching is fastest. It ships with a few caching adapters +for the most widespread and suited to caching backends. It also provides a +`doctrine/cache` proxy adapter to cover more advanced caching needs and a proxy +adapter for greater interoperability between PSR-6 implementations. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/cache.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Cache/Simple/AbstractCache.php b/src/Symfony/Component/Cache/Simple/AbstractCache.php new file mode 100644 index 0000000000000..e4046463f1609 --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/AbstractCache.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Psr\Log\LoggerAwareInterface; +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\AbstractTrait; + +/** + * @author Nicolas Grekas + */ +abstract class AbstractCache implements CacheInterface, LoggerAwareInterface +{ + use AbstractTrait { + deleteItems as private; + AbstractTrait::deleteItem as delete; + AbstractTrait::hasItem as has; + } + + private $defaultLifetime; + + /** + * @param string $namespace + * @param int $defaultLifetime + */ + protected function __construct($namespace = '', $defaultLifetime = 0) + { + $this->defaultLifetime = max(0, (int) $defaultLifetime); + $this->namespace = '' === $namespace ? '' : $this->getId($namespace).':'; + if (null !== $this->maxIdLength && strlen($namespace) > $this->maxIdLength - 24) { + throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, strlen($namespace), $namespace)); + } + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $id = $this->getId($key); + + try { + foreach ($this->doFetch(array($id)) as $value) { + return $value; + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch key "{key}"', array('key' => $key, 'exception' => $e)); + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + CacheItem::validateKey($key); + + return $this->setMultiple(array($key => $value), $ttl); + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + $ids = array(); + + foreach ($keys as $key) { + $ids[] = $this->getId($key); + } + try { + $values = $this->doFetch($ids); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch requested values', array('keys' => $keys, 'exception' => $e)); + $values = array(); + } + $ids = array_combine($ids, $keys); + + return $this->generateValues($values, $ids, $default); + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + if (!is_array($values) && !$values instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', is_object($values) ? get_class($values) : gettype($values))); + } + $valuesById = array(); + + foreach ($values as $key => $value) { + if (is_int($key)) { + $key = (string) $key; + } + $valuesById[$this->getId($key)] = $value; + } + if (false === $ttl = $this->normalizeTtl($ttl)) { + return $this->doDelete(array_keys($valuesById)); + } + + try { + $e = $this->doSave($valuesById, $ttl); + } catch (\Exception $e) { + } + if (true === $e || array() === $e) { + return true; + } + $keys = array(); + foreach (is_array($e) ? $e : array_keys($valuesById) as $id) { + $keys[] = substr($id, strlen($this->namespace)); + } + CacheItem::log($this->logger, 'Failed to save values', array('keys' => $keys, 'exception' => $e instanceof \Exception ? $e : null)); + + return false; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + + return $this->deleteItems($keys); + } + + private function normalizeTtl($ttl) + { + if (null === $ttl) { + return $this->defaultLifetime; + } + if ($ttl instanceof \DateInterval) { + $ttl = (int) \DateTime::createFromFormat('U', 0)->add($ttl)->format('U'); + } + if (is_int($ttl)) { + return 0 < $ttl ? $ttl : false; + } + + throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given', is_object($ttl) ? get_class($ttl) : gettype($ttl))); + } + + private function generateValues($values, &$keys, $default) + { + try { + foreach ($values as $id => $value) { + if (!isset($keys[$id])) { + $id = key($keys); + } + $key = $keys[$id]; + unset($keys[$id]); + yield $key => $value; + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch requested values', array('keys' => array_values($keys), 'exception' => $e)); + } + + foreach ($keys as $key) { + yield $key => $default; + } + } +} diff --git a/src/Symfony/Component/Cache/Simple/ApcuCache.php b/src/Symfony/Component/Cache/Simple/ApcuCache.php new file mode 100644 index 0000000000000..e583b44341dce --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/ApcuCache.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Symfony\Component\Cache\Traits\ApcuTrait; + +class ApcuCache extends AbstractCache +{ + use ApcuTrait; + + /** + * @param string $namespace + * @param int $defaultLifetime + * @param string|null $version + */ + public function __construct($namespace = '', $defaultLifetime = 0, $version = null) + { + $this->init($namespace, $defaultLifetime, $version); + } +} diff --git a/src/Symfony/Component/Cache/Simple/ArrayCache.php b/src/Symfony/Component/Cache/Simple/ArrayCache.php new file mode 100644 index 0000000000000..a89768b0e2331 --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/ArrayCache.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Psr\Log\LoggerAwareInterface; +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\ArrayTrait; + +/** + * @author Nicolas Grekas + */ +class ArrayCache implements CacheInterface, LoggerAwareInterface +{ + use ArrayTrait { + ArrayTrait::deleteItem as delete; + ArrayTrait::hasItem as has; + } + + private $defaultLifetime; + + /** + * @param int $defaultLifetime + * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise + */ + public function __construct($defaultLifetime = 0, $storeSerialized = true) + { + $this->defaultLifetime = (int) $defaultLifetime; + $this->storeSerialized = $storeSerialized; + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + foreach ($this->getMultiple(array($key), $default) as $v) { + return $v; + } + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + foreach ($keys as $key) { + CacheItem::validateKey($key); + } + + return $this->generateItems($keys, time(), function ($k, $v, $hit) use ($default) { return $hit ? $v : $default; }); + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + if (!is_array($keys) && !$keys instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + CacheItem::validateKey($key); + + return $this->setMultiple(array($key => $value), $ttl); + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + if (!is_array($values) && !$values instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', is_object($values) ? get_class($values) : gettype($values))); + } + $valuesArray = array(); + + foreach ($values as $key => $value) { + is_int($key) || CacheItem::validateKey($key); + $valuesArray[$key] = $value; + } + if (false === $ttl = $this->normalizeTtl($ttl)) { + return $this->deleteMultiple(array_keys($valuesArray)); + } + if ($this->storeSerialized) { + foreach ($valuesArray as $key => $value) { + try { + $valuesArray[$key] = serialize($value); + } catch (\Exception $e) { + $type = is_object($value) ? get_class($value) : gettype($value); + CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', array('key' => $key, 'type' => $type, 'exception' => $e)); + + return false; + } + } + } + $expiry = 0 < $ttl ? time() + $ttl : PHP_INT_MAX; + + foreach ($valuesArray as $key => $value) { + $this->values[$key] = $value; + $this->expiries[$key] = $expiry; + } + + return true; + } + + private function normalizeTtl($ttl) + { + if (null === $ttl) { + return $this->defaultLifetime; + } + if ($ttl instanceof \DateInterval) { + $ttl = (int) \DateTime::createFromFormat('U', 0)->add($ttl)->format('U'); + } + if (is_int($ttl)) { + return 0 < $ttl ? $ttl : false; + } + + throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given', is_object($ttl) ? get_class($ttl) : gettype($ttl))); + } +} diff --git a/src/Symfony/Component/Cache/Simple/ChainCache.php b/src/Symfony/Component/Cache/Simple/ChainCache.php new file mode 100644 index 0000000000000..08bb4881b463f --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/ChainCache.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * Chains several caches together. + * + * Cached items are fetched from the first cache having them in its data store. + * They are saved and deleted in all caches at once. + * + * @author Nicolas Grekas + */ +class ChainCache implements CacheInterface +{ + private $miss; + private $caches = array(); + private $defaultLifetime; + private $cacheCount; + + /** + * @param CacheInterface[] $caches The ordered list of caches used to fetch cached items + * @param int $defaultLifetime The lifetime of items propagated from lower caches to upper ones + */ + public function __construct(array $caches, $defaultLifetime = 0) + { + if (!$caches) { + throw new InvalidArgumentException('At least one cache must be specified.'); + } + + foreach ($caches as $cache) { + if (!$cache instanceof CacheInterface) { + throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_class($cache), CacheInterface::class)); + } + } + + $this->miss = new \stdClass(); + $this->caches = array_values($caches); + $this->cacheCount = count($this->caches); + $this->defaultLifetime = 0 < $defaultLifetime ? (int) $defaultLifetime : null; + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $miss = null !== $default && is_object($default) ? $default : $this->miss; + + foreach ($this->caches as $i => $cache) { + $value = $cache->get($key, $miss); + + if ($miss !== $value) { + while (0 <= --$i) { + $this->caches[$i]->set($key, $value, $this->defaultLifetime); + } + + return $value; + } + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $miss = null !== $default && is_object($default) ? $default : $this->miss; + + return $this->generateItems($this->caches[0]->getMultiple($keys, $miss), 0, $miss, $default); + } + + private function generateItems($values, $cacheIndex, $miss, $default) + { + $missing = array(); + $nextCacheIndex = $cacheIndex + 1; + $nextCache = isset($this->caches[$nextCacheIndex]) ? $this->caches[$nextCacheIndex] : null; + + foreach ($values as $k => $value) { + if ($miss !== $value) { + yield $k => $value; + } elseif (!$nextCache) { + yield $k => $default; + } else { + $missing[] = $k; + } + } + + if ($missing) { + $cache = $this->caches[$cacheIndex]; + $values = $this->generateItems($nextCache->getMultiple($missing, $miss), $nextCacheIndex, $miss, $default); + + foreach ($values as $k => $value) { + if ($miss !== $value) { + $cache->set($k, $value, $this->defaultLifetime); + yield $k => $value; + } else { + yield $k => $default; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + foreach ($this->caches as $cache) { + if ($cache->has($key)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $cleared = true; + $i = $this->cacheCount; + + while ($i--) { + $cleared = $this->caches[$i]->clear() && $cleared; + } + + return $cleared; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $deleted = true; + $i = $this->cacheCount; + + while ($i--) { + $deleted = $this->caches[$i]->delete($key) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } + $deleted = true; + $i = $this->cacheCount; + + while ($i--) { + $deleted = $this->caches[$i]->deleteMultiple($keys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $saved = true; + $i = $this->cacheCount; + + while ($i--) { + $saved = $this->caches[$i]->set($key, $value, $ttl) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + if ($values instanceof \Traversable) { + $valuesIterator = $values; + $values = function () use ($valuesIterator, &$values) { + $generatedValues = array(); + + foreach ($valuesIterator as $key => $value) { + yield $key => $value; + $generatedValues[$key] = $value; + } + + $values = $generatedValues; + }; + $values = $values(); + } + $saved = true; + $i = $this->cacheCount; + + while ($i--) { + $saved = $this->caches[$i]->setMultiple($values, $ttl) && $saved; + } + + return $saved; + } +} diff --git a/src/Symfony/Component/Cache/Simple/DoctrineCache.php b/src/Symfony/Component/Cache/Simple/DoctrineCache.php new file mode 100644 index 0000000000000..00f0b9c6fc326 --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/DoctrineCache.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Doctrine\Common\Cache\CacheProvider; +use Symfony\Component\Cache\Traits\DoctrineTrait; + +class DoctrineCache extends AbstractCache +{ + use DoctrineTrait; + + /** + * @param CacheProvider $provider + * @param string $namespace + * @param int $defaultLifetime + */ + public function __construct(CacheProvider $provider, $namespace = '', $defaultLifetime = 0) + { + parent::__construct('', $defaultLifetime); + $this->provider = $provider; + $provider->setNamespace($namespace); + } +} diff --git a/src/Symfony/Component/Cache/Simple/FilesystemCache.php b/src/Symfony/Component/Cache/Simple/FilesystemCache.php new file mode 100644 index 0000000000000..ccd579534288e --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/FilesystemCache.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemTrait; + +class FilesystemCache extends AbstractCache implements PruneableInterface +{ + use FilesystemTrait; + + /** + * @param string $namespace + * @param int $defaultLifetime + * @param string|null $directory + */ + public function __construct($namespace = '', $defaultLifetime = 0, $directory = null) + { + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + } +} diff --git a/src/Symfony/Component/Cache/Simple/MemcachedCache.php b/src/Symfony/Component/Cache/Simple/MemcachedCache.php new file mode 100644 index 0000000000000..7717740622c5e --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/MemcachedCache.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Symfony\Component\Cache\Traits\MemcachedTrait; + +class MemcachedCache extends AbstractCache +{ + use MemcachedTrait; + + protected $maxIdLength = 250; + + /** + * @param \Memcached $client + * @param string $namespace + * @param int $defaultLifetime + */ + public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0) + { + $this->init($client, $namespace, $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Simple/NullCache.php b/src/Symfony/Component/Cache/Simple/NullCache.php new file mode 100644 index 0000000000000..fa986aebd11b0 --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/NullCache.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\Component\Cache\Simple; + +use Psr\SimpleCache\CacheInterface; + +/** + * @author Nicolas Grekas + */ +class NullCache implements CacheInterface +{ + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + return $default; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + foreach ($keys as $key) { + yield $key => $default; + } + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + return false; + } +} diff --git a/src/Symfony/Component/Cache/Simple/PdoCache.php b/src/Symfony/Component/Cache/Simple/PdoCache.php new file mode 100644 index 0000000000000..3e698e2f952c8 --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/PdoCache.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Symfony\Component\Cache\Traits\PdoTrait; + +class PdoCache extends AbstractCache +{ + use PdoTrait; + + protected $maxIdLength = 255; + + /** + * Constructor. + * + * You can either pass an existing database connection as PDO instance or + * a Doctrine DBAL Connection or a DSN string that will be used to + * lazy-connect to the database when the cache is actually used. + * + * List of available options: + * * db_table: The name of the table [default: cache_items] + * * db_id_col: The column where to store the cache id [default: item_id] + * * db_data_col: The column where to store the cache data [default: item_data] + * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime] + * * db_time_col: The column where to store the timestamp [default: item_time] + * * db_username: The username when lazy-connect [default: ''] + * * db_password: The password when lazy-connect [default: ''] + * * db_connection_options: An array of driver-specific connection options [default: array()] + * + * @param \PDO|Connection|string $connOrDsn A \PDO or Connection instance or DSN string or null + * @param string $namespace + * @param int $defaultLifetime + * @param array $options An associative array of options + * + * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string + * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + * @throws InvalidArgumentException When namespace contains invalid characters + */ + public function __construct($connOrDsn, $namespace = '', $defaultLifetime = 0, array $options = array()) + { + $this->init($connOrDsn, $namespace, $defaultLifetime, $options); + } +} diff --git a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php new file mode 100644 index 0000000000000..1c1ea42e0bc20 --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php @@ -0,0 +1,254 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\PhpArrayTrait; + +/** + * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0. + * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter. + * + * @author Titouan Galopin + * @author Nicolas Grekas + */ +class PhpArrayCache implements CacheInterface +{ + use PhpArrayTrait; + + /** + * @param string $file The PHP file were values are cached + * @param CacheInterface $fallbackPool A pool to fallback on when an item is not hit + */ + public function __construct($file, CacheInterface $fallbackPool) + { + $this->file = $file; + $this->fallbackPool = $fallbackPool; + } + + /** + * This adapter takes advantage of how PHP stores arrays in its latest versions. + * + * @param string $file The PHP file were values are cached + * + * @return CacheInterface + */ + public static function create($file, CacheInterface $fallbackPool) + { + // Shared memory is available in PHP 7.0+ with OPCache enabled + if (ini_get('opcache.enable')) { + return new static($file, $fallbackPool); + } + + return $fallbackPool; + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + if (null === $this->values) { + $this->initialize(); + } + if (!isset($this->values[$key])) { + return $this->fallbackPool->get($key, $default); + } + + $value = $this->values[$key]; + + if ('N;' === $value) { + $value = null; + } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) { + try { + $e = null; + $value = unserialize($value); + } catch (\Error $e) { + } catch (\Exception $e) { + } + if (null !== $e) { + return $default; + } + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + foreach ($keys as $key) { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + } + if (null === $this->values) { + $this->initialize(); + } + + return $this->generateItems($keys, $default); + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return isset($this->values[$key]) || $this->fallbackPool->has($key); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->values[$key]) && $this->fallbackPool->delete($key); + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + if (!is_array($keys) && !$keys instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + + $deleted = true; + $fallbackKeys = array(); + + foreach ($keys as $key) { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + + if (isset($this->values[$key])) { + $deleted = false; + } else { + $fallbackKeys[] = $key; + } + } + if (null === $this->values) { + $this->initialize(); + } + + if ($fallbackKeys) { + $deleted = $this->fallbackPool->deleteMultiple($fallbackKeys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + if (!is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->values[$key]) && $this->fallbackPool->set($key, $value, $ttl); + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + if (!is_array($values) && !$values instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', is_object($values) ? get_class($values) : gettype($values))); + } + + $saved = true; + $fallbackValues = array(); + + foreach ($values as $key => $value) { + if (!is_string($key) && !is_int($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', is_object($key) ? get_class($key) : gettype($key))); + } + + if (isset($this->values[$key])) { + $saved = false; + } else { + $fallbackValues[$key] = $value; + } + } + + if ($fallbackValues) { + $saved = $this->fallbackPool->setMultiple($fallbackValues, $ttl) && $saved; + } + + return $saved; + } + + private function generateItems(array $keys, $default) + { + $fallbackKeys = array(); + + foreach ($keys as $key) { + if (isset($this->values[$key])) { + $value = $this->values[$key]; + + if ('N;' === $value) { + yield $key => null; + } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) { + try { + yield $key => unserialize($value); + } catch (\Error $e) { + yield $key => $default; + } catch (\Exception $e) { + yield $key => $default; + } + } else { + yield $key => $value; + } + } else { + $fallbackKeys[] = $key; + } + } + + if ($fallbackKeys) { + foreach ($this->fallbackPool->getMultiple($fallbackKeys, $default) as $key => $item) { + yield $key => $item; + } + } + } +} diff --git a/src/Symfony/Component/Cache/Simple/PhpFilesCache.php b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php new file mode 100644 index 0000000000000..38a9fe3e5f18d --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\PhpFilesTrait; + +class PhpFilesCache extends AbstractCache implements PruneableInterface +{ + use PhpFilesTrait; + + /** + * @param string $namespace + * @param int $defaultLifetime + * @param string|null $directory + * + * @throws CacheException if OPcache is not enabled + */ + public function __construct($namespace = '', $defaultLifetime = 0, $directory = null) + { + if (!static::isSupported()) { + throw new CacheException('OPcache is not enabled'); + } + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + + $e = new \Exception(); + $this->includeHandler = function () use ($e) { throw $e; }; + } +} diff --git a/src/Symfony/Component/Cache/Simple/Psr6Cache.php b/src/Symfony/Component/Cache/Simple/Psr6Cache.php new file mode 100644 index 0000000000000..55fa98da1274c --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/Psr6Cache.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\CacheException as Psr6CacheException; +use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\CacheException as SimpleCacheException; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + */ +class Psr6Cache implements CacheInterface +{ + private $pool; + private $createCacheItem; + + public function __construct(CacheItemPoolInterface $pool) + { + $this->pool = $pool; + + if ($pool instanceof AbstractAdapter) { + $this->createCacheItem = \Closure::bind( + function ($key, $value, $allowInt = false) { + if ($allowInt && is_int($key)) { + $key = (string) $key; + } else { + CacheItem::validateKey($key); + } + $f = $this->createCacheItem; + + return $f($key, $value, false); + }, + $pool, + AbstractAdapter::class + ); + } + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + try { + $item = $this->pool->getItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + return $item->isHit() ? $item->get() : $default; + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + try { + if (null !== $f = $this->createCacheItem) { + $item = $f($key, $value); + } else { + $item = $this->pool->getItem($key)->set($value); + } + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + + return $this->pool->save($item); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + try { + return $this->pool->deleteItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + + try { + $items = $this->pool->getItems($keys); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + $values = array(); + + foreach ($items as $key => $item) { + $values[$key] = $item->isHit() ? $item->get() : $default; + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $valuesIsArray = is_array($values); + if (!$valuesIsArray && !$values instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given', is_object($values) ? get_class($values) : gettype($values))); + } + $items = array(); + + try { + if (null !== $f = $this->createCacheItem) { + $valuesIsArray = false; + foreach ($values as $key => $value) { + $items[$key] = $f($key, $value, true); + } + } elseif ($valuesIsArray) { + $items = array(); + foreach ($values as $key => $value) { + $items[] = (string) $key; + } + $items = $this->pool->getItems($items); + } else { + foreach ($values as $key => $value) { + if (is_int($key)) { + $key = (string) $key; + } + $items[$key] = $this->pool->getItem($key)->set($value); + } + } + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + $ok = true; + + foreach ($items as $key => $item) { + if ($valuesIsArray) { + $item->set($values[$key]); + } + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + $ok = $this->pool->saveDeferred($item) && $ok; + } + + return $this->pool->commit() && $ok; + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given', is_object($keys) ? get_class($keys) : gettype($keys))); + } + + try { + return $this->pool->deleteItems($keys); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + try { + return $this->pool->hasItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/src/Symfony/Component/Cache/Simple/RedisCache.php b/src/Symfony/Component/Cache/Simple/RedisCache.php new file mode 100644 index 0000000000000..e82c0627e241d --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/RedisCache.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Simple; + +use Symfony\Component\Cache\Traits\RedisTrait; + +class RedisCache extends AbstractCache +{ + use RedisTrait; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient + * @param string $namespace + * @param int $defaultLifetime + */ + public function __construct($redisClient, $namespace = '', $defaultLifetime = 0) + { + $this->init($redisClient, $namespace, $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Simple/TraceableCache.php b/src/Symfony/Component/Cache/Simple/TraceableCache.php new file mode 100644 index 0000000000000..29cc10bbb27a1 --- /dev/null +++ b/src/Symfony/Component/Cache/Simple/TraceableCache.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\Component\Cache\Simple; + +use Psr\SimpleCache\CacheInterface; + +/** + * An adapter that collects data about all cache calls. + * + * @author Nicolas Grekas + */ +class TraceableCache implements CacheInterface +{ + private $pool; + private $miss; + private $calls = array(); + + public function __construct(CacheInterface $pool) + { + $this->pool = $pool; + $this->miss = new \stdClass(); + } + + /** + * {@inheritdoc} + */ + public function get($key, $default = null) + { + $miss = null !== $default && is_object($default) ? $default : $this->miss; + $event = $this->start(__FUNCTION__); + try { + $value = $this->pool->get($key, $miss); + } finally { + $event->end = microtime(true); + } + if ($event->result[$key] = $miss !== $value) { + ++$event->hits; + } else { + ++$event->misses; + $value = $default; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function has($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->has($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->delete($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function set($key, $value, $ttl = null) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->set($key, $value, $ttl); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function setMultiple($values, $ttl = null) + { + $event = $this->start(__FUNCTION__); + $event->result['keys'] = array(); + + if ($values instanceof \Traversable) { + $values = function () use ($values, $event) { + foreach ($values as $k => $v) { + $event->result['keys'][] = $k; + yield $k => $v; + } + }; + $values = $values(); + } elseif (is_array($values)) { + $event->result['keys'] = array_keys($values); + } + + try { + return $event->result['result'] = $this->pool->setMultiple($values, $ttl); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function getMultiple($keys, $default = null) + { + $miss = null !== $default && is_object($default) ? $default : $this->miss; + $event = $this->start(__FUNCTION__); + try { + $result = $this->pool->getMultiple($keys, $miss); + } finally { + $event->end = microtime(true); + } + $f = function () use ($result, $event, $miss, $default) { + $event->result = array(); + foreach ($result as $key => $value) { + if ($event->result[$key] = $miss !== $value) { + ++$event->hits; + } else { + ++$event->misses; + $value = $default; + } + yield $key => $value; + } + }; + + return $f(); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->clear(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($keys) + { + $event = $this->start(__FUNCTION__); + if ($keys instanceof \Traversable) { + $keys = $event->result['keys'] = iterator_to_array($keys, false); + } else { + $event->result['keys'] = $keys; + } + try { + return $event->result['result'] = $this->pool->deleteMultiple($keys); + } finally { + $event->end = microtime(true); + } + } + + public function getCalls() + { + try { + return $this->calls; + } finally { + $this->calls = array(); + } + } + + private function start($name) + { + $this->calls[] = $event = new TraceableCacheEvent(); + $event->name = $name; + $event->start = microtime(true); + + return $event; + } +} + +class TraceableCacheEvent +{ + public $name; + public $start; + public $end; + public $result; + public $hits = 0; + public $misses = 0; +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTest.php new file mode 100644 index 0000000000000..86b16d436f23f --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\RedisAdapter; + +abstract class AbstractRedisAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testExpiration' => 'Testing expiration slows down the test suite', + 'testHasItemReturnsFalseWhenDeferredItemIsExpired' => 'Testing expiration slows down the test suite', + 'testDefaultLifeTime' => 'Testing expiration slows down the test suite', + ); + + protected static $redis; + + public function createCachePool($defaultLifetime = 0) + { + return new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); + } + + public static function setupBeforeClass() + { + if (!extension_loaded('redis')) { + self::markTestSkipped('Extension redis required.'); + } + if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) { + $e = error_get_last(); + self::markTestSkipped($e['message']); + } + } + + public static function tearDownAfterClass() + { + self::$redis->flushDB(); + self::$redis = null; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php new file mode 100644 index 0000000000000..db715c243903e --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Cache\IntegrationTests\CachePoolTest; +use Symfony\Component\Cache\PruneableInterface; + +abstract class AdapterTestCase extends CachePoolTest +{ + protected function setUp() + { + parent::setUp(); + + if (!array_key_exists('testPrune', $this->skippedTests) && !$this->createCachePool() instanceof PruneableInterface) { + $this->skippedTests['testPrune'] = 'Not a pruneable cache pool.'; + } + } + + public function testDefaultLifeTime() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = $this->createCachePool(2); + + $item = $cache->getItem('key.dlt'); + $item->set('value'); + $cache->save($item); + sleep(1); + + $item = $cache->getItem('key.dlt'); + $this->assertTrue($item->isHit()); + + sleep(2); + $item = $cache->getItem('key.dlt'); + $this->assertFalse($item->isHit()); + } + + public function testNotUnserializable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = $this->createCachePool(); + + $item = $cache->getItem('foo'); + $cache->save($item->set(new NotUnserializable())); + + $item = $cache->getItem('foo'); + $this->assertFalse($item->isHit()); + + foreach ($cache->getItems(array('foo')) as $item) { + } + $cache->save($item->set(new NotUnserializable())); + + foreach ($cache->getItems(array('foo')) as $item) { + } + $this->assertFalse($item->isHit()); + } + + public function testPrune() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + if (!method_exists($this, 'isPruned')) { + $this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.'); + } + + $cache = $this->createCachePool(); + + $doSet = function ($name, $value, \DateInterval $expiresAfter = null) use ($cache) { + $item = $cache->getItem($name); + $item->set($value); + + if ($expiresAfter) { + $item->expiresAfter($expiresAfter); + } + + $cache->save($item); + }; + + $doSet('foo', 'foo-val'); + $doSet('bar', 'bar-val', new \DateInterval('PT20S')); + $doSet('baz', 'baz-val', new \DateInterval('PT40S')); + $doSet('qux', 'qux-val', new \DateInterval('PT80S')); + + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertFalse($this->isPruned($cache, 'bar')); + $this->assertFalse($this->isPruned($cache, 'baz')); + $this->assertFalse($this->isPruned($cache, 'qux')); + + sleep(30); + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'bar')); + $this->assertFalse($this->isPruned($cache, 'baz')); + $this->assertFalse($this->isPruned($cache, 'qux')); + + sleep(30); + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'baz')); + $this->assertFalse($this->isPruned($cache, 'qux')); + + sleep(30); + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'qux')); + } +} + +class NotUnserializable implements \Serializable +{ + public function serialize() + { + return serialize(123); + } + + public function unserialize($ser) + { + throw new \Exception(__CLASS__); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ApcuAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ApcuAdapterTest.php new file mode 100644 index 0000000000000..6cad6135cf4a0 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/ApcuAdapterTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Psr\Log\NullLogger; +use Symfony\Component\Cache\Adapter\ApcuAdapter; + +class ApcuAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testExpiration' => 'Testing expiration slows down the test suite', + 'testHasItemReturnsFalseWhenDeferredItemIsExpired' => 'Testing expiration slows down the test suite', + 'testDefaultLifeTime' => 'Testing expiration slows down the test suite', + ); + + public function createCachePool($defaultLifetime = 0) + { + if (!function_exists('apcu_fetch') || !ini_get('apc.enabled')) { + $this->markTestSkipped('APCu extension is required.'); + } + if ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { + if ('testWithCliSapi' !== $this->getName()) { + $this->markTestSkipped('APCu extension is required.'); + } + } + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Fails transiently on Windows.'); + } + + return new ApcuAdapter(str_replace('\\', '.', __CLASS__), $defaultLifetime); + } + + public function testUnserializable() + { + $pool = $this->createCachePool(); + + $item = $pool->getItem('foo'); + $item->set(function () {}); + + $this->assertFalse($pool->save($item)); + + $item = $pool->getItem('foo'); + $this->assertFalse($item->isHit()); + } + + public function testVersion() + { + $namespace = str_replace('\\', '.', get_class($this)); + + $pool1 = new ApcuAdapter($namespace, 0, 'p1'); + + $item = $pool1->getItem('foo'); + $this->assertFalse($item->isHit()); + $this->assertTrue($pool1->save($item->set('bar'))); + + $item = $pool1->getItem('foo'); + $this->assertTrue($item->isHit()); + $this->assertSame('bar', $item->get()); + + $pool2 = new ApcuAdapter($namespace, 0, 'p2'); + + $item = $pool2->getItem('foo'); + $this->assertFalse($item->isHit()); + $this->assertNull($item->get()); + + $item = $pool1->getItem('foo'); + $this->assertFalse($item->isHit()); + $this->assertNull($item->get()); + } + + public function testWithCliSapi() + { + try { + // disable PHPUnit error handler to mimic a production environment + $isCalled = false; + set_error_handler(function () use (&$isCalled) { + $isCalled = true; + }); + $pool = new ApcuAdapter(str_replace('\\', '.', __CLASS__)); + $pool->setLogger(new NullLogger()); + + $item = $pool->getItem('foo'); + $item->isHit(); + $pool->save($item->set('bar')); + $this->assertFalse($isCalled); + } finally { + restore_error_handler(); + } + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php new file mode 100644 index 0000000000000..725d79015082e --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +/** + * @group time-sensitive + */ +class ArrayAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', + 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', + ); + + public function createCachePool($defaultLifetime = 0) + { + return new ArrayAdapter($defaultLifetime); + } + + public function testGetValuesHitAndMiss() + { + /** @var ArrayAdapter $cache */ + $cache = $this->createCachePool(); + + // Hit + $item = $cache->getItem('foo'); + $item->set('4711'); + $cache->save($item); + + $fooItem = $cache->getItem('foo'); + $this->assertTrue($fooItem->isHit()); + $this->assertEquals('4711', $fooItem->get()); + + // Miss (should be present as NULL in $values) + $cache->getItem('bar'); + + $values = $cache->getValues(); + + $this->assertCount(2, $values); + $this->assertArrayHasKey('foo', $values); + $this->assertSame(serialize('4711'), $values['foo']); + $this->assertArrayHasKey('bar', $values); + $this->assertNull($values['bar']); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php new file mode 100644 index 0000000000000..b80913c6e089c --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter; + +/** + * @author Kévin Dunglas + * @group time-sensitive + */ +class ChainAdapterTest extends AdapterTestCase +{ + public function createCachePool($defaultLifetime = 0) + { + return new ChainAdapter(array(new ArrayAdapter($defaultLifetime), new ExternalAdapter(), new FilesystemAdapter('', $defaultLifetime)), $defaultLifetime); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage At least one adapter must be specified. + */ + public function testEmptyAdaptersException() + { + new ChainAdapter(array()); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage The class "stdClass" does not implement + */ + public function testInvalidAdapterException() + { + new ChainAdapter(array(new \stdClass())); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php new file mode 100644 index 0000000000000..93ec9824388e1 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Cache\Adapter\DoctrineAdapter; + +/** + * @group time-sensitive + */ +class DoctrineAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayCache is not.', + 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayCache is not.', + 'testNotUnserializable' => 'ArrayCache does not use serialize/unserialize', + ); + + public function createCachePool($defaultLifetime = 0) + { + return new DoctrineAdapter(new ArrayCache($defaultLifetime), '', $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php new file mode 100644 index 0000000000000..b6757514eb67e --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; + +/** + * @group time-sensitive + */ +class FilesystemAdapterTest extends AdapterTestCase +{ + public function createCachePool($defaultLifetime = 0) + { + return new FilesystemAdapter('', $defaultLifetime); + } + + public static function tearDownAfterClass() + { + self::rmdir(sys_get_temp_dir().'/symfony-cache'); + } + + public static function rmdir($dir) + { + if (!file_exists($dir)) { + return; + } + if (!$dir || 0 !== strpos(dirname($dir), sys_get_temp_dir())) { + throw new \Exception(__METHOD__."() operates only on subdirs of system's temp dir"); + } + $children = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($children as $child) { + if ($child->isDir()) { + rmdir($child); + } else { + unlink($child); + } + } + rmdir($dir); + } + + protected function isPruned(CacheItemPoolInterface $cache, $name) + { + $getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile'); + $getFileMethod->setAccessible(true); + + return !file_exists($getFileMethod->invoke($cache, $name)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php new file mode 100644 index 0000000000000..cf2384c5f37e6 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\AbstractAdapter; + +class MaxIdLengthAdapterTest extends TestCase +{ + public function testLongKey() + { + $cache = $this->getMockBuilder(MaxIdLengthAdapter::class) + ->setConstructorArgs(array(str_repeat('-', 10))) + ->setMethods(array('doHave', 'doFetch', 'doDelete', 'doSave', 'doClear')) + ->getMock(); + + $cache->expects($this->exactly(2)) + ->method('doHave') + ->withConsecutive( + array($this->equalTo('----------:0GTYWa9n4ed8vqNlOT2iEr:')), + array($this->equalTo('----------:---------------------------------------')) + ); + + $cache->hasItem(str_repeat('-', 40)); + $cache->hasItem(str_repeat('-', 39)); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Namespace must be 26 chars max, 40 given ("----------------------------------------") + */ + public function testTooLongNamespace() + { + $cache = $this->getMockBuilder(MaxIdLengthAdapter::class) + ->setConstructorArgs(array(str_repeat('-', 40))) + ->getMock(); + } +} + +abstract class MaxIdLengthAdapter extends AbstractAdapter +{ + protected $maxIdLength = 50; + + public function __construct($ns) + { + parent::__construct($ns); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php new file mode 100644 index 0000000000000..82b41c3b4d870 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\MemcachedAdapter; + +class MemcachedAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testExpiration' => 'Testing expiration slows down the test suite', + 'testHasItemReturnsFalseWhenDeferredItemIsExpired' => 'Testing expiration slows down the test suite', + 'testDefaultLifeTime' => 'Testing expiration slows down the test suite', + ); + + protected static $client; + + public static function setupBeforeClass() + { + if (!MemcachedAdapter::isSupported()) { + self::markTestSkipped('Extension memcached >=2.2.0 required.'); + } + self::$client = AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'), array('binary_protocol' => false)); + self::$client->get('foo'); + $code = self::$client->getResultCode(); + + if (\Memcached::RES_SUCCESS !== $code && \Memcached::RES_NOTFOUND !== $code) { + self::markTestSkipped('Memcached error: '.strtolower(self::$client->getResultMessage())); + } + } + + public function createCachePool($defaultLifetime = 0) + { + $client = $defaultLifetime ? AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST')) : self::$client; + + return new MemcachedAdapter($client, str_replace('\\', '.', __CLASS__), $defaultLifetime); + } + + public function testOptions() + { + $client = MemcachedAdapter::createConnection(array(), array( + 'libketama_compatible' => false, + 'distribution' => 'modula', + 'compression' => true, + 'serializer' => 'php', + 'hash' => 'md5', + )); + + $this->assertSame(\Memcached::SERIALIZER_PHP, $client->getOption(\Memcached::OPT_SERIALIZER)); + $this->assertSame(\Memcached::HASH_MD5, $client->getOption(\Memcached::OPT_HASH)); + $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION)); + $this->assertSame(0, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE)); + $this->assertSame(\Memcached::DISTRIBUTION_MODULA, $client->getOption(\Memcached::OPT_DISTRIBUTION)); + } + + /** + * @dataProvider provideBadOptions + * @expectedException \ErrorException + * @expectedExceptionMessage constant(): Couldn't find constant Memcached:: + */ + public function testBadOptions($name, $value) + { + MemcachedAdapter::createConnection(array(), array($name => $value)); + } + + public function provideBadOptions() + { + return array( + array('foo', 'bar'), + array('hash', 'zyx'), + array('serializer', 'zyx'), + array('distribution', 'zyx'), + ); + } + + public function testDefaultOptions() + { + $this->assertTrue(MemcachedAdapter::isSupported()); + + $client = MemcachedAdapter::createConnection(array()); + + $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION)); + $this->assertSame(1, $client->getOption(\Memcached::OPT_BINARY_PROTOCOL)); + $this->assertSame(1, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE)); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\CacheException + * @expectedExceptionMessage MemcachedAdapter: "serializer" option must be "php" or "igbinary". + */ + public function testOptionSerializer() + { + if (!\Memcached::HAVE_JSON) { + $this->markTestSkipped('Memcached::HAVE_JSON required'); + } + + new MemcachedAdapter(MemcachedAdapter::createConnection(array(), array('serializer' => 'json'))); + } + + /** + * @dataProvider provideServersSetting + */ + public function testServersSetting($dsn, $host, $port) + { + $client1 = MemcachedAdapter::createConnection($dsn); + $client2 = MemcachedAdapter::createConnection(array($dsn)); + $client3 = MemcachedAdapter::createConnection(array(array($host, $port))); + $expect = array( + 'host' => $host, + 'port' => $port, + ); + + $f = function ($s) { return array('host' => $s['host'], 'port' => $s['port']); }; + $this->assertSame(array($expect), array_map($f, $client1->getServerList())); + $this->assertSame(array($expect), array_map($f, $client2->getServerList())); + $this->assertSame(array($expect), array_map($f, $client3->getServerList())); + } + + public function provideServersSetting() + { + yield array( + 'memcached://127.0.0.1/50', + '127.0.0.1', + 11211, + ); + yield array( + 'memcached://localhost:11222?weight=25', + 'localhost', + 11222, + ); + if (ini_get('memcached.use_sasl')) { + yield array( + 'memcached://user:password@127.0.0.1?weight=50', + '127.0.0.1', + 11211, + ); + } + yield array( + 'memcached:///var/run/memcached.sock?weight=25', + '/var/run/memcached.sock', + 0, + ); + yield array( + 'memcached:///var/local/run/memcached.socket?weight=25', + '/var/local/run/memcached.socket', + 0, + ); + if (ini_get('memcached.use_sasl')) { + yield array( + 'memcached://user:password@/var/local/run/memcached.socket?weight=25', + '/var/local/run/memcached.socket', + 0, + ); + } + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/NamespacedProxyAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/NamespacedProxyAdapterTest.php new file mode 100644 index 0000000000000..c2714033385f4 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/NamespacedProxyAdapterTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ProxyAdapter; + +/** + * @group time-sensitive + */ +class NamespacedProxyAdapterTest extends ProxyAdapterTest +{ + public function createCachePool($defaultLifetime = 0) + { + return new ProxyAdapter(new ArrayAdapter($defaultLifetime), 'foo', $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/NullAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/NullAdapterTest.php new file mode 100644 index 0000000000000..c28a4550263df --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/NullAdapterTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use PHPUnit\Framework\TestCase; +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\Adapter\NullAdapter; + +/** + * @group time-sensitive + */ +class NullAdapterTest extends TestCase +{ + public function createCachePool() + { + return new NullAdapter(); + } + + public function testGetItem() + { + $adapter = $this->createCachePool(); + + $item = $adapter->getItem('key'); + $this->assertFalse($item->isHit()); + $this->assertNull($item->get(), "Item's value must be null when isHit is false."); + } + + public function testHasItem() + { + $this->assertFalse($this->createCachePool()->hasItem('key')); + } + + public function testGetItems() + { + $adapter = $this->createCachePool(); + + $keys = array('foo', 'bar', 'baz', 'biz'); + + /** @var CacheItemInterface[] $items */ + $items = $adapter->getItems($keys); + $count = 0; + + foreach ($items as $key => $item) { + $itemKey = $item->getKey(); + + $this->assertEquals($itemKey, $key, 'Keys must be preserved when fetching multiple items'); + $this->assertTrue(in_array($key, $keys), 'Cache key can not change.'); + $this->assertFalse($item->isHit()); + + // Remove $key for $keys + foreach ($keys as $k => $v) { + if ($v === $key) { + unset($keys[$k]); + } + } + + ++$count; + } + + $this->assertSame(4, $count); + } + + public function testIsHit() + { + $adapter = $this->createCachePool(); + + $item = $adapter->getItem('key'); + $this->assertFalse($item->isHit()); + } + + public function testClear() + { + $this->assertTrue($this->createCachePool()->clear()); + } + + public function testDeleteItem() + { + $this->assertTrue($this->createCachePool()->deleteItem('key')); + } + + public function testDeleteItems() + { + $this->assertTrue($this->createCachePool()->deleteItems(array('key', 'foo', 'bar'))); + } + + public function testSave() + { + $adapter = $this->createCachePool(); + + $item = $adapter->getItem('key'); + $this->assertFalse($item->isHit()); + $this->assertNull($item->get(), "Item's value must be null when isHit is false."); + + $this->assertFalse($adapter->save($item)); + } + + public function testDeferredSave() + { + $adapter = $this->createCachePool(); + + $item = $adapter->getItem('key'); + $this->assertFalse($item->isHit()); + $this->assertNull($item->get(), "Item's value must be null when isHit is false."); + + $this->assertFalse($adapter->saveDeferred($item)); + } + + public function testCommit() + { + $adapter = $this->createCachePool(); + + $item = $adapter->getItem('key'); + $this->assertFalse($item->isHit()); + $this->assertNull($item->get(), "Item's value must be null when isHit is false."); + + $this->assertFalse($adapter->saveDeferred($item)); + $this->assertFalse($this->createCachePool()->commit()); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php new file mode 100644 index 0000000000000..90c9ece8bcc9d --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\PdoAdapter; + +/** + * @group time-sensitive + */ +class PdoAdapterTest extends AdapterTestCase +{ + protected static $dbFile; + + public static function setupBeforeClass() + { + if (!extension_loaded('pdo_sqlite')) { + self::markTestSkipped('Extension pdo_sqlite required.'); + } + + self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); + + $pool = new PdoAdapter('sqlite:'.self::$dbFile); + $pool->createTable(); + } + + public static function tearDownAfterClass() + { + @unlink(self::$dbFile); + } + + public function createCachePool($defaultLifetime = 0) + { + return new PdoAdapter('sqlite:'.self::$dbFile, 'ns', $defaultLifetime); + } + + public function testCleanupExpiredItems() + { + $pdo = new \PDO('sqlite:'.self::$dbFile); + + $getCacheItemCount = function () use ($pdo) { + return (int) $pdo->query('SELECT COUNT(*) FROM cache_items')->fetch(\PDO::FETCH_COLUMN); + }; + + $this->assertSame(0, $getCacheItemCount()); + + $cache = $this->createCachePool(); + + $item = $cache->getItem('some_nice_key'); + $item->expiresAfter(1); + $item->set(1); + + $cache->save($item); + $this->assertSame(1, $getCacheItemCount()); + + sleep(2); + + $newItem = $cache->getItem($item->getKey()); + $this->assertFalse($newItem->isHit()); + $this->assertSame(0, $getCacheItemCount(), 'PDOAdapter must clean up expired items'); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PdoDbalAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PdoDbalAdapterTest.php new file mode 100644 index 0000000000000..b9c396fdc59eb --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PdoDbalAdapterTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Doctrine\DBAL\DriverManager; +use Symfony\Component\Cache\Adapter\PdoAdapter; + +/** + * @group time-sensitive + */ +class PdoDbalAdapterTest extends AdapterTestCase +{ + protected static $dbFile; + + public static function setupBeforeClass() + { + if (!extension_loaded('pdo_sqlite')) { + self::markTestSkipped('Extension pdo_sqlite required.'); + } + + self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); + + $pool = new PdoAdapter(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile))); + $pool->createTable(); + } + + public static function tearDownAfterClass() + { + @unlink(self::$dbFile); + } + + public function createCachePool($defaultLifetime = 0) + { + return new PdoAdapter(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile)), '', $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php new file mode 100644 index 0000000000000..134dba7c90444 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\Adapter\NullAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; + +/** + * @group time-sensitive + */ +class PhpArrayAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testBasicUsage' => 'PhpArrayAdapter is read-only.', + 'testBasicUsageWithLongKey' => 'PhpArrayAdapter is read-only.', + 'testClear' => 'PhpArrayAdapter is read-only.', + 'testClearWithDeferredItems' => 'PhpArrayAdapter is read-only.', + 'testDeleteItem' => 'PhpArrayAdapter is read-only.', + 'testSaveExpired' => 'PhpArrayAdapter is read-only.', + 'testSaveWithoutExpire' => 'PhpArrayAdapter is read-only.', + 'testDeferredSave' => 'PhpArrayAdapter is read-only.', + 'testDeferredSaveWithoutCommit' => 'PhpArrayAdapter is read-only.', + 'testDeleteItems' => 'PhpArrayAdapter is read-only.', + 'testDeleteDeferredItem' => 'PhpArrayAdapter is read-only.', + 'testCommit' => 'PhpArrayAdapter is read-only.', + 'testSaveDeferredWhenChangingValues' => 'PhpArrayAdapter is read-only.', + 'testSaveDeferredOverwrite' => 'PhpArrayAdapter is read-only.', + 'testIsHitDeferred' => 'PhpArrayAdapter is read-only.', + + 'testExpiresAt' => 'PhpArrayAdapter does not support expiration.', + 'testExpiresAtWithNull' => 'PhpArrayAdapter does not support expiration.', + 'testExpiresAfterWithNull' => 'PhpArrayAdapter does not support expiration.', + 'testDeferredExpired' => 'PhpArrayAdapter does not support expiration.', + 'testExpiration' => 'PhpArrayAdapter does not support expiration.', + + 'testGetItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testGetItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testHasItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testDeleteItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + + 'testDefaultLifeTime' => 'PhpArrayAdapter does not allow configuring a default lifetime.', + ); + + protected static $file; + + public static function setupBeforeClass() + { + self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php'; + } + + protected function tearDown() + { + if (file_exists(sys_get_temp_dir().'/symfony-cache')) { + FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + } + } + + public function createCachePool() + { + return new PhpArrayAdapterWrapper(self::$file, new NullAdapter()); + } + + public function testStore() + { + $arrayWithRefs = array(); + $arrayWithRefs[0] = 123; + $arrayWithRefs[1] = &$arrayWithRefs[0]; + + $object = (object) array( + 'foo' => 'bar', + 'foo2' => 'bar2', + ); + + $expected = array( + 'null' => null, + 'serializedString' => serialize($object), + 'arrayWithRefs' => $arrayWithRefs, + 'object' => $object, + 'arrayWithObject' => array('bar' => $object), + ); + + $adapter = $this->createCachePool(); + $adapter->warmUp($expected); + + foreach ($expected as $key => $value) { + $this->assertSame(serialize($value), serialize($adapter->getItem($key)->get()), 'Warm up should create a PHP file that OPCache can load in memory'); + } + } + + public function testStoredFile() + { + $expected = array( + 'integer' => 42, + 'float' => 42.42, + 'boolean' => true, + 'array_simple' => array('foo', 'bar'), + 'array_associative' => array('foo' => 'bar', 'foo2' => 'bar2'), + ); + + $adapter = $this->createCachePool(); + $adapter->warmUp($expected); + + $values = eval(substr(file_get_contents(self::$file), 6)); + + $this->assertSame($expected, $values, 'Warm up should create a PHP file that OPCache can load in memory'); + } +} + +class PhpArrayAdapterWrapper extends PhpArrayAdapter +{ + public function save(CacheItemInterface $item) + { + call_user_func(\Closure::bind(function () use ($item) { + $this->values[$item->getKey()] = $item->get(); + $this->warmUp($this->values); + $this->values = eval(substr(file_get_contents($this->file), 6)); + }, $this, PhpArrayAdapter::class)); + + return true; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php new file mode 100644 index 0000000000000..45a50d2323a61 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; + +/** + * @group time-sensitive + */ +class PhpArrayAdapterWithFallbackTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testGetItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testGetItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testHasItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testDeleteItemInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + 'testDeleteItemsInvalidKeys' => 'PhpArrayAdapter does not throw exceptions on invalid key.', + ); + + protected static $file; + + public static function setupBeforeClass() + { + self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php'; + } + + protected function tearDown() + { + if (file_exists(sys_get_temp_dir().'/symfony-cache')) { + FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + } + } + + public function createCachePool($defaultLifetime = 0) + { + return new PhpArrayAdapter(self::$file, new FilesystemAdapter('php-array-fallback', $defaultLifetime)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php new file mode 100644 index 0000000000000..8e93c937f6a65 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\PhpFilesAdapter; + +/** + * @group time-sensitive + */ +class PhpFilesAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testDefaultLifeTime' => 'PhpFilesAdapter does not allow configuring a default lifetime.', + ); + + public function createCachePool() + { + if (!PhpFilesAdapter::isSupported()) { + $this->markTestSkipped('OPcache extension is not enabled.'); + } + + return new PhpFilesAdapter('sf-cache'); + } + + public static function tearDownAfterClass() + { + FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + } + + protected function isPruned(CacheItemPoolInterface $cache, $name) + { + $getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile'); + $getFileMethod->setAccessible(true); + + return !file_exists($getFileMethod->invoke($cache, $name)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php new file mode 100644 index 0000000000000..85ca36c9ef263 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Predis\Connection\StreamConnection; +use Symfony\Component\Cache\Adapter\RedisAdapter; + +class PredisAdapterTest extends AbstractRedisAdapterTest +{ + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + self::$redis = new \Predis\Client(array('host' => getenv('REDIS_HOST'))); + } + + public function testCreateConnection() + { + $redisHost = getenv('REDIS_HOST'); + + $redis = RedisAdapter::createConnection('redis://'.$redisHost.'/1', array('class' => \Predis\Client::class, 'timeout' => 3)); + $this->assertInstanceOf(\Predis\Client::class, $redis); + + $connection = $redis->getConnection(); + $this->assertInstanceOf(StreamConnection::class, $connection); + + $params = array( + 'scheme' => 'tcp', + 'host' => $redisHost, + 'path' => '', + 'dbindex' => '1', + 'port' => 6379, + 'class' => 'Predis\Client', + 'timeout' => 3, + 'persistent' => 0, + 'persistent_id' => null, + 'read_timeout' => 0, + 'retry_interval' => 0, + 'database' => '1', + 'password' => null, + ); + $this->assertSame($params, $connection->getParameters()->toArray()); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisClusterAdapterTest.php new file mode 100644 index 0000000000000..6ed1c7d6a9f3b --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisClusterAdapterTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +class PredisClusterAdapterTest extends AbstractRedisAdapterTest +{ + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + self::$redis = new \Predis\Client(array(array('host' => getenv('REDIS_HOST')))); + } + + public static function tearDownAfterClass() + { + self::$redis->getConnection()->getConnectionByKey('foo')->executeCommand(self::$redis->createCommand('FLUSHDB')); + self::$redis = null; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php new file mode 100644 index 0000000000000..c0174dd248222 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ProxyAdapter; +use Symfony\Component\Cache\CacheItem; + +/** + * @group time-sensitive + */ +class ProxyAdapterTest extends AdapterTestCase +{ + protected $skippedTests = array( + 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', + 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', + ); + + public function createCachePool($defaultLifetime = 0) + { + return new ProxyAdapter(new ArrayAdapter(), '', $defaultLifetime); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage OK bar + */ + public function testProxyfiedItem() + { + $item = new CacheItem(); + $pool = new ProxyAdapter(new TestingArrayAdapter($item)); + + $proxyItem = $pool->getItem('foo'); + + $this->assertFalse($proxyItem === $item); + $pool->save($proxyItem->set('bar')); + } +} + +class TestingArrayAdapter extends ArrayAdapter +{ + private $item; + + public function __construct(CacheItemInterface $item) + { + $this->item = $item; + } + + public function getItem($key) + { + return $this->item; + } + + public function save(CacheItemInterface $item) + { + if ($item === $this->item) { + throw new \Exception('OK '.$item->get()); + } + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php new file mode 100644 index 0000000000000..a8f7a673f8b87 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\RedisAdapter; + +class RedisAdapterTest extends AbstractRedisAdapterTest +{ + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + self::$redis = AbstractAdapter::createConnection('redis://'.getenv('REDIS_HOST')); + } + + public function testCreateConnection() + { + $redisHost = getenv('REDIS_HOST'); + + $redis = RedisAdapter::createConnection('redis://'.$redisHost); + $this->assertInstanceOf(\Redis::class, $redis); + $this->assertTrue($redis->isConnected()); + $this->assertSame(0, $redis->getDbNum()); + + $redis = RedisAdapter::createConnection('redis://'.$redisHost.'/2'); + $this->assertSame(2, $redis->getDbNum()); + + $redis = RedisAdapter::createConnection('redis://'.$redisHost, array('timeout' => 3)); + $this->assertEquals(3, $redis->getTimeout()); + + $redis = RedisAdapter::createConnection('redis://'.$redisHost.'?timeout=4'); + $this->assertEquals(4, $redis->getTimeout()); + + $redis = RedisAdapter::createConnection('redis://'.$redisHost, array('read_timeout' => 5)); + $this->assertEquals(5, $redis->getReadTimeout()); + } + + /** + * @dataProvider provideFailedCreateConnection + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Redis connection failed + */ + public function testFailedCreateConnection($dsn) + { + RedisAdapter::createConnection($dsn); + } + + public function provideFailedCreateConnection() + { + return array( + array('redis://localhost:1234'), + array('redis://foo@localhost'), + array('redis://localhost/123'), + ); + } + + /** + * @dataProvider provideInvalidCreateConnection + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid Redis DSN + */ + public function testInvalidCreateConnection($dsn) + { + RedisAdapter::createConnection($dsn); + } + + public function provideInvalidCreateConnection() + { + return array( + array('foo://localhost'), + array('redis://'), + ); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php new file mode 100644 index 0000000000000..bef3eb88729f5 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +class RedisArrayAdapterTest extends AbstractRedisAdapterTest +{ + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + if (!class_exists('RedisArray')) { + self::markTestSkipped('The RedisArray class is required.'); + } + self::$redis = new \RedisArray(array(getenv('REDIS_HOST')), array('lazy_connect' => true)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php new file mode 100644 index 0000000000000..1e0297c69e993 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Simple\FilesystemCache; +use Symfony\Component\Cache\Adapter\SimpleCacheAdapter; + +/** + * @group time-sensitive + */ +class SimpleCacheAdapterTest extends AdapterTestCase +{ + public function createCachePool($defaultLifetime = 0) + { + return new SimpleCacheAdapter(new FilesystemCache(), '', $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php new file mode 100644 index 0000000000000..deca227c47dd0 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; + +/** + * @group time-sensitive + */ +class TagAwareAdapterTest extends AdapterTestCase +{ + public function createCachePool($defaultLifetime = 0) + { + return new TagAwareAdapter(new FilesystemAdapter('', $defaultLifetime)); + } + + public static function tearDownAfterClass() + { + FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + } + + /** + * @expectedException \Psr\Cache\InvalidArgumentException + */ + public function testInvalidTag() + { + $pool = $this->createCachePool(); + $item = $pool->getItem('foo'); + $item->tag(':'); + } + + public function testInvalidateTags() + { + $pool = $this->createCachePool(); + + $i0 = $pool->getItem('i0'); + $i1 = $pool->getItem('i1'); + $i2 = $pool->getItem('i2'); + $i3 = $pool->getItem('i3'); + $foo = $pool->getItem('foo'); + + $pool->save($i0->tag('bar')); + $pool->save($i1->tag('foo')); + $pool->save($i2->tag('foo')->tag('bar')); + $pool->save($i3->tag('foo')->tag('baz')); + $pool->save($foo); + + $pool->invalidateTags(array('bar')); + + $this->assertFalse($pool->getItem('i0')->isHit()); + $this->assertTrue($pool->getItem('i1')->isHit()); + $this->assertFalse($pool->getItem('i2')->isHit()); + $this->assertTrue($pool->getItem('i3')->isHit()); + $this->assertTrue($pool->getItem('foo')->isHit()); + + $pool->invalidateTags(array('foo')); + + $this->assertFalse($pool->getItem('i1')->isHit()); + $this->assertFalse($pool->getItem('i3')->isHit()); + $this->assertTrue($pool->getItem('foo')->isHit()); + } + + public function testTagsAreCleanedOnSave() + { + $pool = $this->createCachePool(); + + $i = $pool->getItem('k'); + $pool->save($i->tag('foo')); + + $i = $pool->getItem('k'); + $pool->save($i->tag('bar')); + + $pool->invalidateTags(array('foo')); + $this->assertTrue($pool->getItem('k')->isHit()); + } + + public function testTagsAreCleanedOnDelete() + { + $pool = $this->createCachePool(); + + $i = $pool->getItem('k'); + $pool->save($i->tag('foo')); + $pool->deleteItem('k'); + + $pool->save($pool->getItem('k')); + $pool->invalidateTags(array('foo')); + + $this->assertTrue($pool->getItem('k')->isHit()); + } + + public function testTagItemExpiry() + { + $pool = $this->createCachePool(10); + + $item = $pool->getItem('foo'); + $item->tag(array('baz')); + $item->expiresAfter(100); + + $pool->save($item); + $pool->invalidateTags(array('baz')); + $this->assertFalse($pool->getItem('foo')->isHit()); + + sleep(20); + + $this->assertFalse($pool->getItem('foo')->isHit()); + } + + public function testGetPreviousTags() + { + $pool = $this->createCachePool(); + + $i = $pool->getItem('k'); + $pool->save($i->tag('foo')); + + $i = $pool->getItem('k'); + $this->assertSame(array('foo' => 'foo'), $i->getPreviousTags()); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.php new file mode 100644 index 0000000000000..dec2f255555be --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TraceableAdapterTest.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\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\TraceableAdapter; + +/** + * @group time-sensitive + */ +class TraceableAdapterTest extends AdapterTestCase +{ + public function createCachePool($defaultLifetime = 0) + { + return new TraceableAdapter(new FilesystemAdapter('', $defaultLifetime)); + } + + public function testGetItemMissTrace() + { + $pool = $this->createCachePool(); + $pool->getItem('k'); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('getItem', $call->name); + $this->assertSame(array('k' => false), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(1, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testGetItemHitTrace() + { + $pool = $this->createCachePool(); + $item = $pool->getItem('k')->set('foo'); + $pool->save($item); + $pool->getItem('k'); + $calls = $pool->getCalls(); + $this->assertCount(3, $calls); + + $call = $calls[2]; + $this->assertSame(1, $call->hits); + $this->assertSame(0, $call->misses); + } + + public function testGetItemsMissTrace() + { + $pool = $this->createCachePool(); + $arg = array('k0', 'k1'); + $items = $pool->getItems($arg); + foreach ($items as $item) { + } + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('getItems', $call->name); + $this->assertSame(array('k0' => false, 'k1' => false), $call->result); + $this->assertSame(2, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testHasItemMissTrace() + { + $pool = $this->createCachePool(); + $pool->hasItem('k'); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('hasItem', $call->name); + $this->assertSame(array('k' => false), $call->result); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testHasItemHitTrace() + { + $pool = $this->createCachePool(); + $item = $pool->getItem('k')->set('foo'); + $pool->save($item); + $pool->hasItem('k'); + $calls = $pool->getCalls(); + $this->assertCount(3, $calls); + + $call = $calls[2]; + $this->assertSame('hasItem', $call->name); + $this->assertSame(array('k' => true), $call->result); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testDeleteItemTrace() + { + $pool = $this->createCachePool(); + $pool->deleteItem('k'); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('deleteItem', $call->name); + $this->assertSame(array('k' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testDeleteItemsTrace() + { + $pool = $this->createCachePool(); + $arg = array('k0', 'k1'); + $pool->deleteItems($arg); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('deleteItems', $call->name); + $this->assertSame(array('keys' => $arg, 'result' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testSaveTrace() + { + $pool = $this->createCachePool(); + $item = $pool->getItem('k')->set('foo'); + $pool->save($item); + $calls = $pool->getCalls(); + $this->assertCount(2, $calls); + + $call = $calls[1]; + $this->assertSame('save', $call->name); + $this->assertSame(array('k' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testSaveDeferredTrace() + { + $pool = $this->createCachePool(); + $item = $pool->getItem('k')->set('foo'); + $pool->saveDeferred($item); + $calls = $pool->getCalls(); + $this->assertCount(2, $calls); + + $call = $calls[1]; + $this->assertSame('saveDeferred', $call->name); + $this->assertSame(array('k' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testCommitTrace() + { + $pool = $this->createCachePool(); + $pool->commit(); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('commit', $call->name); + $this->assertTrue($call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TraceableTagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TraceableTagAwareAdapterTest.php new file mode 100644 index 0000000000000..9b50bfabe65b7 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TraceableTagAwareAdapterTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; +use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter; + +/** + * @group time-sensitive + */ +class TraceableTagAwareAdapterTest extends TraceableAdapterTest +{ + public function testInvalidateTags() + { + $pool = new TraceableTagAwareAdapter(new TagAwareAdapter(new FilesystemAdapter())); + $pool->invalidateTags(array('foo')); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('invalidateTags', $call->name); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } +} diff --git a/src/Symfony/Component/Cache/Tests/CacheItemTest.php b/src/Symfony/Component/Cache/Tests/CacheItemTest.php new file mode 100644 index 0000000000000..4e455c64a48cc --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/CacheItemTest.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\Component\Cache\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\CacheItem; + +class CacheItemTest extends TestCase +{ + public function testValidKey() + { + $this->assertNull(CacheItem::validateKey('foo')); + } + + /** + * @dataProvider provideInvalidKey + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Cache key + */ + public function testInvalidKey($key) + { + CacheItem::validateKey($key); + } + + public function provideInvalidKey() + { + return array( + array(''), + array('{'), + array('}'), + array('('), + array(')'), + array('/'), + array('\\'), + array('@'), + array(':'), + array(true), + array(null), + array(1), + array(1.1), + array(array(array())), + array(new \Exception('foo')), + ); + } + + public function testTag() + { + $item = new CacheItem(); + + $this->assertSame($item, $item->tag('foo')); + $this->assertSame($item, $item->tag(array('bar', 'baz'))); + + call_user_func(\Closure::bind(function () use ($item) { + $this->assertSame(array('foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'), $item->tags); + }, $this, CacheItem::class)); + } + + /** + * @dataProvider provideInvalidKey + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Cache tag + */ + public function testInvalidTag($tag) + { + $item = new CacheItem(); + $item->tag($tag); + } +} diff --git a/src/Symfony/Component/Cache/Tests/DoctrineProviderTest.php b/src/Symfony/Component/Cache/Tests/DoctrineProviderTest.php new file mode 100644 index 0000000000000..91a5516ab7719 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/DoctrineProviderTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests; + +use Doctrine\Common\Cache\CacheProvider; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\DoctrineProvider; + +class DoctrineProviderTest extends TestCase +{ + public function testProvider() + { + $pool = new ArrayAdapter(); + $cache = new DoctrineProvider($pool); + + $this->assertInstanceOf(CacheProvider::class, $cache); + + $key = '{}()/\@:'; + + $this->assertTrue($cache->delete($key)); + $this->assertFalse($cache->contains($key)); + + $this->assertTrue($cache->save($key, 'bar')); + $this->assertTrue($cache->contains($key)); + $this->assertSame('bar', $cache->fetch($key)); + + $this->assertTrue($cache->delete($key)); + $this->assertFalse($cache->fetch($key)); + $this->assertTrue($cache->save($key, 'bar')); + + $cache->flushAll(); + $this->assertFalse($cache->fetch($key)); + $this->assertFalse($cache->contains($key)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Fixtures/ExternalAdapter.php b/src/Symfony/Component/Cache/Tests/Fixtures/ExternalAdapter.php new file mode 100644 index 0000000000000..493906ea0cccc --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Fixtures/ExternalAdapter.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Fixtures; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +/** + * Adapter not implementing the {@see \Symfony\Component\Cache\Adapter\AdapterInterface}. + * + * @author Kévin Dunglas + */ +class ExternalAdapter implements CacheItemPoolInterface +{ + private $cache; + + public function __construct() + { + $this->cache = new ArrayAdapter(); + } + + public function getItem($key) + { + return $this->cache->getItem($key); + } + + public function getItems(array $keys = array()) + { + return $this->cache->getItems($keys); + } + + public function hasItem($key) + { + return $this->cache->hasItem($key); + } + + public function clear() + { + return $this->cache->clear(); + } + + public function deleteItem($key) + { + return $this->cache->deleteItem($key); + } + + public function deleteItems(array $keys) + { + return $this->cache->deleteItems($keys); + } + + public function save(CacheItemInterface $item) + { + return $this->cache->save($item); + } + + public function saveDeferred(CacheItemInterface $item) + { + return $this->cache->saveDeferred($item); + } + + public function commit() + { + return $this->cache->commit(); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/AbstractRedisCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/AbstractRedisCacheTest.php new file mode 100644 index 0000000000000..1d097fff85fcd --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/AbstractRedisCacheTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\RedisCache; + +abstract class AbstractRedisCacheTest extends CacheTestCase +{ + protected $skippedTests = array( + 'testSetTtl' => 'Testing expiration slows down the test suite', + 'testSetMultipleTtl' => 'Testing expiration slows down the test suite', + 'testDefaultLifeTime' => 'Testing expiration slows down the test suite', + ); + + protected static $redis; + + public function createSimpleCache($defaultLifetime = 0) + { + return new RedisCache(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); + } + + public static function setupBeforeClass() + { + if (!extension_loaded('redis')) { + self::markTestSkipped('Extension redis required.'); + } + if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) { + $e = error_get_last(); + self::markTestSkipped($e['message']); + } + } + + public static function tearDownAfterClass() + { + self::$redis->flushDB(); + self::$redis = null; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/ApcuCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/ApcuCacheTest.php new file mode 100644 index 0000000000000..297a41756f427 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/ApcuCacheTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\ApcuCache; + +class ApcuCacheTest extends CacheTestCase +{ + protected $skippedTests = array( + 'testSetTtl' => 'Testing expiration slows down the test suite', + 'testSetMultipleTtl' => 'Testing expiration slows down the test suite', + 'testDefaultLifeTime' => 'Testing expiration slows down the test suite', + ); + + public function createSimpleCache($defaultLifetime = 0) + { + if (!function_exists('apcu_fetch') || !ini_get('apc.enabled') || ('cli' === PHP_SAPI && !ini_get('apc.enable_cli'))) { + $this->markTestSkipped('APCu extension is required.'); + } + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Fails transiently on Windows.'); + } + + return new ApcuCache(str_replace('\\', '.', __CLASS__), $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/ArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/ArrayCacheTest.php new file mode 100644 index 0000000000000..26c3e14d0965c --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/ArrayCacheTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\ArrayCache; + +/** + * @group time-sensitive + */ +class ArrayCacheTest extends CacheTestCase +{ + public function createSimpleCache($defaultLifetime = 0) + { + return new ArrayCache($defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php b/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php new file mode 100644 index 0000000000000..fcde497eca98c --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.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\Component\Cache\Tests\Simple; + +use Cache\IntegrationTests\SimpleCacheTest; +use Symfony\Component\Cache\PruneableInterface; + +abstract class CacheTestCase extends SimpleCacheTest +{ + protected function setUp() + { + parent::setUp(); + + if (!array_key_exists('testPrune', $this->skippedTests) && !$this->createSimpleCache() instanceof PruneableInterface) { + $this->skippedTests['testPrune'] = 'Not a pruneable cache pool.'; + } + } + + public static function validKeys() + { + return array_merge(parent::validKeys(), array(array("a\0b"))); + } + + public function testDefaultLifeTime() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = $this->createSimpleCache(2); + + $cache->set('key.dlt', 'value'); + sleep(1); + + $this->assertSame('value', $cache->get('key.dlt')); + + sleep(2); + $this->assertNull($cache->get('key.dlt')); + } + + public function testNotUnserializable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = $this->createSimpleCache(); + + $cache->set('foo', new NotUnserializable()); + + $this->assertNull($cache->get('foo')); + + $cache->setMultiple(array('foo' => new NotUnserializable())); + + foreach ($cache->getMultiple(array('foo')) as $value) { + } + $this->assertNull($value); + } + + public function testPrune() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + if (!method_exists($this, 'isPruned')) { + $this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.'); + } + + $cache = $this->createSimpleCache(); + + $cache->set('foo', 'foo-val'); + $cache->set('bar', 'bar-val', new \DateInterval('PT20S')); + $cache->set('baz', 'baz-val', new \DateInterval('PT40S')); + $cache->set('qux', 'qux-val', new \DateInterval('PT80S')); + + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertFalse($this->isPruned($cache, 'bar')); + $this->assertFalse($this->isPruned($cache, 'baz')); + $this->assertFalse($this->isPruned($cache, 'qux')); + + sleep(30); + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'bar')); + $this->assertFalse($this->isPruned($cache, 'baz')); + $this->assertFalse($this->isPruned($cache, 'qux')); + + sleep(30); + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'baz')); + $this->assertFalse($this->isPruned($cache, 'qux')); + + sleep(30); + $cache->prune(); + $this->assertFalse($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'qux')); + } +} + +class NotUnserializable implements \Serializable +{ + public function serialize() + { + return serialize(123); + } + + public function unserialize($ser) + { + throw new \Exception(__CLASS__); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php new file mode 100644 index 0000000000000..282bb62a6530e --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\ArrayCache; +use Symfony\Component\Cache\Simple\ChainCache; +use Symfony\Component\Cache\Simple\FilesystemCache; + +/** + * @group time-sensitive + */ +class ChainCacheTest extends CacheTestCase +{ + public function createSimpleCache($defaultLifetime = 0) + { + return new ChainCache(array(new ArrayCache($defaultLifetime), new FilesystemCache('', $defaultLifetime)), $defaultLifetime); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage At least one cache must be specified. + */ + public function testEmptyCachesException() + { + new ChainCache(array()); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage The class "stdClass" does not implement + */ + public function testInvalidCacheException() + { + new Chaincache(array(new \stdClass())); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/DoctrineCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/DoctrineCacheTest.php new file mode 100644 index 0000000000000..0a185297ab453 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/DoctrineCacheTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Cache\Simple\DoctrineCache; + +/** + * @group time-sensitive + */ +class DoctrineCacheTest extends CacheTestCase +{ + protected $skippedTests = array( + 'testObjectDoesNotChangeInCache' => 'ArrayCache does not use serialize/unserialize', + 'testNotUnserializable' => 'ArrayCache does not use serialize/unserialize', + ); + + public function createSimpleCache($defaultLifetime = 0) + { + return new DoctrineCache(new ArrayCache($defaultLifetime), '', $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/FilesystemCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/FilesystemCacheTest.php new file mode 100644 index 0000000000000..620305a58a44e --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/FilesystemCacheTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\Simple\FilesystemCache; + +/** + * @group time-sensitive + */ +class FilesystemCacheTest extends CacheTestCase +{ + public function createSimpleCache($defaultLifetime = 0) + { + return new FilesystemCache('', $defaultLifetime); + } + + protected function isPruned(CacheInterface $cache, $name) + { + $getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile'); + $getFileMethod->setAccessible(true); + + return !file_exists($getFileMethod->invoke($cache, $name)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php new file mode 100644 index 0000000000000..c4af891af7ba7 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Simple\MemcachedCache; + +class MemcachedCacheTest extends CacheTestCase +{ + protected $skippedTests = array( + 'testSetTtl' => 'Testing expiration slows down the test suite', + 'testSetMultipleTtl' => 'Testing expiration slows down the test suite', + 'testDefaultLifeTime' => 'Testing expiration slows down the test suite', + ); + + protected static $client; + + public static function setupBeforeClass() + { + if (!MemcachedCache::isSupported()) { + self::markTestSkipped('Extension memcached >=2.2.0 required.'); + } + self::$client = AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST')); + self::$client->get('foo'); + $code = self::$client->getResultCode(); + + if (\Memcached::RES_SUCCESS !== $code && \Memcached::RES_NOTFOUND !== $code) { + self::markTestSkipped('Memcached error: '.strtolower(self::$client->getResultMessage())); + } + } + + public function createSimpleCache($defaultLifetime = 0) + { + $client = $defaultLifetime ? AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'), array('binary_protocol' => false)) : self::$client; + + return new MemcachedCache($client, str_replace('\\', '.', __CLASS__), $defaultLifetime); + } + + public function testOptions() + { + $client = MemcachedCache::createConnection(array(), array( + 'libketama_compatible' => false, + 'distribution' => 'modula', + 'compression' => true, + 'serializer' => 'php', + 'hash' => 'md5', + )); + + $this->assertSame(\Memcached::SERIALIZER_PHP, $client->getOption(\Memcached::OPT_SERIALIZER)); + $this->assertSame(\Memcached::HASH_MD5, $client->getOption(\Memcached::OPT_HASH)); + $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION)); + $this->assertSame(0, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE)); + $this->assertSame(\Memcached::DISTRIBUTION_MODULA, $client->getOption(\Memcached::OPT_DISTRIBUTION)); + } + + /** + * @dataProvider provideBadOptions + * @expectedException \ErrorException + * @expectedExceptionMessage constant(): Couldn't find constant Memcached:: + */ + public function testBadOptions($name, $value) + { + MemcachedCache::createConnection(array(), array($name => $value)); + } + + public function provideBadOptions() + { + return array( + array('foo', 'bar'), + array('hash', 'zyx'), + array('serializer', 'zyx'), + array('distribution', 'zyx'), + ); + } + + public function testDefaultOptions() + { + $this->assertTrue(MemcachedCache::isSupported()); + + $client = MemcachedCache::createConnection(array()); + + $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION)); + $this->assertSame(1, $client->getOption(\Memcached::OPT_BINARY_PROTOCOL)); + $this->assertSame(1, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE)); + } + + /** + * @expectedException \Symfony\Component\Cache\Exception\CacheException + * @expectedExceptionMessage MemcachedAdapter: "serializer" option must be "php" or "igbinary". + */ + public function testOptionSerializer() + { + if (!\Memcached::HAVE_JSON) { + $this->markTestSkipped('Memcached::HAVE_JSON required'); + } + + new MemcachedCache(MemcachedCache::createConnection(array(), array('serializer' => 'json'))); + } + + /** + * @dataProvider provideServersSetting + */ + public function testServersSetting($dsn, $host, $port) + { + $client1 = MemcachedCache::createConnection($dsn); + $client2 = MemcachedCache::createConnection(array($dsn)); + $client3 = MemcachedCache::createConnection(array(array($host, $port))); + $expect = array( + 'host' => $host, + 'port' => $port, + ); + + $f = function ($s) { return array('host' => $s['host'], 'port' => $s['port']); }; + $this->assertSame(array($expect), array_map($f, $client1->getServerList())); + $this->assertSame(array($expect), array_map($f, $client2->getServerList())); + $this->assertSame(array($expect), array_map($f, $client3->getServerList())); + } + + public function provideServersSetting() + { + yield array( + 'memcached://127.0.0.1/50', + '127.0.0.1', + 11211, + ); + yield array( + 'memcached://localhost:11222?weight=25', + 'localhost', + 11222, + ); + if (ini_get('memcached.use_sasl')) { + yield array( + 'memcached://user:password@127.0.0.1?weight=50', + '127.0.0.1', + 11211, + ); + } + yield array( + 'memcached:///var/run/memcached.sock?weight=25', + '/var/run/memcached.sock', + 0, + ); + yield array( + 'memcached:///var/local/run/memcached.socket?weight=25', + '/var/local/run/memcached.socket', + 0, + ); + if (ini_get('memcached.use_sasl')) { + yield array( + 'memcached://user:password@/var/local/run/memcached.socket?weight=25', + '/var/local/run/memcached.socket', + 0, + ); + } + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/NullCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/NullCacheTest.php new file mode 100644 index 0000000000000..16dd7764d2ca1 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/NullCacheTest.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Simple\NullCache; + +/** + * @group time-sensitive + */ +class NullCacheTest extends TestCase +{ + public function createCachePool() + { + return new NullCache(); + } + + public function testGetItem() + { + $cache = $this->createCachePool(); + + $this->assertNull($cache->get('key')); + } + + public function testHas() + { + $this->assertFalse($this->createCachePool()->has('key')); + } + + public function testGetMultiple() + { + $cache = $this->createCachePool(); + + $keys = array('foo', 'bar', 'baz', 'biz'); + + $default = new \stdClass(); + $items = $cache->getMultiple($keys, $default); + $count = 0; + + foreach ($items as $key => $item) { + $this->assertTrue(in_array($key, $keys), 'Cache key can not change.'); + $this->assertSame($default, $item); + + // Remove $key for $keys + foreach ($keys as $k => $v) { + if ($v === $key) { + unset($keys[$k]); + } + } + + ++$count; + } + + $this->assertSame(4, $count); + } + + public function testClear() + { + $this->assertTrue($this->createCachePool()->clear()); + } + + public function testDelete() + { + $this->assertTrue($this->createCachePool()->delete('key')); + } + + public function testDeleteMultiple() + { + $this->assertTrue($this->createCachePool()->deleteMultiple(array('key', 'foo', 'bar'))); + } + + public function testSet() + { + $cache = $this->createCachePool(); + + $this->assertFalse($cache->set('key', 'val')); + $this->assertNull($cache->get('key')); + } + + public function testSetMultiple() + { + $cache = $this->createCachePool(); + + $this->assertFalse($cache->setMultiple(array('key' => 'val'))); + $this->assertNull($cache->get('key')); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php new file mode 100644 index 0000000000000..47c0ee52d99ac --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\PdoCache; + +/** + * @group time-sensitive + */ +class PdoCacheTest extends CacheTestCase +{ + protected static $dbFile; + + public static function setupBeforeClass() + { + if (!extension_loaded('pdo_sqlite')) { + self::markTestSkipped('Extension pdo_sqlite required.'); + } + + self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); + + $pool = new PdoCache('sqlite:'.self::$dbFile); + $pool->createTable(); + } + + public static function tearDownAfterClass() + { + @unlink(self::$dbFile); + } + + public function createSimpleCache($defaultLifetime = 0) + { + return new PdoCache('sqlite:'.self::$dbFile, 'ns', $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php new file mode 100644 index 0000000000000..51a10af30663b --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Doctrine\DBAL\DriverManager; +use Symfony\Component\Cache\Simple\PdoCache; + +/** + * @group time-sensitive + */ +class PdoDbalCacheTest extends CacheTestCase +{ + protected static $dbFile; + + public static function setupBeforeClass() + { + if (!extension_loaded('pdo_sqlite')) { + self::markTestSkipped('Extension pdo_sqlite required.'); + } + + self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); + + $pool = new PdoCache(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile))); + $pool->createTable(); + } + + public static function tearDownAfterClass() + { + @unlink(self::$dbFile); + } + + public function createSimpleCache($defaultLifetime = 0) + { + return new PdoCache(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile)), '', $defaultLifetime); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php new file mode 100644 index 0000000000000..57361905f8869 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheTest.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Tests\Adapter\FilesystemAdapterTest; +use Symfony\Component\Cache\Simple\NullCache; +use Symfony\Component\Cache\Simple\PhpArrayCache; + +/** + * @group time-sensitive + */ +class PhpArrayCacheTest extends CacheTestCase +{ + protected $skippedTests = array( + 'testBasicUsageWithLongKey' => 'PhpArrayCache does no writes', + + 'testDelete' => 'PhpArrayCache does no writes', + 'testDeleteMultiple' => 'PhpArrayCache does no writes', + 'testDeleteMultipleGenerator' => 'PhpArrayCache does no writes', + + 'testSetTtl' => 'PhpArrayCache does no expiration', + 'testSetMultipleTtl' => 'PhpArrayCache does no expiration', + 'testSetExpiredTtl' => 'PhpArrayCache does no expiration', + 'testSetMultipleExpiredTtl' => 'PhpArrayCache does no expiration', + + 'testGetInvalidKeys' => 'PhpArrayCache does no validation', + 'testGetMultipleInvalidKeys' => 'PhpArrayCache does no validation', + 'testSetInvalidKeys' => 'PhpArrayCache does no validation', + 'testDeleteInvalidKeys' => 'PhpArrayCache does no validation', + 'testDeleteMultipleInvalidKeys' => 'PhpArrayCache does no validation', + 'testSetInvalidTtl' => 'PhpArrayCache does no validation', + 'testSetMultipleInvalidKeys' => 'PhpArrayCache does no validation', + 'testSetMultipleInvalidTtl' => 'PhpArrayCache does no validation', + 'testHasInvalidKeys' => 'PhpArrayCache does no validation', + 'testSetValidData' => 'PhpArrayCache does no validation', + + 'testDefaultLifeTime' => 'PhpArrayCache does not allow configuring a default lifetime.', + ); + + protected static $file; + + public static function setupBeforeClass() + { + self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php'; + } + + protected function tearDown() + { + if (file_exists(sys_get_temp_dir().'/symfony-cache')) { + FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + } + } + + public function createSimpleCache() + { + return new PhpArrayCacheWrapper(self::$file, new NullCache()); + } + + public function testStore() + { + $arrayWithRefs = array(); + $arrayWithRefs[0] = 123; + $arrayWithRefs[1] = &$arrayWithRefs[0]; + + $object = (object) array( + 'foo' => 'bar', + 'foo2' => 'bar2', + ); + + $expected = array( + 'null' => null, + 'serializedString' => serialize($object), + 'arrayWithRefs' => $arrayWithRefs, + 'object' => $object, + 'arrayWithObject' => array('bar' => $object), + ); + + $cache = new PhpArrayCache(self::$file, new NullCache()); + $cache->warmUp($expected); + + foreach ($expected as $key => $value) { + $this->assertSame(serialize($value), serialize($cache->get($key)), 'Warm up should create a PHP file that OPCache can load in memory'); + } + } + + public function testStoredFile() + { + $expected = array( + 'integer' => 42, + 'float' => 42.42, + 'boolean' => true, + 'array_simple' => array('foo', 'bar'), + 'array_associative' => array('foo' => 'bar', 'foo2' => 'bar2'), + ); + + $cache = new PhpArrayCache(self::$file, new NullCache()); + $cache->warmUp($expected); + + $values = eval(substr(file_get_contents(self::$file), 6)); + + $this->assertSame($expected, $values, 'Warm up should create a PHP file that OPCache can load in memory'); + } +} + +class PhpArrayCacheWrapper extends PhpArrayCache +{ + public function set($key, $value, $ttl = null) + { + call_user_func(\Closure::bind(function () use ($key, $value) { + $this->values[$key] = $value; + $this->warmUp($this->values); + $this->values = eval(substr(file_get_contents($this->file), 6)); + }, $this, PhpArrayCache::class)); + + return true; + } + + public function setMultiple($values, $ttl = null) + { + if (!is_array($values) && !$values instanceof \Traversable) { + return parent::setMultiple($values, $ttl); + } + call_user_func(\Closure::bind(function () use ($values) { + foreach ($values as $key => $value) { + $this->values[$key] = $value; + } + $this->warmUp($this->values); + $this->values = eval(substr(file_get_contents($this->file), 6)); + }, $this, PhpArrayCache::class)); + + return true; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php new file mode 100644 index 0000000000000..a624fa73e783a --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/PhpArrayCacheWithFallbackTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\FilesystemCache; +use Symfony\Component\Cache\Simple\PhpArrayCache; +use Symfony\Component\Cache\Tests\Adapter\FilesystemAdapterTest; + +/** + * @group time-sensitive + */ +class PhpArrayCacheWithFallbackTest extends CacheTestCase +{ + protected $skippedTests = array( + 'testGetInvalidKeys' => 'PhpArrayCache does no validation', + 'testGetMultipleInvalidKeys' => 'PhpArrayCache does no validation', + 'testDeleteInvalidKeys' => 'PhpArrayCache does no validation', + 'testDeleteMultipleInvalidKeys' => 'PhpArrayCache does no validation', + //'testSetValidData' => 'PhpArrayCache does no validation', + 'testSetInvalidKeys' => 'PhpArrayCache does no validation', + 'testSetInvalidTtl' => 'PhpArrayCache does no validation', + 'testSetMultipleInvalidKeys' => 'PhpArrayCache does no validation', + 'testSetMultipleInvalidTtl' => 'PhpArrayCache does no validation', + 'testHasInvalidKeys' => 'PhpArrayCache does no validation', + ); + + protected static $file; + + public static function setupBeforeClass() + { + self::$file = sys_get_temp_dir().'/symfony-cache/php-array-adapter-test.php'; + } + + protected function tearDown() + { + if (file_exists(sys_get_temp_dir().'/symfony-cache')) { + FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + } + } + + public function createSimpleCache($defaultLifetime = 0) + { + return new PhpArrayCache(self::$file, new FilesystemCache('php-array-fallback', $defaultLifetime)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php new file mode 100644 index 0000000000000..7a402682ae247 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\Simple\PhpFilesCache; + +/** + * @group time-sensitive + */ +class PhpFilesCacheTest extends CacheTestCase +{ + protected $skippedTests = array( + 'testDefaultLifeTime' => 'PhpFilesCache does not allow configuring a default lifetime.', + ); + + public function createSimpleCache() + { + if (!PhpFilesCache::isSupported()) { + $this->markTestSkipped('OPcache extension is not enabled.'); + } + + return new PhpFilesCache('sf-cache'); + } + + protected function isPruned(CacheInterface $cache, $name) + { + $getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile'); + $getFileMethod->setAccessible(true); + + return !file_exists($getFileMethod->invoke($cache, $name)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php new file mode 100644 index 0000000000000..16e21d0c0b63b --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/Psr6CacheTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Simple\Psr6Cache; + +/** + * @group time-sensitive + */ +class Psr6CacheTest extends CacheTestCase +{ + public function createSimpleCache($defaultLifetime = 0) + { + return new Psr6Cache(new FilesystemAdapter('', $defaultLifetime)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/RedisArrayCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/RedisArrayCacheTest.php new file mode 100644 index 0000000000000..3c903c8a9b4a6 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/RedisArrayCacheTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +class RedisArrayCacheTest extends AbstractRedisCacheTest +{ + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + if (!class_exists('RedisArray')) { + self::markTestSkipped('The RedisArray class is required.'); + } + self::$redis = new \RedisArray(array(getenv('REDIS_HOST')), array('lazy_connect' => true)); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php new file mode 100644 index 0000000000000..d33421f9aae46 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\RedisCache; + +class RedisCacheTest extends AbstractRedisCacheTest +{ + public static function setupBeforeClass() + { + parent::setupBeforeClass(); + self::$redis = RedisCache::createConnection('redis://'.getenv('REDIS_HOST')); + } + + public function testCreateConnection() + { + $redisHost = getenv('REDIS_HOST'); + + $redis = RedisCache::createConnection('redis://'.$redisHost); + $this->assertInstanceOf(\Redis::class, $redis); + $this->assertTrue($redis->isConnected()); + $this->assertSame(0, $redis->getDbNum()); + + $redis = RedisCache::createConnection('redis://'.$redisHost.'/2'); + $this->assertSame(2, $redis->getDbNum()); + + $redis = RedisCache::createConnection('redis://'.$redisHost, array('timeout' => 3)); + $this->assertEquals(3, $redis->getTimeout()); + + $redis = RedisCache::createConnection('redis://'.$redisHost.'?timeout=4'); + $this->assertEquals(4, $redis->getTimeout()); + + $redis = RedisCache::createConnection('redis://'.$redisHost, array('read_timeout' => 5)); + $this->assertEquals(5, $redis->getReadTimeout()); + } + + /** + * @dataProvider provideFailedCreateConnection + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Redis connection failed + */ + public function testFailedCreateConnection($dsn) + { + RedisCache::createConnection($dsn); + } + + public function provideFailedCreateConnection() + { + return array( + array('redis://localhost:1234'), + array('redis://foo@localhost'), + array('redis://localhost/123'), + ); + } + + /** + * @dataProvider provideInvalidCreateConnection + * @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid Redis DSN + */ + public function testInvalidCreateConnection($dsn) + { + RedisCache::createConnection($dsn); + } + + public function provideInvalidCreateConnection() + { + return array( + array('foo://localhost'), + array('redis://'), + ); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php new file mode 100644 index 0000000000000..7feccba1af905 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Simple/TraceableCacheTest.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Simple; + +use Symfony\Component\Cache\Simple\FilesystemCache; +use Symfony\Component\Cache\Simple\TraceableCache; + +/** + * @group time-sensitive + */ +class TraceableCacheTest extends CacheTestCase +{ + public function createSimpleCache($defaultLifetime = 0) + { + return new TraceableCache(new FilesystemCache('', $defaultLifetime)); + } + + public function testGetMissTrace() + { + $pool = $this->createSimpleCache(); + $pool->get('k'); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('get', $call->name); + $this->assertSame(array('k' => false), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(1, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testGetHitTrace() + { + $pool = $this->createSimpleCache(); + $pool->set('k', 'foo'); + $pool->get('k'); + $calls = $pool->getCalls(); + $this->assertCount(2, $calls); + + $call = $calls[1]; + $this->assertSame(1, $call->hits); + $this->assertSame(0, $call->misses); + } + + public function testGetMultipleMissTrace() + { + $pool = $this->createSimpleCache(); + $pool->set('k1', 123); + $values = $pool->getMultiple(array('k0', 'k1')); + foreach ($values as $value) { + } + $calls = $pool->getCalls(); + $this->assertCount(2, $calls); + + $call = $calls[1]; + $this->assertSame('getMultiple', $call->name); + $this->assertSame(array('k1' => true, 'k0' => false), $call->result); + $this->assertSame(1, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testHasMissTrace() + { + $pool = $this->createSimpleCache(); + $pool->has('k'); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('has', $call->name); + $this->assertSame(array('k' => false), $call->result); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testHasHitTrace() + { + $pool = $this->createSimpleCache(); + $pool->set('k', 'foo'); + $pool->has('k'); + $calls = $pool->getCalls(); + $this->assertCount(2, $calls); + + $call = $calls[1]; + $this->assertSame('has', $call->name); + $this->assertSame(array('k' => true), $call->result); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testDeleteTrace() + { + $pool = $this->createSimpleCache(); + $pool->delete('k'); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('delete', $call->name); + $this->assertSame(array('k' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testDeleteMultipleTrace() + { + $pool = $this->createSimpleCache(); + $arg = array('k0', 'k1'); + $pool->deleteMultiple($arg); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('deleteMultiple', $call->name); + $this->assertSame(array('keys' => $arg, 'result' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testTraceSetTrace() + { + $pool = $this->createSimpleCache(); + $pool->set('k', 'foo'); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('set', $call->name); + $this->assertSame(array('k' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } + + public function testSetMultipleTrace() + { + $pool = $this->createSimpleCache(); + $pool->setMultiple(array('k' => 'foo')); + $calls = $pool->getCalls(); + $this->assertCount(1, $calls); + + $call = $calls[0]; + $this->assertSame('setMultiple', $call->name); + $this->assertSame(array('keys' => array('k'), 'result' => true), $call->result); + $this->assertSame(0, $call->hits); + $this->assertSame(0, $call->misses); + $this->assertNotEmpty($call->start); + $this->assertNotEmpty($call->end); + } +} diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php new file mode 100644 index 0000000000000..375ccf7620d83 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php @@ -0,0 +1,209 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait AbstractTrait +{ + use LoggerAwareTrait; + + private $namespace; + private $deferred = array(); + + /** + * @var int|null The maximum length to enforce for identifiers or null when no limit applies + */ + protected $maxIdLength; + + /** + * Fetches several cache items. + * + * @param array $ids The cache identifiers to fetch + * + * @return array|\Traversable The corresponding values found in the cache + */ + abstract protected function doFetch(array $ids); + + /** + * Confirms if the cache contains specified cache item. + * + * @param string $id The identifier for which to check existence + * + * @return bool True if item exists in the cache, false otherwise + */ + abstract protected function doHave($id); + + /** + * Deletes all items in the pool. + * + * @param string The prefix used for all identifiers managed by this pool + * + * @return bool True if the pool was successfully cleared, false otherwise + */ + abstract protected function doClear($namespace); + + /** + * Removes multiple items from the pool. + * + * @param array $ids An array of identifiers that should be removed from the pool + * + * @return bool True if the items were successfully removed, false otherwise + */ + abstract protected function doDelete(array $ids); + + /** + * Persists several cache items immediately. + * + * @param array $values The values to cache, indexed by their cache identifier + * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning + * + * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not + */ + abstract protected function doSave(array $values, $lifetime); + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + $id = $this->getId($key); + + if (isset($this->deferred[$key])) { + $this->commit(); + } + + try { + return $this->doHave($id); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached', array('key' => $key, 'exception' => $e)); + + return false; + } + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->deferred = array(); + + try { + return $this->doClear($this->namespace); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to clear the cache', array('exception' => $e)); + + return false; + } + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + return $this->deleteItems(array($key)); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + $ids = array(); + + foreach ($keys as $key) { + $ids[$key] = $this->getId($key); + unset($this->deferred[$key]); + } + + try { + if ($this->doDelete($ids)) { + return true; + } + } catch (\Exception $e) { + } + + $ok = true; + + // When bulk-delete failed, retry each item individually + foreach ($ids as $key => $id) { + try { + $e = null; + if ($this->doDelete(array($id))) { + continue; + } + } catch (\Exception $e) { + } + CacheItem::log($this->logger, 'Failed to delete key "{key}"', array('key' => $key, 'exception' => $e)); + $ok = false; + } + + return $ok; + } + + /** + * Like the native unserialize() function but throws an exception if anything goes wrong. + * + * @param string $value + * + * @return mixed + * + * @throws \Exception + */ + protected static function unserialize($value) + { + if ('b:0;' === $value) { + return false; + } + $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + try { + if (false !== $value = unserialize($value)) { + return $value; + } + throw new \DomainException('Failed to unserialize cached value'); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + private function getId($key) + { + CacheItem::validateKey($key); + + if (null === $this->maxIdLength) { + return $this->namespace.$key; + } + if (strlen($id = $this->namespace.$key) > $this->maxIdLength) { + $id = $this->namespace.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22); + } + + return $id; + } + + /** + * @internal + */ + public static function handleUnserializeCallback($class) + { + throw new \DomainException('Class not found: '.$class); + } +} diff --git a/src/Symfony/Component/Cache/Traits/ApcuTrait.php b/src/Symfony/Component/Cache/Traits/ApcuTrait.php new file mode 100644 index 0000000000000..5614b390cf2f4 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/ApcuTrait.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\Component\Cache\Traits; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\CacheException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait ApcuTrait +{ + public static function isSupported() + { + return function_exists('apcu_fetch') && ini_get('apc.enabled'); + } + + private function init($namespace, $defaultLifetime, $version) + { + if (!static::isSupported()) { + throw new CacheException('APCu is not enabled'); + } + if ('cli' === PHP_SAPI) { + ini_set('apc.use_request_time', 0); + } + parent::__construct($namespace, $defaultLifetime); + + if (null !== $version) { + CacheItem::validateKey($version); + + if (!apcu_exists($version.'@'.$namespace)) { + $this->doClear($namespace); + apcu_add($version.'@'.$namespace, null); + } + } + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + try { + return apcu_fetch($ids) ?: array(); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + return apcu_exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + return isset($namespace[0]) && class_exists('APCuIterator', false) && ('cli' !== PHP_SAPI || ini_get('apc.enable_cli')) + ? apcu_delete(new \APCuIterator(sprintf('/^%s/', preg_quote($namespace, '/')), APC_ITER_KEY)) + : apcu_clear_cache(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + foreach ($ids as $id) { + apcu_delete($id); + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + try { + if (false === $failures = apcu_store($values, null, $lifetime)) { + $failures = $values; + } + + return array_keys($failures); + } catch (\Error $e) { + } catch (\Exception $e) { + } + + if (1 === count($values)) { + // Workaround https://github.com/krakjoe/apcu/issues/170 + apcu_delete(key($values)); + } + + throw $e; + } +} diff --git a/src/Symfony/Component/Cache/Traits/ArrayTrait.php b/src/Symfony/Component/Cache/Traits/ArrayTrait.php new file mode 100644 index 0000000000000..3fb5fa36bef2c --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/ArrayTrait.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait ArrayTrait +{ + use LoggerAwareTrait; + + private $storeSerialized; + private $values = array(); + private $expiries = array(); + + /** + * Returns all cached values, with cache miss as null. + * + * @return array + */ + public function getValues() + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + CacheItem::validateKey($key); + + return isset($this->expiries[$key]) && ($this->expiries[$key] >= time() || !$this->deleteItem($key)); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->values = $this->expiries = array(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + CacheItem::validateKey($key); + + unset($this->values[$key], $this->expiries[$key]); + + return true; + } + + private function generateItems(array $keys, $now, $f) + { + foreach ($keys as $i => $key) { + try { + if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) { + $this->values[$key] = $value = null; + } elseif (!$this->storeSerialized) { + $value = $this->values[$key]; + } elseif ('b:0;' === $value = $this->values[$key]) { + $value = false; + } elseif (false === $value = unserialize($value)) { + $this->values[$key] = $value = null; + $isHit = false; + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e)); + $this->values[$key] = $value = null; + $isHit = false; + } + unset($keys[$i]); + + yield $key => $f($key, $value, $isHit); + } + + foreach ($keys as $key) { + yield $key => $f($key, null, false); + } + } +} diff --git a/src/Symfony/Component/Cache/Traits/DoctrineTrait.php b/src/Symfony/Component/Cache/Traits/DoctrineTrait.php new file mode 100644 index 0000000000000..be351cf53a552 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/DoctrineTrait.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait DoctrineTrait +{ + private $provider; + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback'); + try { + return $this->provider->fetchMultiple($ids); + } catch (\Error $e) { + $trace = $e->getTrace(); + + if (isset($trace[0]['function']) && !isset($trace[0]['class'])) { + switch ($trace[0]['function']) { + case 'unserialize': + case 'apcu_fetch': + case 'apc_fetch': + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } + } + + throw $e; + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + return $this->provider->contains($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + $namespace = $this->provider->getNamespace(); + + return isset($namespace[0]) + ? $this->provider->deleteAll() + : $this->provider->flushAll(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + foreach ($ids as $id) { + $ok = $this->provider->delete($id) && $ok; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + return $this->provider->saveMultiple($values, $lifetime); + } +} diff --git a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php new file mode 100644 index 0000000000000..b0f495e4d4c51 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait FilesystemCommonTrait +{ + private $directory; + private $tmp; + + private function init($namespace, $directory) + { + if (!isset($directory[0])) { + $directory = sys_get_temp_dir().'/symfony-cache'; + } else { + $directory = realpath($directory) ?: $directory; + } + if (isset($namespace[0])) { + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + $directory .= DIRECTORY_SEPARATOR.$namespace; + } + if (!file_exists($directory)) { + @mkdir($directory, 0777, true); + } + $directory .= DIRECTORY_SEPARATOR; + // On Windows the whole path is limited to 258 chars + if ('\\' === DIRECTORY_SEPARATOR && strlen($directory) > 234) { + throw new InvalidArgumentException(sprintf('Cache directory too long (%s)', $directory)); + } + + $this->directory = $directory; + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + $ok = true; + + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS)) as $file) { + $ok = ($file->isDir() || @unlink($file) || !file_exists($file)) && $ok; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + + foreach ($ids as $id) { + $file = $this->getFile($id); + $ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok; + } + + return $ok; + } + + private function write($file, $data, $expiresAt = null) + { + set_error_handler(__CLASS__.'::throwError'); + try { + if (null === $this->tmp) { + $this->tmp = $this->directory.uniqid('', true); + } + file_put_contents($this->tmp, $data); + + if (null !== $expiresAt) { + touch($this->tmp, $expiresAt); + } + + return rename($this->tmp, $file); + } finally { + restore_error_handler(); + } + } + + private function getFile($id, $mkdir = false) + { + $hash = str_replace('/', '-', base64_encode(hash('sha256', static::class.$id, true))); + $dir = $this->directory.strtoupper($hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR); + + if ($mkdir && !file_exists($dir)) { + @mkdir($dir, 0777, true); + } + + return $dir.substr($hash, 2, 20); + } + + /** + * @internal + */ + public static function throwError($type, $message, $file, $line) + { + throw new \ErrorException($message, 0, $type, $file, $line); + } + + public function __destruct() + { + if (method_exists(parent::class, '__destruct')) { + parent::__destruct(); + } + if (null !== $this->tmp && file_exists($this->tmp)) { + unlink($this->tmp); + } + } +} diff --git a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php new file mode 100644 index 0000000000000..bcb940cb0f754 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\CacheException; + +/** + * @author Nicolas Grekas + * @author Rob Frawley 2nd + * + * @internal + */ +trait FilesystemTrait +{ + use FilesystemCommonTrait; + + /** + * @return bool + */ + public function prune() + { + $time = time(); + $pruned = true; + + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!$h = @fopen($file, 'rb')) { + continue; + } + + if ($time >= (int) $expiresAt = fgets($h)) { + fclose($h); + $pruned = isset($expiresAt[0]) && @unlink($file) && !file_exists($file) && $pruned; + } else { + fclose($h); + } + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $values = array(); + $now = time(); + + foreach ($ids as $id) { + $file = $this->getFile($id); + if (!file_exists($file) || !$h = @fopen($file, 'rb')) { + continue; + } + if ($now >= (int) $expiresAt = fgets($h)) { + fclose($h); + if (isset($expiresAt[0])) { + @unlink($file); + } + } else { + $i = rawurldecode(rtrim(fgets($h))); + $value = stream_get_contents($h); + fclose($h); + if ($i === $id) { + $values[$id] = parent::unserialize($value); + } + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + $file = $this->getFile($id); + + return file_exists($file) && (@filemtime($file) > time() || $this->doFetch(array($id))); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + $ok = true; + $expiresAt = time() + ($lifetime ?: 31557600); // 31557600s = 1 year + + foreach ($values as $id => $value) { + $ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok; + } + + if (!$ok && !is_writable($this->directory)) { + throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory)); + } + + return $ok; + } +} diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php new file mode 100644 index 0000000000000..c2832946f98c2 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php @@ -0,0 +1,249 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Rob Frawley 2nd + * @author Nicolas Grekas + * + * @internal + */ +trait MemcachedTrait +{ + private static $defaultClientOptions = array( + 'persistent_id' => null, + 'username' => null, + 'password' => null, + 'serializer' => 'php', + ); + + private $client; + + public static function isSupported() + { + return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>='); + } + + private function init(\Memcached $client, $namespace, $defaultLifetime) + { + if (!static::isSupported()) { + throw new CacheException('Memcached >= 2.2.0 is required'); + } + $opt = $client->getOption(\Memcached::OPT_SERIALIZER); + if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) { + throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); + } + $this->maxIdLength -= strlen($client->getOption(\Memcached::OPT_PREFIX_KEY)); + + parent::__construct($namespace, $defaultLifetime); + $this->client = $client; + } + + /** + * Creates a Memcached instance. + * + * By default, the binary protocol, no block, and libketama compatible options are enabled. + * + * Examples for servers: + * - 'memcached://user:pass@localhost?weight=33' + * - array(array('localhost', 11211, 33)) + * + * @param array[]|string|string[] An array of servers, a DSN, or an array of DSNs + * @param array An array of options + * + * @return \Memcached + * + * @throws \ErrorEception When invalid options or servers are provided + */ + public static function createConnection($servers, array $options = array()) + { + if (is_string($servers)) { + $servers = array($servers); + } elseif (!is_array($servers)) { + throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, %s given.', gettype($servers))); + } + if (!static::isSupported()) { + throw new CacheException('Memcached >= 2.2.0 is required'); + } + set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); + try { + $options += static::$defaultClientOptions; + $client = new \Memcached($options['persistent_id']); + $username = $options['username']; + $password = $options['password']; + unset($options['persistent_id'], $options['username'], $options['password']); + $options = array_change_key_case($options, CASE_UPPER); + + // set client's options + $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $client->setOption(\Memcached::OPT_NO_BLOCK, true); + if (!array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) { + $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true); + } + foreach ($options as $name => $value) { + if (is_int($name)) { + continue; + } + if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) { + $value = constant('Memcached::'.$name.'_'.strtoupper($value)); + } + $opt = constant('Memcached::OPT_'.$name); + + unset($options[$name]); + $options[$opt] = $value; + } + $client->setOptions($options); + + // parse any DSN in $servers + foreach ($servers as $i => $dsn) { + if (is_array($dsn)) { + continue; + } + if (0 !== strpos($dsn, 'memcached://')) { + throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $dsn)); + } + $params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) { + if (!empty($m[1])) { + list($username, $password) = explode(':', $m[1], 2) + array(1 => null); + } + + return 'file://'; + }, $dsn); + if (false === $params = parse_url($params)) { + throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn)); + } + if (!isset($params['host']) && !isset($params['path'])) { + throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn)); + } + if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) { + $params['weight'] = $m[1]; + $params['path'] = substr($params['path'], 0, -strlen($m[0])); + } + $params += array( + 'host' => isset($params['host']) ? $params['host'] : $params['path'], + 'port' => isset($params['host']) ? 11211 : null, + 'weight' => 0, + ); + if (isset($params['query'])) { + parse_str($params['query'], $query); + $params += $query; + } + + $servers[$i] = array($params['host'], $params['port'], $params['weight']); + } + + // set client's servers, taking care of persistent connections + if (!$client->isPristine()) { + $oldServers = array(); + foreach ($client->getServerList() as $server) { + $oldServers[] = array($server['host'], $server['port']); + } + + $newServers = array(); + foreach ($servers as $server) { + if (1 < count($server)) { + $server = array_values($server); + unset($server[2]); + $server[1] = (int) $server[1]; + } + $newServers[] = $server; + } + + if ($oldServers !== $newServers) { + // before resetting, ensure $servers is valid + $client->addServers($servers); + $client->resetServerList(); + } + } + $client->addServers($servers); + + if (null !== $username || null !== $password) { + if (!method_exists($client, 'setSaslAuthData')) { + trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.'); + } + $client->setSaslAuthData($username, $password); + } + + return $client; + } finally { + restore_error_handler(); + } + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + return $this->checkResultCode($this->client->setMulti($values, $lifetime)); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + try { + return $this->checkResultCode($this->client->getMulti($ids)); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + return false !== $this->client->get($id) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode()); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + foreach ($this->checkResultCode($this->client->deleteMulti($ids)) as $result) { + if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) { + $ok = false; + } + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + return $this->checkResultCode($this->client->flush()); + } + + private function checkResultCode($result) + { + $code = $this->client->getResultCode(); + + if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) { + return $result; + } + + throw new CacheException(sprintf('MemcachedAdapter client error: %s.', strtolower($this->client->getResultMessage()))); + } +} diff --git a/src/Symfony/Component/Cache/Traits/PdoTrait.php b/src/Symfony/Component/Cache/Traits/PdoTrait.php new file mode 100644 index 0000000000000..20d1eee1bd1b6 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/PdoTrait.php @@ -0,0 +1,381 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\ServerInfoAwareConnection; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Schema; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @internal + */ +trait PdoTrait +{ + private $conn; + private $dsn; + private $driver; + private $serverVersion; + private $table = 'cache_items'; + private $idCol = 'item_id'; + private $dataCol = 'item_data'; + private $lifetimeCol = 'item_lifetime'; + private $timeCol = 'item_time'; + private $username = ''; + private $password = ''; + private $connectionOptions = array(); + + private function init($connOrDsn, $namespace, $defaultLifetime, array $options) + { + if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0])); + } + + if ($connOrDsn instanceof \PDO) { + if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { + throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); + } + + $this->conn = $connOrDsn; + } elseif ($connOrDsn instanceof Connection) { + $this->conn = $connOrDsn; + } elseif (is_string($connOrDsn)) { + $this->dsn = $connOrDsn; + } else { + throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, is_object($connOrDsn) ? get_class($connOrDsn) : gettype($connOrDsn))); + } + + $this->table = isset($options['db_table']) ? $options['db_table'] : $this->table; + $this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol; + $this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol; + $this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol; + $this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol; + $this->username = isset($options['db_username']) ? $options['db_username'] : $this->username; + $this->password = isset($options['db_password']) ? $options['db_password'] : $this->password; + $this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions; + + parent::__construct($namespace, $defaultLifetime); + } + + /** + * Creates the table to store cache items which can be called once for setup. + * + * Cache ID are saved in a column of maximum length 255. Cache data is + * saved in a BLOB. + * + * @throws \PDOException When the table already exists + * @throws DBALException When the table already exists + * @throws \DomainException When an unsupported PDO driver is used + */ + public function createTable() + { + // connect if we are not yet + $conn = $this->getConnection(); + + if ($conn instanceof Connection) { + $types = array( + 'mysql' => 'binary', + 'sqlite' => 'text', + 'pgsql' => 'string', + 'oci' => 'string', + 'sqlsrv' => 'string', + ); + if (!isset($types[$this->driver])) { + throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver)); + } + + $schema = new Schema(); + $table = $schema->createTable($this->table); + $table->addColumn($this->idCol, $types[$this->driver], array('length' => 255)); + $table->addColumn($this->dataCol, 'blob', array('length' => 16777215)); + $table->addColumn($this->lifetimeCol, 'integer', array('unsigned' => true, 'notnull' => false)); + $table->addColumn($this->timeCol, 'integer', array('unsigned' => true, 'foo' => 'bar')); + $table->setPrimaryKey(array($this->idCol)); + + foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) { + $conn->exec($sql); + } + + return; + } + + switch ($this->driver) { + case 'mysql': + // We use varbinary for the ID column because it prevents unwanted conversions: + // - character set conversions between server and client + // - trailing space removal + // - case-insensitivity + // - language processing like é == e + $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB"; + break; + case 'sqlite': + $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'pgsql': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'oci': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'sqlsrv': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + default: + throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver)); + } + + $conn->exec($sql); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $now = time(); + $expired = array(); + + $sql = str_pad('', (count($ids) << 1) - 1, '?,'); + $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)"; + $stmt = $this->getConnection()->prepare($sql); + $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); + foreach ($ids as $id) { + $stmt->bindValue(++$i, $id); + } + $stmt->execute(); + + while ($row = $stmt->fetch(\PDO::FETCH_NUM)) { + if (null === $row[1]) { + $expired[] = $row[0]; + } else { + yield $row[0] => parent::unserialize(is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); + } + } + + if ($expired) { + $sql = str_pad('', (count($expired) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)"; + $stmt = $this->getConnection()->prepare($sql); + $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); + foreach ($expired as $id) { + $stmt->bindValue(++$i, $id); + } + $stmt->execute(); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)"; + $stmt = $this->getConnection()->prepare($sql); + + $stmt->bindValue(':id', $id); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); + + return (bool) $stmt->fetchColumn(); + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + $conn = $this->getConnection(); + + if ('' === $namespace) { + if ('sqlite' === $this->driver) { + $sql = "DELETE FROM $this->table"; + } else { + $sql = "TRUNCATE TABLE $this->table"; + } + } else { + $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'"; + } + + $conn->exec($sql); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $sql = str_pad('', (count($ids) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)"; + $stmt = $this->getConnection()->prepare($sql); + $stmt->execute(array_values($ids)); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + $serialized = array(); + $failed = array(); + + foreach ($values as $id => $value) { + try { + $serialized[$id] = serialize($value); + } catch (\Exception $e) { + $failed[] = $id; + } + } + + if (!$serialized) { + return $failed; + } + + $conn = $this->getConnection(); + $driver = $this->driver; + $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; + + switch (true) { + case 'mysql' === $driver: + $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + break; + case 'oci' === $driver: + // DUAL is Oracle specific dummy table + $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?"; + break; + case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='): + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; + break; + case 'sqlite' === $driver: + $sql = 'INSERT OR REPLACE'.substr($insertSql, 6); + break; + case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='): + $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; + break; + default: + $driver = null; + $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"; + break; + } + + $now = time(); + $lifetime = $lifetime ?: null; + $stmt = $conn->prepare($sql); + + if ('sqlsrv' === $driver || 'oci' === $driver) { + $stmt->bindParam(1, $id); + $stmt->bindParam(2, $id); + $stmt->bindParam(3, $data, \PDO::PARAM_LOB); + $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(5, $now, \PDO::PARAM_INT); + $stmt->bindParam(6, $data, \PDO::PARAM_LOB); + $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(8, $now, \PDO::PARAM_INT); + } else { + $stmt->bindParam(':id', $id); + $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(':time', $now, \PDO::PARAM_INT); + } + if (null === $driver) { + $insertStmt = $conn->prepare($insertSql); + + $insertStmt->bindParam(':id', $id); + $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT); + $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT); + } + + foreach ($serialized as $id => $data) { + $stmt->execute(); + + if (null === $driver && !$stmt->rowCount()) { + try { + $insertStmt->execute(); + } catch (DBALException $e) { + } catch (\PDOException $e) { + // A concurrent write won, let it be + } + } + } + + return $failed; + } + + /** + * @return \PDO|Connection + */ + private function getConnection() + { + if (null === $this->conn) { + $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions); + $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + if (null === $this->driver) { + if ($this->conn instanceof \PDO) { + $this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME); + } else { + switch ($this->driver = $this->conn->getDriver()->getName()) { + case 'mysqli': + case 'pdo_mysql': + case 'drizzle_pdo_mysql': + $this->driver = 'mysql'; + break; + case 'pdo_sqlite': + $this->driver = 'sqlite'; + break; + case 'pdo_pgsql': + $this->driver = 'pgsql'; + break; + case 'oci8': + case 'pdo_oracle': + $this->driver = 'oci'; + break; + case 'pdo_sqlsrv': + $this->driver = 'sqlsrv'; + break; + } + } + } + + return $this->conn; + } + + /** + * @return string + */ + private function getServerVersion() + { + if (null === $this->serverVersion) { + $conn = $this->conn instanceof \PDO ? $this->conn : $this->conn->getWrappedConnection(); + if ($conn instanceof \PDO) { + $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION); + } elseif ($conn instanceof ServerInfoAwareConnection) { + $this->serverVersion = $conn->getServerVersion(); + } else { + $this->serverVersion = '0'; + } + } + + return $this->serverVersion; + } +} diff --git a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php new file mode 100644 index 0000000000000..2d2c7db3d014f --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Titouan Galopin + * @author Nicolas Grekas + * + * @internal + */ +trait PhpArrayTrait +{ + private $file; + private $values; + private $fallbackPool; + + /** + * Store an array of cached values. + * + * @param array $values The cached values + */ + public function warmUp(array $values) + { + if (file_exists($this->file)) { + if (!is_file($this->file)) { + throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: %s.', $this->file)); + } + + if (!is_writable($this->file)) { + throw new InvalidArgumentException(sprintf('Cache file is not writable: %s.', $this->file)); + } + } else { + $directory = dirname($this->file); + + if (!is_dir($directory) && !@mkdir($directory, 0777, true)) { + throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: %s.', $directory)); + } + + if (!is_writable($directory)) { + throw new InvalidArgumentException(sprintf('Cache directory is not writable: %s.', $directory)); + } + } + + $dump = <<<'EOF' + $value) { + CacheItem::validateKey(is_int($key) ? (string) $key : $key); + + if (null === $value || is_object($value)) { + try { + $value = serialize($value); + } catch (\Exception $e) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, get_class($value)), 0, $e); + } + } elseif (is_array($value)) { + try { + $serialized = serialize($value); + $unserialized = unserialize($serialized); + } catch (\Exception $e) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable array value.', $key), 0, $e); + } + // Store arrays serialized if they contain any objects or references + if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) { + $value = $serialized; + } + } elseif (is_string($value)) { + // Serialize strings if they could be confused with serialized objects or arrays + if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) { + $value = serialize($value); + } + } elseif (!is_scalar($value)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value))); + } + + $dump .= var_export($key, true).' => '.var_export($value, true).",\n"; + } + + $dump .= "\n);\n"; + $dump = str_replace("' . \"\\0\" . '", "\0", $dump); + + $tmpFile = uniqid($this->file, true); + + file_put_contents($tmpFile, $dump); + @chmod($tmpFile, 0666 & ~umask()); + unset($serialized, $unserialized, $value, $dump); + + @rename($tmpFile, $this->file); + + $this->values = (include $this->file) ?: array(); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + $this->values = array(); + + $cleared = @unlink($this->file) || !file_exists($this->file); + + return $this->fallbackPool->clear() && $cleared; + } + + /** + * Load the cache file. + */ + private function initialize() + { + $this->values = file_exists($this->file) ? (include $this->file ?: array()) : array(); + } +} diff --git a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php new file mode 100644 index 0000000000000..4a7c296134fc9 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Piotr Stankowski + * @author Nicolas Grekas + * @author Rob Frawley 2nd + * + * @internal + */ +trait PhpFilesTrait +{ + use FilesystemCommonTrait; + + private $includeHandler; + + public static function isSupported() + { + return function_exists('opcache_invalidate') && ini_get('opcache.enable'); + } + + /** + * @return bool + */ + public function prune() + { + $time = time(); + $pruned = true; + $allowCompile = 'cli' !== PHP_SAPI || ini_get('opcache.enable_cli'); + + set_error_handler($this->includeHandler); + try { + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + list($expiresAt) = include $file; + + if ($time >= $expiresAt) { + $pruned = @unlink($file) && !file_exists($file) && $pruned; + + if ($allowCompile) { + @opcache_invalidate($file, true); + } + } + } + } finally { + restore_error_handler(); + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $values = array(); + $now = time(); + + set_error_handler($this->includeHandler); + try { + foreach ($ids as $id) { + try { + $file = $this->getFile($id); + list($expiresAt, $values[$id]) = include $file; + if ($now >= $expiresAt) { + unset($values[$id]); + } + } catch (\Exception $e) { + continue; + } + } + } finally { + restore_error_handler(); + } + + foreach ($values as $id => $value) { + if ('N;' === $value) { + $values[$id] = null; + } elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) { + $values[$id] = parent::unserialize($value); + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + return (bool) $this->doFetch(array($id)); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + $ok = true; + $data = array($lifetime ? time() + $lifetime : PHP_INT_MAX, ''); + $allowCompile = 'cli' !== PHP_SAPI || ini_get('opcache.enable_cli'); + + foreach ($values as $key => $value) { + if (null === $value || is_object($value)) { + $value = serialize($value); + } elseif (is_array($value)) { + $serialized = serialize($value); + $unserialized = parent::unserialize($serialized); + // Store arrays serialized if they contain any objects or references + if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) { + $value = $serialized; + } + } elseif (is_string($value)) { + // Serialize strings if they could be confused with serialized objects or arrays + if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) { + $value = serialize($value); + } + } elseif (!is_scalar($value)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable %s value.', $key, gettype($value))); + } + + $data[1] = $value; + $file = $this->getFile($key, true); + $ok = $this->write($file, 'directory)) { + throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory)); + } + + return $ok; + } +} diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php new file mode 100644 index 0000000000000..b45d65adc8530 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -0,0 +1,334 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Predis\Connection\Factory; +use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\Aggregate\PredisCluster; +use Predis\Connection\Aggregate\RedisCluster; +use Predis\Response\Status; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Aurimas Niekis + * @author Nicolas Grekas + * + * @internal + */ +trait RedisTrait +{ + private static $defaultConnectionOptions = array( + 'class' => null, + 'persistent' => 0, + 'persistent_id' => null, + 'timeout' => 30, + 'read_timeout' => 0, + 'retry_interval' => 0, + ); + private $redis; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient + */ + public function init($redisClient, $namespace = '', $defaultLifetime = 0) + { + parent::__construct($namespace, $defaultLifetime); + + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) { + throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient))); + } + $this->redis = $redisClient; + } + + /** + * Creates a Redis connection using a DSN configuration. + * + * Example DSN: + * - redis://localhost + * - redis://example.com:1234 + * - redis://secret@example.com/13 + * - redis:///var/run/redis.sock + * - redis://secret@/var/run/redis.sock/13 + * + * @param string $dsn + * @param array $options See self::$defaultConnectionOptions + * + * @throws InvalidArgumentException When the DSN is invalid. + * + * @return \Redis|\Predis\Client According to the "class" option + */ + public static function createConnection($dsn, array $options = array()) + { + if (0 !== strpos($dsn, 'redis://')) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn)); + } + $params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) { + if (isset($m[1])) { + $auth = $m[1]; + } + + return 'file://'; + }, $dsn); + if (false === $params = parse_url($params)) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn)); + } + if (!isset($params['host']) && !isset($params['path'])) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn)); + } + if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) { + $params['dbindex'] = $m[1]; + $params['path'] = substr($params['path'], 0, -strlen($m[0])); + } + if (isset($params['host'])) { + $scheme = 'tcp'; + } else { + $scheme = 'unix'; + } + $params += array( + 'host' => isset($params['host']) ? $params['host'] : $params['path'], + 'port' => isset($params['host']) ? 6379 : null, + 'dbindex' => 0, + ); + if (isset($params['query'])) { + parse_str($params['query'], $query); + $params += $query; + } + $params += $options + self::$defaultConnectionOptions; + $class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class']; + + if (is_a($class, \Redis::class, true)) { + $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect'; + $redis = new $class(); + @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']); + + if (@!$redis->isConnected()) { + $e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : ''; + throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn)); + } + + if ((null !== $auth && !$redis->auth($auth)) + || ($params['dbindex'] && !$redis->select($params['dbindex'])) + || ($params['read_timeout'] && !$redis->setOption(\Redis::OPT_READ_TIMEOUT, $params['read_timeout'])) + ) { + $e = preg_replace('/^ERR /', '', $redis->getLastError()); + throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn)); + } + } elseif (is_a($class, \Predis\Client::class, true)) { + $params['scheme'] = $scheme; + $params['database'] = $params['dbindex'] ?: null; + $params['password'] = $auth; + $redis = new $class((new Factory())->create($params)); + } elseif (class_exists($class, false)) { + throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class)); + } else { + throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class)); + } + + return $redis; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + if ($ids) { + $values = $this->pipeline(function () use ($ids) { + foreach ($ids as $id) { + yield 'get' => array($id); + } + }); + foreach ($values as $id => $v) { + if ($v) { + yield $id => parent::unserialize($v); + } + } + } + } + + /** + * {@inheritdoc} + */ + protected function doHave($id) + { + return (bool) $this->redis->exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace) + { + // When using a native Redis cluster, clearing the cache cannot work and always returns false. + // Clearing the cache should then be done by any other means (e.g. by restarting the cluster). + + $cleared = true; + $hosts = array($this->redis); + $evalArgs = array(array($namespace), 0); + + if ($this->redis instanceof \Predis\Client) { + $evalArgs = array(0, $namespace); + + $connection = $this->redis->getConnection(); + if ($connection instanceof PredisCluster) { + $hosts = array(); + foreach ($connection as $c) { + $hosts[] = new \Predis\Client($c); + } + } elseif ($connection instanceof RedisCluster) { + return false; + } + } elseif ($this->redis instanceof \RedisArray) { + $hosts = array(); + foreach ($this->redis->_hosts() as $host) { + $hosts[] = $this->redis->_instance($host); + } + } elseif ($this->redis instanceof \RedisCluster) { + return false; + } + foreach ($hosts as $host) { + if (!isset($namespace[0])) { + $cleared = $host->flushDb() && $cleared; + continue; + } + + $info = $host->info('Server'); + $info = isset($info['Server']) ? $info['Server'] : $info; + + if (!version_compare($info['redis_version'], '2.8', '>=')) { + // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS + // can hang your server when it is executed against large databases (millions of items). + // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above. + $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('DEL',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $evalArgs[0], $evalArgs[1]) && $cleared; + continue; + } + + $cursor = null; + do { + $keys = $host instanceof \Predis\Client ? $host->scan($cursor, 'MATCH', $namespace.'*', 'COUNT', 1000) : $host->scan($cursor, $namespace.'*', 1000); + if (isset($keys[1]) && is_array($keys[1])) { + $cursor = $keys[0]; + $keys = $keys[1]; + } + if ($keys) { + $host->del($keys); + } + } while ($cursor = (int) $cursor); + } + + return $cleared; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + if ($ids) { + $this->redis->del($ids); + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + $serialized = array(); + $failed = array(); + + foreach ($values as $id => $value) { + try { + $serialized[$id] = serialize($value); + } catch (\Exception $e) { + $failed[] = $id; + } + } + + if (!$serialized) { + return $failed; + } + + $results = $this->pipeline(function () use ($serialized, $lifetime) { + foreach ($serialized as $id => $value) { + if (0 >= $lifetime) { + yield 'set' => array($id, $value); + } else { + yield 'setEx' => array($id, $lifetime, $value); + } + } + }); + foreach ($results as $id => $result) { + if (true !== $result && (!$result instanceof Status || $result !== Status::get('OK'))) { + $failed[] = $id; + } + } + + return $failed; + } + + private function pipeline(\Closure $generator) + { + $ids = array(); + + if ($this->redis instanceof \Predis\Client && !$this->redis->getConnection() instanceof ClusterInterface) { + $results = $this->redis->pipeline(function ($redis) use ($generator, &$ids) { + foreach ($generator() as $command => $args) { + call_user_func_array(array($redis, $command), $args); + $ids[] = $args[0]; + } + }); + } elseif ($this->redis instanceof \RedisArray) { + $connections = $results = $ids = array(); + foreach ($generator() as $command => $args) { + if (!isset($connections[$h = $this->redis->_target($args[0])])) { + $connections[$h] = array($this->redis->_instance($h), -1); + $connections[$h][0]->multi(\Redis::PIPELINE); + } + call_user_func_array(array($connections[$h][0], $command), $args); + $results[] = array($h, ++$connections[$h][1]); + $ids[] = $args[0]; + } + foreach ($connections as $h => $c) { + $connections[$h] = $c[0]->exec(); + } + foreach ($results as $k => list($h, $c)) { + $results[$k] = $connections[$h][$c]; + } + } elseif ($this->redis instanceof \RedisCluster || ($this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof ClusterInterface)) { + // phpredis & predis don't support pipelining with RedisCluster + // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining + // see https://github.com/nrk/predis/issues/267#issuecomment-123781423 + $results = array(); + foreach ($generator() as $command => $args) { + $results[] = call_user_func_array(array($this->redis, $command), $args); + $ids[] = $args[0]; + } + } else { + $this->redis->multi(\Redis::PIPELINE); + foreach ($generator() as $command => $args) { + call_user_func_array(array($this->redis, $command), $args); + $ids[] = $args[0]; + } + $results = $this->redis->exec(); + } + + foreach ($ids as $k => $id) { + yield $id => $results[$k]; + } + } +} diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json new file mode 100644 index 0000000000000..510d910686224 --- /dev/null +++ b/src/Symfony/Component/Cache/composer.json @@ -0,0 +1,49 @@ +{ + "name": "symfony/cache", + "type": "library", + "description": "Symfony Cache component with PSR-6, PSR-16, and tags", + "keywords": ["caching", "psr6"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "provide": { + "psr/cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0" + }, + "require": { + "php": "^7.1.3", + "psr/cache": "~1.0", + "psr/log": "~1.0", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "~1.6", + "doctrine/dbal": "~2.4", + "predis/predis": "~1.0" + }, + "conflict": { + "symfony/var-dumper": "<3.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Cache\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + } +} diff --git a/src/Symfony/Component/Cache/phpunit.xml.dist b/src/Symfony/Component/Cache/phpunit.xml.dist new file mode 100644 index 0000000000000..6b5c79331d14f --- /dev/null +++ b/src/Symfony/Component/Cache/phpunit.xml.dist @@ -0,0 +1,49 @@ + + + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + + + + + + + + Cache\IntegrationTests + Doctrine\Common\Cache + Symfony\Component\Cache + Symfony\Component\Cache\Traits + + + + + + + diff --git a/src/Symfony/Component/ClassLoader/ApcClassLoader.php b/src/Symfony/Component/ClassLoader/ApcClassLoader.php deleted file mode 100644 index 46ee4a8566b1f..0000000000000 --- a/src/Symfony/Component/ClassLoader/ApcClassLoader.php +++ /dev/null @@ -1,141 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -/** - * ApcClassLoader implements a wrapping autoloader cached in APC for PHP 5.3. - * - * It expects an object implementing a findFile method to find the file. This - * allows using it as a wrapper around the other loaders of the component (the - * ClassLoader and the UniversalClassLoader for instance) but also around any - * other autoloaders following this convention (the Composer one for instance). - * - * // with a Symfony autoloader - * use Symfony\Component\ClassLoader\ClassLoader; - * - * $loader = new ClassLoader(); - * $loader->addPrefix('Symfony\Component', __DIR__.'/component'); - * $loader->addPrefix('Symfony', __DIR__.'/framework'); - * - * // or with a Composer autoloader - * use Composer\Autoload\ClassLoader; - * - * $loader = new ClassLoader(); - * $loader->add('Symfony\Component', __DIR__.'/component'); - * $loader->add('Symfony', __DIR__.'/framework'); - * - * $cachedLoader = new ApcClassLoader('my_prefix', $loader); - * - * // activate the cached autoloader - * $cachedLoader->register(); - * - * // eventually deactivate the non-cached loader if it was registered previously - * // to be sure to use the cached one. - * $loader->unregister(); - * - * @author Fabien Potencier - * @author Kris Wallsmith - */ -class ApcClassLoader -{ - private $prefix; - - /** - * A class loader object that implements the findFile() method. - * - * @var object - */ - protected $decorated; - - /** - * Constructor. - * - * @param string $prefix The APC namespace prefix to use - * @param object $decorated A class loader object that implements the findFile() method - * - * @throws \RuntimeException - * @throws \InvalidArgumentException - */ - public function __construct($prefix, $decorated) - { - if (!function_exists('apcu_fetch')) { - throw new \RuntimeException('Unable to use ApcClassLoader as APC is not installed.'); - } - - if (!method_exists($decorated, 'findFile')) { - throw new \InvalidArgumentException('The class finder must implement a "findFile" method.'); - } - - $this->prefix = $prefix; - $this->decorated = $decorated; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return bool|null True, if loaded - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - require $file; - - return true; - } - } - - /** - * Finds a file by class name while caching lookups to APC. - * - * @param string $class A class name to resolve to file - * - * @return string|null - */ - public function findFile($class) - { - $file = apcu_fetch($this->prefix.$class, $success); - - if (!$success) { - apcu_store($this->prefix.$class, $file = $this->decorated->findFile($class) ?: null); - } - - return $file; - } - - /** - * Passes through all unknown calls onto the decorated object. - */ - public function __call($method, $args) - { - return call_user_func_array(array($this->decorated, $method), $args); - } -} diff --git a/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php b/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php deleted file mode 100644 index 0ab45678f9af1..0000000000000 --- a/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.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\Component\ClassLoader; - -@trigger_error('The '.__NAMESPACE__.'\ApcUniversalClassLoader class is deprecated since version 2.7 and will be removed in 3.0. Use the Symfony\Component\ClassLoader\ApcClassLoader class instead.', E_USER_DEPRECATED); - -/** - * ApcUniversalClassLoader implements a "universal" autoloader cached in APC for PHP 5.3. - * - * It is able to load classes that use either: - * - * * The technical interoperability standards for PHP 5.3 namespaces and - * class names (https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md); - * - * * The PEAR naming convention for classes (http://pear.php.net/). - * - * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be - * looked for in a list of locations to ease the vendoring of a sub-set of - * classes for large projects. - * - * Example usage: - * - * require 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php'; - * require 'vendor/symfony/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php'; - * - * use Symfony\Component\ClassLoader\ApcUniversalClassLoader; - * - * $loader = new ApcUniversalClassLoader('apc.prefix.'); - * - * // register classes with namespaces - * $loader->registerNamespaces(array( - * 'Symfony\Component' => __DIR__.'/component', - * 'Symfony' => __DIR__.'/framework', - * 'Sensio' => array(__DIR__.'/src', __DIR__.'/vendor'), - * )); - * - * // register a library using the PEAR naming convention - * $loader->registerPrefixes(array( - * 'Swift_' => __DIR__.'/Swift', - * )); - * - * // activate the autoloader - * $loader->register(); - * - * In this example, if you try to use a class in the Symfony\Component - * namespace or one of its children (Symfony\Component\Console for instance), - * the autoloader will first look for the class under the component/ - * directory, and it will then fallback to the framework/ directory if not - * found before giving up. - * - * @author Fabien Potencier - * @author Kris Wallsmith - * - * @deprecated since version 2.4, to be removed in 3.0. - * Use the {@link ClassLoader} class instead. - */ -class ApcUniversalClassLoader extends UniversalClassLoader -{ - private $prefix; - - /** - * Constructor. - * - * @param string $prefix A prefix to create a namespace in APC - * - * @throws \RuntimeException - */ - public function __construct($prefix) - { - if (!function_exists('apcu_fetch')) { - throw new \RuntimeException('Unable to use ApcUniversalClassLoader as APC is not enabled.'); - } - - $this->prefix = $prefix; - } - - /** - * Finds a file by class name while caching lookups to APC. - * - * @param string $class A class name to resolve to file - * - * @return string|null The path, if found - */ - public function findFile($class) - { - $file = apcu_fetch($this->prefix.$class, $success); - - if (!$success) { - apcu_store($this->prefix.$class, $file = parent::findFile($class) ?: null); - } - - return $file; - } -} diff --git a/src/Symfony/Component/ClassLoader/CHANGELOG.md b/src/Symfony/Component/ClassLoader/CHANGELOG.md deleted file mode 100644 index 64660a8768645..0000000000000 --- a/src/Symfony/Component/ClassLoader/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -CHANGELOG -========= - -2.4.0 ------ - - * deprecated the UniversalClassLoader in favor of the ClassLoader class instead - * deprecated the ApcUniversalClassLoader in favor of the ApcClassLoader class instead - * deprecated the DebugUniversalClassLoader in favor of the DebugClassLoader class from the Debug component - * deprecated the DebugClassLoader as it has been moved to the Debug component instead - -2.3.0 ------ - - * added a WinCacheClassLoader for WinCache - -2.1.0 ------ - - * added a DebugClassLoader able to wrap any autoloader providing a findFile - method - * added a new ApcClassLoader and XcacheClassLoader using composition to wrap - other loaders - * added a new ClassLoader which does not distinguish between namespaced and - pear-like classes (as the PEAR convention is a subset of PSR-0) and - supports using Composer's namespace maps - * added a class map generator - * added support for loading globally-installed PEAR packages diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php deleted file mode 100644 index cd3c433ed200a..0000000000000 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ /dev/null @@ -1,422 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -/** - * ClassCollectionLoader. - * - * @author Fabien Potencier - */ -class ClassCollectionLoader -{ - private static $loaded; - private static $seen; - private static $useTokenizer = true; - - /** - * Loads a list of classes and caches them in one big file. - * - * @param array $classes An array of classes to load - * @param string $cacheDir A cache directory - * @param string $name The cache name prefix - * @param bool $autoReload Whether to flush the cache when the cache is stale or not - * @param bool $adaptive Whether to remove already declared classes or not - * @param string $extension File extension of the resulting file - * - * @throws \InvalidArgumentException When class can't be loaded - */ - public static function load($classes, $cacheDir, $name, $autoReload, $adaptive = false, $extension = '.php') - { - // each $name can only be loaded once per PHP process - if (isset(self::$loaded[$name])) { - return; - } - - self::$loaded[$name] = true; - - if ($adaptive) { - $declared = array_merge(get_declared_classes(), get_declared_interfaces()); - if (function_exists('get_declared_traits')) { - $declared = array_merge($declared, get_declared_traits()); - } - - // don't include already declared classes - $classes = array_diff($classes, $declared); - - // the cache is different depending on which classes are already declared - $name = $name.'-'.substr(hash('sha256', implode('|', $classes)), 0, 5); - } - - $classes = array_unique($classes); - - // cache the core classes - if (!is_dir($cacheDir) && !@mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) { - throw new \RuntimeException(sprintf('Class Collection Loader was not able to create directory "%s"', $cacheDir)); - } - $cacheDir = rtrim(realpath($cacheDir) ?: $cacheDir, '/'.DIRECTORY_SEPARATOR); - $cache = $cacheDir.'/'.$name.$extension; - - // auto-reload - $reload = false; - if ($autoReload) { - $metadata = $cache.'.meta'; - if (!is_file($metadata) || !is_file($cache)) { - $reload = true; - } else { - $time = filemtime($cache); - $meta = unserialize(file_get_contents($metadata)); - - sort($meta[1]); - sort($classes); - - if ($meta[1] != $classes) { - $reload = true; - } else { - foreach ($meta[0] as $resource) { - if (!is_file($resource) || filemtime($resource) > $time) { - $reload = true; - - break; - } - } - } - } - } - - if (!$reload && file_exists($cache)) { - require_once $cache; - - return; - } - if (!$adaptive) { - $declared = array_merge(get_declared_classes(), get_declared_interfaces()); - if (function_exists('get_declared_traits')) { - $declared = array_merge($declared, get_declared_traits()); - } - } - - $spacesRegex = '(?:\s*+(?:(?:\#|//)[^\n]*+\n|/\*(?:(?getName(), $declared)) { - continue; - } - - $files[] = $file = $class->getFileName(); - $c = file_get_contents($file); - - if (preg_match($dontInlineRegex, $c)) { - $file = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $file)); - - for ($i = 0; isset($file[$i], $cacheDir[$i]); ++$i) { - if ($file[$i] !== $cacheDir[$i]) { - break; - } - } - if (1 >= $i) { - $file = var_export(implode('/', $file), true); - } else { - $file = array_slice($file, $i); - $file = str_repeat('../', count($cacheDir) - $i).implode('/', $file); - $file = '__DIR__.'.var_export('/'.$file, true); - } - - $c = "\nnamespace {require $file;}"; - } else { - $c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', $c); - - // fakes namespace declaration for global code - if (!$class->inNamespace()) { - $c = "\nnamespace\n{\n".$c."\n}\n"; - } - - $c = self::fixNamespaceDeclarations('= 70000) { - // PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098 - unset($tokens, $rawChunk); - gc_mem_caches(); - } - - return $output; - } - - /** - * This method is only useful for testing. - */ - public static function enableTokenizer($bool) - { - self::$useTokenizer = (bool) $bool; - } - - /** - * Strips leading & trailing ws, multiple EOL, multiple ws. - * - * @param string $code Original PHP code - * - * @return string compressed code - */ - private static function compressCode($code) - { - return preg_replace( - array('/^\s+/m', '/\s+$/m', '/([\n\r]+ *[\n\r]+)+/', '/[ \t]+/'), - array('', '', "\n", ' '), - $code - ); - } - - /** - * Writes a cache file. - * - * @param string $file Filename - * @param string $content Temporary file content - * - * @throws \RuntimeException when a cache file cannot be written - */ - private static function writeCacheFile($file, $content) - { - $dir = dirname($file); - if (!is_writable($dir)) { - throw new \RuntimeException(sprintf('Cache directory "%s" is not writable.', $dir)); - } - - $tmpFile = tempnam($dir, basename($file)); - - if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) { - @chmod($file, 0666 & ~umask()); - - return; - } - - throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file)); - } - - /** - * Gets an ordered array of passed classes including all their dependencies. - * - * @param array $classes - * - * @return \ReflectionClass[] An array of sorted \ReflectionClass instances (dependencies added if needed) - * - * @throws \InvalidArgumentException When a class can't be loaded - */ - private static function getOrderedClasses(array $classes) - { - $map = array(); - self::$seen = array(); - foreach ($classes as $class) { - try { - $reflectionClass = new \ReflectionClass($class); - } catch (\ReflectionException $e) { - throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class)); - } - - $map = array_merge($map, self::getClassHierarchy($reflectionClass)); - } - - return $map; - } - - private static function getClassHierarchy(\ReflectionClass $class) - { - if (isset(self::$seen[$class->getName()])) { - return array(); - } - - self::$seen[$class->getName()] = true; - - $classes = array($class); - $parent = $class; - while (($parent = $parent->getParentClass()) && $parent->isUserDefined() && !isset(self::$seen[$parent->getName()])) { - self::$seen[$parent->getName()] = true; - - array_unshift($classes, $parent); - } - - $traits = array(); - - if (method_exists('ReflectionClass', 'getTraits')) { - foreach ($classes as $c) { - foreach (self::resolveDependencies(self::computeTraitDeps($c), $c) as $trait) { - if ($trait !== $c) { - $traits[] = $trait; - } - } - } - } - - return array_merge(self::getInterfaces($class), $traits, $classes); - } - - private static function getInterfaces(\ReflectionClass $class) - { - $classes = array(); - - foreach ($class->getInterfaces() as $interface) { - $classes = array_merge($classes, self::getInterfaces($interface)); - } - - if ($class->isUserDefined() && $class->isInterface() && !isset(self::$seen[$class->getName()])) { - self::$seen[$class->getName()] = true; - - $classes[] = $class; - } - - return $classes; - } - - private static function computeTraitDeps(\ReflectionClass $class) - { - $traits = $class->getTraits(); - $deps = array($class->getName() => $traits); - while ($trait = array_pop($traits)) { - if ($trait->isUserDefined() && !isset(self::$seen[$trait->getName()])) { - self::$seen[$trait->getName()] = true; - $traitDeps = $trait->getTraits(); - $deps[$trait->getName()] = $traitDeps; - $traits = array_merge($traits, $traitDeps); - } - } - - return $deps; - } - - /** - * Dependencies resolution. - * - * This function does not check for circular dependencies as it should never - * occur with PHP traits. - * - * @param array $tree The dependency tree - * @param \ReflectionClass $node The node - * @param \ArrayObject $resolved An array of already resolved dependencies - * @param \ArrayObject $unresolved An array of dependencies to be resolved - * - * @return \ArrayObject The dependencies for the given node - * - * @throws \RuntimeException if a circular dependency is detected - */ - private static function resolveDependencies(array $tree, $node, \ArrayObject $resolved = null, \ArrayObject $unresolved = null) - { - if (null === $resolved) { - $resolved = new \ArrayObject(); - } - if (null === $unresolved) { - $unresolved = new \ArrayObject(); - } - $nodeName = $node->getName(); - - if (isset($tree[$nodeName])) { - $unresolved[$nodeName] = $node; - foreach ($tree[$nodeName] as $dependency) { - if (!$resolved->offsetExists($dependency->getName())) { - self::resolveDependencies($tree, $dependency, $resolved, $unresolved); - } - } - $resolved[$nodeName] = $node; - unset($unresolved[$nodeName]); - } - - return $resolved; - } -} diff --git a/src/Symfony/Component/ClassLoader/ClassLoader.php b/src/Symfony/Component/ClassLoader/ClassLoader.php deleted file mode 100644 index a506dc0941946..0000000000000 --- a/src/Symfony/Component/ClassLoader/ClassLoader.php +++ /dev/null @@ -1,203 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -/** - * ClassLoader implements an PSR-0 class loader. - * - * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md - * - * $loader = new ClassLoader(); - * - * // register classes with namespaces - * $loader->addPrefix('Symfony\Component', __DIR__.'/component'); - * $loader->addPrefix('Symfony', __DIR__.'/framework'); - * - * // activate the autoloader - * $loader->register(); - * - * // to enable searching the include path (e.g. for PEAR packages) - * $loader->setUseIncludePath(true); - * - * In this example, if you try to use a class in the Symfony\Component - * namespace or one of its children (Symfony\Component\Console for instance), - * the autoloader will first look for the class under the component/ - * directory, and it will then fallback to the framework/ directory if not - * found before giving up. - * - * @author Fabien Potencier - * @author Jordi Boggiano - */ -class ClassLoader -{ - private $prefixes = array(); - private $fallbackDirs = array(); - private $useIncludePath = false; - - /** - * Returns prefixes. - * - * @return array - */ - public function getPrefixes() - { - return $this->prefixes; - } - - /** - * Returns fallback directories. - * - * @return array - */ - public function getFallbackDirs() - { - return $this->fallbackDirs; - } - - /** - * Adds prefixes. - * - * @param array $prefixes Prefixes to add - */ - public function addPrefixes(array $prefixes) - { - foreach ($prefixes as $prefix => $path) { - $this->addPrefix($prefix, $path); - } - } - - /** - * Registers a set of classes. - * - * @param string $prefix The classes prefix - * @param array|string $paths The location(s) of the classes - */ - public function addPrefix($prefix, $paths) - { - if (!$prefix) { - foreach ((array) $paths as $path) { - $this->fallbackDirs[] = $path; - } - - return; - } - if (isset($this->prefixes[$prefix])) { - if (is_array($paths)) { - $this->prefixes[$prefix] = array_unique(array_merge( - $this->prefixes[$prefix], - $paths - )); - } elseif (!in_array($paths, $this->prefixes[$prefix])) { - $this->prefixes[$prefix][] = $paths; - } - } else { - $this->prefixes[$prefix] = array_unique((array) $paths); - } - } - - /** - * Turns on searching the include for class files. - * - * @param bool $useIncludePath - */ - public function setUseIncludePath($useIncludePath) - { - $this->useIncludePath = (bool) $useIncludePath; - } - - /** - * Can be used to check if the autoloader uses the include path to check - * for classes. - * - * @return bool - */ - public function getUseIncludePath() - { - return $this->useIncludePath; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return bool|null True, if loaded - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - require $file; - - return true; - } - } - - /** - * Finds the path to the file where the class is defined. - * - * @param string $class The name of the class - * - * @return string|null The path, if found - */ - public function findFile($class) - { - if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name - $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)).DIRECTORY_SEPARATOR; - $className = substr($class, $pos + 1); - } else { - // PEAR-like class name - $classPath = null; - $className = $class; - } - - $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className).'.php'; - - foreach ($this->prefixes as $prefix => $dirs) { - if ($class === strstr($class, $prefix)) { - foreach ($dirs as $dir) { - if (file_exists($dir.DIRECTORY_SEPARATOR.$classPath)) { - return $dir.DIRECTORY_SEPARATOR.$classPath; - } - } - } - } - - foreach ($this->fallbackDirs as $dir) { - if (file_exists($dir.DIRECTORY_SEPARATOR.$classPath)) { - return $dir.DIRECTORY_SEPARATOR.$classPath; - } - } - - if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) { - return $file; - } - } -} diff --git a/src/Symfony/Component/ClassLoader/ClassMapGenerator.php b/src/Symfony/Component/ClassLoader/ClassMapGenerator.php deleted file mode 100644 index a35c90ca10d8b..0000000000000 --- a/src/Symfony/Component/ClassLoader/ClassMapGenerator.php +++ /dev/null @@ -1,164 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -if (!defined('SYMFONY_TRAIT')) { - if (\PHP_VERSION_ID >= 50400) { - define('SYMFONY_TRAIT', T_TRAIT); - } else { - define('SYMFONY_TRAIT', 0); - } -} - -/** - * ClassMapGenerator. - * - * @author Gyula Sallai - */ -class ClassMapGenerator -{ - /** - * Generate a class map file. - * - * @param array|string $dirs Directories or a single path to search in - * @param string $file The name of the class map file - */ - public static function dump($dirs, $file) - { - $dirs = (array) $dirs; - $maps = array(); - - foreach ($dirs as $dir) { - $maps = array_merge($maps, static::createMap($dir)); - } - - file_put_contents($file, sprintf('isFile()) { - continue; - } - - $path = $file->getRealPath() ?: $file->getPathname(); - - if (pathinfo($path, PATHINFO_EXTENSION) !== 'php') { - continue; - } - - $classes = self::findClasses($path); - - if (\PHP_VERSION_ID >= 70000) { - // PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098 - gc_mem_caches(); - } - - foreach ($classes as $class) { - $map[$class] = $path; - } - } - - return $map; - } - - /** - * Extract the classes in the given file. - * - * @param string $path The file to check - * - * @return array The found classes - */ - private static function findClasses($path) - { - $contents = file_get_contents($path); - $tokens = token_get_all($contents); - - $classes = array(); - - $namespace = ''; - for ($i = 0; isset($tokens[$i]); ++$i) { - $token = $tokens[$i]; - - if (!isset($token[1])) { - continue; - } - - $class = ''; - - switch ($token[0]) { - case T_NAMESPACE: - $namespace = ''; - // If there is a namespace, extract it - while (isset($tokens[++$i][1])) { - if (in_array($tokens[$i][0], array(T_STRING, T_NS_SEPARATOR))) { - $namespace .= $tokens[$i][1]; - } - } - $namespace .= '\\'; - break; - case T_CLASS: - case T_INTERFACE: - case SYMFONY_TRAIT: - // Skip usage of ::class constant - $isClassConstant = false; - for ($j = $i - 1; $j > 0; --$j) { - if (!isset($tokens[$j][1])) { - break; - } - - if (T_DOUBLE_COLON === $tokens[$j][0]) { - $isClassConstant = true; - break; - } elseif (!in_array($tokens[$j][0], array(T_WHITESPACE, T_DOC_COMMENT, T_COMMENT))) { - break; - } - } - - if ($isClassConstant) { - break; - } - - // Find the classname - while (isset($tokens[++$i][1])) { - $t = $tokens[$i]; - if (T_STRING === $t[0]) { - $class .= $t[1]; - } elseif ('' !== $class && T_WHITESPACE === $t[0]) { - break; - } - } - - $classes[] = ltrim($namespace.$class, '\\'); - break; - default: - break; - } - } - - return $classes; - } -} diff --git a/src/Symfony/Component/ClassLoader/DebugClassLoader.php b/src/Symfony/Component/ClassLoader/DebugClassLoader.php deleted file mode 100644 index 783cf676f7060..0000000000000 --- a/src/Symfony/Component/ClassLoader/DebugClassLoader.php +++ /dev/null @@ -1,120 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -@trigger_error('The '.__NAMESPACE__.'\DebugClassLoader class is deprecated since version 2.4 and will be removed in 3.0. Use the Symfony\Component\Debug\DebugClassLoader class instead.', E_USER_DEPRECATED); - -/** - * Autoloader checking if the class is really defined in the file found. - * - * The DebugClassLoader will wrap all registered autoloaders providing a - * findFile method and will throw an exception if a file is found but does - * not declare the class. - * - * @author Fabien Potencier - * @author Christophe Coevoet - * - * @deprecated since version 2.4, to be removed in 3.0. - * Use {@link \Symfony\Component\Debug\DebugClassLoader} instead. - */ -class DebugClassLoader -{ - private $classFinder; - - /** - * Constructor. - * - * @param object $classFinder - */ - public function __construct($classFinder) - { - $this->classFinder = $classFinder; - } - - /** - * Gets the wrapped class loader. - * - * @return object a class loader instance - */ - public function getClassLoader() - { - return $this->classFinder; - } - - /** - * Replaces all autoloaders implementing a findFile method by a DebugClassLoader wrapper. - */ - public static function enable() - { - if (!is_array($functions = spl_autoload_functions())) { - return; - } - - foreach ($functions as $function) { - spl_autoload_unregister($function); - } - - foreach ($functions as $function) { - if (is_array($function) && !$function[0] instanceof self && method_exists($function[0], 'findFile')) { - $function = array(new static($function[0]), 'loadClass'); - } - - spl_autoload_register($function); - } - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Finds a file by class name. - * - * @param string $class A class name to resolve to file - * - * @return string|null - */ - public function findFile($class) - { - return $this->classFinder->findFile($class) ?: null; - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return bool|null True, if loaded - * - * @throws \RuntimeException - */ - public function loadClass($class) - { - if ($file = $this->classFinder->findFile($class)) { - require $file; - - if (!class_exists($class, false) && !interface_exists($class, false) && (!function_exists('trait_exists') || !trait_exists($class, false))) { - if (false !== strpos($class, '/')) { - throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); - } - - throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); - } - - return true; - } - } -} diff --git a/src/Symfony/Component/ClassLoader/DebugUniversalClassLoader.php b/src/Symfony/Component/ClassLoader/DebugUniversalClassLoader.php deleted file mode 100644 index 807bcd15e2fb7..0000000000000 --- a/src/Symfony/Component/ClassLoader/DebugUniversalClassLoader.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -@trigger_error('The '.__NAMESPACE__.'\DebugUniversalClassLoader class is deprecated since version 2.4 and will be removed in 3.0. Use the Symfony\Component\Debug\DebugClassLoader class instead.', E_USER_DEPRECATED); - -/** - * Checks that the class is actually declared in the included file. - * - * @author Fabien Potencier - * - * @deprecated since version 2.4, to be removed in 3.0. - * Use the {@link \Symfony\Component\Debug\DebugClassLoader} class instead. - */ -class DebugUniversalClassLoader extends UniversalClassLoader -{ - /** - * Replaces all regular UniversalClassLoader instances by a DebugUniversalClassLoader ones. - */ - public static function enable() - { - if (!is_array($functions = spl_autoload_functions())) { - return; - } - - foreach ($functions as $function) { - spl_autoload_unregister($function); - } - - foreach ($functions as $function) { - if (is_array($function) && $function[0] instanceof UniversalClassLoader) { - $loader = new static(); - $loader->registerNamespaceFallbacks($function[0]->getNamespaceFallbacks()); - $loader->registerPrefixFallbacks($function[0]->getPrefixFallbacks()); - $loader->registerNamespaces($function[0]->getNamespaces()); - $loader->registerPrefixes($function[0]->getPrefixes()); - $loader->useIncludePath($function[0]->getUseIncludePath()); - - $function[0] = $loader; - } - - spl_autoload_register($function); - } - } - - /** - * {@inheritdoc} - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - require $file; - - if (!class_exists($class, false) && !interface_exists($class, false) && (!function_exists('trait_exists') || !trait_exists($class, false))) { - throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); - } - } - } -} diff --git a/src/Symfony/Component/ClassLoader/MapClassLoader.php b/src/Symfony/Component/ClassLoader/MapClassLoader.php deleted file mode 100644 index 1d8bcc2a026b2..0000000000000 --- a/src/Symfony/Component/ClassLoader/MapClassLoader.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -/** - * A class loader that uses a mapping file to look up paths. - * - * @author Fabien Potencier - */ -class MapClassLoader -{ - private $map = array(); - - /** - * Constructor. - * - * @param array $map A map where keys are classes and values the absolute file path - */ - public function __construct(array $map) - { - $this->map = $map; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - */ - public function loadClass($class) - { - if (isset($this->map[$class])) { - require $this->map[$class]; - } - } - - /** - * Finds the path to the file where the class is defined. - * - * @param string $class The name of the class - * - * @return string|null The path, if found - */ - public function findFile($class) - { - if (isset($this->map[$class])) { - return $this->map[$class]; - } - } -} diff --git a/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php b/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php deleted file mode 100644 index a00cf7b83c621..0000000000000 --- a/src/Symfony/Component/ClassLoader/Psr4ClassLoader.php +++ /dev/null @@ -1,94 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -/** - * A PSR-4 compatible class loader. - * - * See http://www.php-fig.org/psr/psr-4/ - * - * @author Alexander M. Turek - */ -class Psr4ClassLoader -{ - /** - * @var array - */ - private $prefixes = array(); - - /** - * @param string $prefix - * @param string $baseDir - */ - public function addPrefix($prefix, $baseDir) - { - $prefix = trim($prefix, '\\').'\\'; - $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; - $this->prefixes[] = array($prefix, $baseDir); - } - - /** - * @param string $class - * - * @return string|null - */ - public function findFile($class) - { - $class = ltrim($class, '\\'); - - foreach ($this->prefixes as $current) { - list($currentPrefix, $currentBaseDir) = $current; - if (0 === strpos($class, $currentPrefix)) { - $classWithoutPrefix = substr($class, strlen($currentPrefix)); - $file = $currentBaseDir.str_replace('\\', DIRECTORY_SEPARATOR, $classWithoutPrefix).'.php'; - if (file_exists($file)) { - return $file; - } - } - } - } - - /** - * @param string $class - * - * @return bool - */ - public function loadClass($class) - { - $file = $this->findFile($class); - if (null !== $file) { - require $file; - - return true; - } - - return false; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Removes this instance from the registered autoloaders. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } -} diff --git a/src/Symfony/Component/ClassLoader/README.md b/src/Symfony/Component/ClassLoader/README.md deleted file mode 100644 index d61992b6a80e7..0000000000000 --- a/src/Symfony/Component/ClassLoader/README.md +++ /dev/null @@ -1,14 +0,0 @@ -ClassLoader Component -===================== - -The ClassLoader component provides tools to autoload your classes and cache -their locations for performance. - -Resources ---------- - - * [Documentation](https://symfony.com/doc/current/components/class_loader/index.html) - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/ClassLoader/Tests/ApcClassLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ApcClassLoaderTest.php deleted file mode 100644 index 8ad7132ee9ea6..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/ApcClassLoaderTest.php +++ /dev/null @@ -1,197 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\ApcClassLoader; -use Symfony\Component\ClassLoader\ClassLoader; - -class ApcClassLoaderTest extends TestCase -{ - protected function setUp() - { - if (!(ini_get('apc.enabled') && ini_get('apc.enable_cli'))) { - $this->markTestSkipped('The apc extension is not enabled.'); - } else { - apcu_clear_cache(); - } - } - - protected function tearDown() - { - if (ini_get('apc.enabled') && ini_get('apc.enable_cli')) { - apcu_clear_cache(); - } - } - - public function testConstructor() - { - $loader = new ClassLoader(); - $loader->addPrefix('Apc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - - $loader = new ApcClassLoader('test.prefix.', $loader); - - $this->assertEquals($loader->findFile('\Apc\Namespaced\FooBar'), apcu_fetch('test.prefix.\Apc\Namespaced\FooBar'), '__construct() takes a prefix as its first argument'); - } - - /** - * @dataProvider getLoadClassTests - */ - public function testLoadClass($className, $testClassName, $message) - { - $loader = new ClassLoader(); - $loader->addPrefix('Apc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Apc_Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - - $loader = new ApcClassLoader('test.prefix.', $loader); - $loader->loadClass($testClassName); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassTests() - { - return array( - array('\\Apc\\Namespaced\\Foo', 'Apc\\Namespaced\\Foo', '->loadClass() loads Apc\Namespaced\Foo class'), - array('Apc_Pearlike_Foo', 'Apc_Pearlike_Foo', '->loadClass() loads Apc_Pearlike_Foo class'), - ); - } - - /** - * @dataProvider getLoadClassFromFallbackTests - */ - public function testLoadClassFromFallback($className, $testClassName, $message) - { - $loader = new ClassLoader(); - $loader->addPrefix('Apc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Apc_Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('', array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/fallback')); - - $loader = new ApcClassLoader('test.prefix.fallback', $loader); - $loader->loadClass($testClassName); - - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassFromFallbackTests() - { - return array( - array('\\Apc\\Namespaced\\Baz', 'Apc\\Namespaced\\Baz', '->loadClass() loads Apc\Namespaced\Baz class'), - array('Apc_Pearlike_Baz', 'Apc_Pearlike_Baz', '->loadClass() loads Apc_Pearlike_Baz class'), - array('\\Apc\\Namespaced\\FooBar', 'Apc\\Namespaced\\FooBar', '->loadClass() loads Apc\Namespaced\Baz class from fallback dir'), - array('Apc_Pearlike_FooBar', 'Apc_Pearlike_FooBar', '->loadClass() loads Apc_Pearlike_Baz class from fallback dir'), - ); - } - - /** - * @dataProvider getLoadClassNamespaceCollisionTests - */ - public function testLoadClassNamespaceCollision($namespaces, $className, $message) - { - $loader = new ClassLoader(); - $loader->addPrefixes($namespaces); - - $loader = new ApcClassLoader('test.prefix.collision.', $loader); - $loader->loadClass($className); - - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassNamespaceCollisionTests() - { - return array( - array( - array( - 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', - 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', - ), - 'Apc\NamespaceCollision\A\Foo', - '->loadClass() loads NamespaceCollision\A\Foo from alpha.', - ), - array( - array( - 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', - 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', - ), - 'Apc\NamespaceCollision\A\Bar', - '->loadClass() loads NamespaceCollision\A\Bar from alpha.', - ), - array( - array( - 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', - 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', - ), - 'Apc\NamespaceCollision\A\B\Foo', - '->loadClass() loads NamespaceCollision\A\B\Foo from beta.', - ), - array( - array( - 'Apc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta', - 'Apc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha', - ), - 'Apc\NamespaceCollision\A\B\Bar', - '->loadClass() loads NamespaceCollision\A\B\Bar from beta.', - ), - ); - } - - /** - * @dataProvider getLoadClassPrefixCollisionTests - */ - public function testLoadClassPrefixCollision($prefixes, $className, $message) - { - $loader = new ClassLoader(); - $loader->addPrefixes($prefixes); - - $loader = new ApcClassLoader('test.prefix.collision.', $loader); - $loader->loadClass($className); - - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassPrefixCollisionTests() - { - return array( - array( - array( - 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', - 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', - ), - 'ApcPrefixCollision_A_Foo', - '->loadClass() loads ApcPrefixCollision_A_Foo from alpha.', - ), - array( - array( - 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', - 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', - ), - 'ApcPrefixCollision_A_Bar', - '->loadClass() loads ApcPrefixCollision_A_Bar from alpha.', - ), - array( - array( - 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', - 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', - ), - 'ApcPrefixCollision_A_B_Foo', - '->loadClass() loads ApcPrefixCollision_A_B_Foo from beta.', - ), - array( - array( - 'ApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/beta/Apc', - 'ApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/Apc/alpha/Apc', - ), - 'ApcPrefixCollision_A_B_Bar', - '->loadClass() loads ApcPrefixCollision_A_B_Bar from beta.', - ), - ); - } -} diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php deleted file mode 100644 index 4adff9fbfeda3..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php +++ /dev/null @@ -1,289 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\ClassCollectionLoader; - -require_once __DIR__.'/Fixtures/ClassesWithParents/GInterface.php'; -require_once __DIR__.'/Fixtures/ClassesWithParents/CInterface.php'; -require_once __DIR__.'/Fixtures/ClassesWithParents/B.php'; -require_once __DIR__.'/Fixtures/ClassesWithParents/A.php'; - -class ClassCollectionLoaderTest extends TestCase -{ - /** - * @requires PHP 5.4 - */ - public function testTraitDependencies() - { - require_once __DIR__.'/Fixtures/deps/traits.php'; - - $r = new \ReflectionClass('Symfony\Component\ClassLoader\ClassCollectionLoader'); - $m = $r->getMethod('getOrderedClasses'); - $m->setAccessible(true); - - $ordered = $m->invoke(null, array('CTFoo')); - - $this->assertEquals( - array('TD', 'TC', 'TB', 'TA', 'TZ', 'CTFoo'), - array_map(function ($class) { return $class->getName(); }, $ordered) - ); - - $ordered = $m->invoke(null, array('CTBar')); - - $this->assertEquals( - array('TD', 'TZ', 'TC', 'TB', 'TA', 'CTBar'), - array_map(function ($class) { return $class->getName(); }, $ordered) - ); - } - - /** - * @dataProvider getDifferentOrders - */ - public function testClassReordering(array $classes) - { - $expected = array( - 'ClassesWithParents\\GInterface', - 'ClassesWithParents\\CInterface', - 'ClassesWithParents\\B', - 'ClassesWithParents\\A', - ); - - $r = new \ReflectionClass('Symfony\Component\ClassLoader\ClassCollectionLoader'); - $m = $r->getMethod('getOrderedClasses'); - $m->setAccessible(true); - - $ordered = $m->invoke(null, $classes); - - $this->assertEquals($expected, array_map(function ($class) { return $class->getName(); }, $ordered)); - } - - public function getDifferentOrders() - { - return array( - array(array( - 'ClassesWithParents\\A', - 'ClassesWithParents\\CInterface', - 'ClassesWithParents\\GInterface', - 'ClassesWithParents\\B', - )), - array(array( - 'ClassesWithParents\\B', - 'ClassesWithParents\\A', - 'ClassesWithParents\\CInterface', - )), - array(array( - 'ClassesWithParents\\CInterface', - 'ClassesWithParents\\B', - 'ClassesWithParents\\A', - )), - array(array( - 'ClassesWithParents\\A', - )), - ); - } - - /** - * @dataProvider getDifferentOrdersForTraits - * @requires PHP 5.4 - */ - public function testClassWithTraitsReordering(array $classes) - { - require_once __DIR__.'/Fixtures/ClassesWithParents/ATrait.php'; - require_once __DIR__.'/Fixtures/ClassesWithParents/BTrait.php'; - require_once __DIR__.'/Fixtures/ClassesWithParents/CTrait.php'; - require_once __DIR__.'/Fixtures/ClassesWithParents/D.php'; - require_once __DIR__.'/Fixtures/ClassesWithParents/E.php'; - - $expected = array( - 'ClassesWithParents\\GInterface', - 'ClassesWithParents\\CInterface', - 'ClassesWithParents\\ATrait', - 'ClassesWithParents\\BTrait', - 'ClassesWithParents\\CTrait', - 'ClassesWithParents\\B', - 'ClassesWithParents\\A', - 'ClassesWithParents\\D', - 'ClassesWithParents\\E', - ); - - $r = new \ReflectionClass('Symfony\Component\ClassLoader\ClassCollectionLoader'); - $m = $r->getMethod('getOrderedClasses'); - $m->setAccessible(true); - - $ordered = $m->invoke(null, $classes); - - $this->assertEquals($expected, array_map(function ($class) { return $class->getName(); }, $ordered)); - } - - public function getDifferentOrdersForTraits() - { - return array( - array(array( - 'ClassesWithParents\\E', - 'ClassesWithParents\\ATrait', - )), - array(array( - 'ClassesWithParents\\E', - )), - ); - } - - /** - * @requires PHP 5.4 - */ - public function testFixClassWithTraitsOrdering() - { - require_once __DIR__.'/Fixtures/ClassesWithParents/CTrait.php'; - require_once __DIR__.'/Fixtures/ClassesWithParents/F.php'; - require_once __DIR__.'/Fixtures/ClassesWithParents/G.php'; - - $classes = array( - 'ClassesWithParents\\F', - 'ClassesWithParents\\G', - ); - - $expected = array( - 'ClassesWithParents\\CTrait', - 'ClassesWithParents\\F', - 'ClassesWithParents\\G', - ); - - $r = new \ReflectionClass('Symfony\Component\ClassLoader\ClassCollectionLoader'); - $m = $r->getMethod('getOrderedClasses'); - $m->setAccessible(true); - - $ordered = $m->invoke(null, $classes); - - $this->assertEquals($expected, array_map(function ($class) { return $class->getName(); }, $ordered)); - } - - /** - * @dataProvider getFixNamespaceDeclarationsData - */ - public function testFixNamespaceDeclarations($source, $expected) - { - $this->assertEquals('assertEquals('assertEquals(<<<'EOF' -namespace Namespaced -{ -class WithComments -{ -public static $loaded = true; -} -$string ='string should not be modified {$string}'; -$heredoc = (<< - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\ClassLoader; - -class ClassLoaderTest extends TestCase -{ - public function testGetPrefixes() - { - $loader = new ClassLoader(); - $loader->addPrefix('Foo', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Bar', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Bas', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $prefixes = $loader->getPrefixes(); - $this->assertArrayHasKey('Foo', $prefixes); - $this->assertArrayNotHasKey('Foo1', $prefixes); - $this->assertArrayHasKey('Bar', $prefixes); - $this->assertArrayHasKey('Bas', $prefixes); - } - - public function testGetFallbackDirs() - { - $loader = new ClassLoader(); - $loader->addPrefix(null, __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix(null, __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $fallback_dirs = $loader->getFallbackDirs(); - $this->assertCount(2, $fallback_dirs); - } - - /** - * @dataProvider getLoadClassTests - */ - public function testLoadClass($className, $testClassName, $message) - { - $loader = new ClassLoader(); - $loader->addPrefix('Namespaced2\\', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Pearlike2_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->loadClass($testClassName); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassTests() - { - return array( - array('\\Namespaced2\\Foo', 'Namespaced2\\Foo', '->loadClass() loads Namespaced2\Foo class'), - array('\\Pearlike2_Foo', 'Pearlike2_Foo', '->loadClass() loads Pearlike2_Foo class'), - ); - } - - /** - * @dataProvider getLoadNonexistentClassTests - */ - public function testLoadNonexistentClass($className, $testClassName, $message) - { - $loader = new ClassLoader(); - $loader->addPrefix('Namespaced2\\', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Pearlike2_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->loadClass($testClassName); - $this->assertFalse(class_exists($className), $message); - } - - public function getLoadNonexistentClassTests() - { - return array( - array('\\Pearlike3_Bar', '\\Pearlike3_Bar', '->loadClass() loads non existing Pearlike3_Bar class with a leading slash'), - ); - } - - public function testAddPrefixSingle() - { - $loader = new ClassLoader(); - $loader->addPrefix('Foo', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Foo', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $prefixes = $loader->getPrefixes(); - $this->assertArrayHasKey('Foo', $prefixes); - $this->assertCount(1, $prefixes['Foo']); - } - - public function testAddPrefixesSingle() - { - $loader = new ClassLoader(); - $loader->addPrefixes(array('Foo' => array('foo', 'foo'))); - $loader->addPrefixes(array('Foo' => array('foo'))); - $prefixes = $loader->getPrefixes(); - $this->assertArrayHasKey('Foo', $prefixes); - $this->assertCount(1, $prefixes['Foo'], print_r($prefixes, true)); - } - - public function testAddPrefixMulti() - { - $loader = new ClassLoader(); - $loader->addPrefix('Foo', 'foo'); - $loader->addPrefix('Foo', 'bar'); - $prefixes = $loader->getPrefixes(); - $this->assertArrayHasKey('Foo', $prefixes); - $this->assertCount(2, $prefixes['Foo']); - $this->assertContains('foo', $prefixes['Foo']); - $this->assertContains('bar', $prefixes['Foo']); - } - - public function testUseIncludePath() - { - $loader = new ClassLoader(); - $this->assertFalse($loader->getUseIncludePath()); - - $this->assertNull($loader->findFile('Foo')); - - $includePath = get_include_path(); - - $loader->setUseIncludePath(true); - $this->assertTrue($loader->getUseIncludePath()); - - set_include_path(__DIR__.'/Fixtures/includepath'.PATH_SEPARATOR.$includePath); - - $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'includepath'.DIRECTORY_SEPARATOR.'Foo.php', $loader->findFile('Foo')); - - set_include_path($includePath); - } - - /** - * @dataProvider getLoadClassFromFallbackTests - */ - public function testLoadClassFromFallback($className, $testClassName, $message) - { - $loader = new ClassLoader(); - $loader->addPrefix('Namespaced2\\', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('Pearlike2_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->addPrefix('', array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/fallback')); - $loader->loadClass($testClassName); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassFromFallbackTests() - { - return array( - array('\\Namespaced2\\Baz', 'Namespaced2\\Baz', '->loadClass() loads Namespaced2\Baz class'), - array('\\Pearlike2_Baz', 'Pearlike2_Baz', '->loadClass() loads Pearlike2_Baz class'), - array('\\Namespaced2\\FooBar', 'Namespaced2\\FooBar', '->loadClass() loads Namespaced2\Baz class from fallback dir'), - array('\\Pearlike2_FooBar', 'Pearlike2_FooBar', '->loadClass() loads Pearlike2_Baz class from fallback dir'), - ); - } - - /** - * @dataProvider getLoadClassNamespaceCollisionTests - */ - public function testLoadClassNamespaceCollision($namespaces, $className, $message) - { - $loader = new ClassLoader(); - $loader->addPrefixes($namespaces); - - $loader->loadClass($className); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassNamespaceCollisionTests() - { - return array( - array( - array( - 'NamespaceCollision\\C' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'NamespaceCollision\\C\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'NamespaceCollision\C\Foo', - '->loadClass() loads NamespaceCollision\C\Foo from alpha.', - ), - array( - array( - 'NamespaceCollision\\C\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'NamespaceCollision\\C' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'NamespaceCollision\C\Bar', - '->loadClass() loads NamespaceCollision\C\Bar from alpha.', - ), - array( - array( - 'NamespaceCollision\\C' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'NamespaceCollision\\C\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'NamespaceCollision\C\B\Foo', - '->loadClass() loads NamespaceCollision\C\B\Foo from beta.', - ), - array( - array( - 'NamespaceCollision\\C\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'NamespaceCollision\\C' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'NamespaceCollision\C\B\Bar', - '->loadClass() loads NamespaceCollision\C\B\Bar from beta.', - ), - array( - array( - 'PrefixCollision_C_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'PrefixCollision_C_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'PrefixCollision_C_Foo', - '->loadClass() loads PrefixCollision_C_Foo from alpha.', - ), - array( - array( - 'PrefixCollision_C_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'PrefixCollision_C_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'PrefixCollision_C_Bar', - '->loadClass() loads PrefixCollision_C_Bar from alpha.', - ), - array( - array( - 'PrefixCollision_C_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'PrefixCollision_C_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'PrefixCollision_C_B_Foo', - '->loadClass() loads PrefixCollision_C_B_Foo from beta.', - ), - array( - array( - 'PrefixCollision_C_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'PrefixCollision_C_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'PrefixCollision_C_B_Bar', - '->loadClass() loads PrefixCollision_C_B_Bar from beta.', - ), - ); - } -} diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassMapGeneratorTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassMapGeneratorTest.php deleted file mode 100644 index c12182497a01b..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/ClassMapGeneratorTest.php +++ /dev/null @@ -1,154 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\ClassMapGenerator; - -class ClassMapGeneratorTest extends TestCase -{ - /** - * @var string|null - */ - private $workspace = null; - - public function prepare_workspace() - { - $this->workspace = sys_get_temp_dir().'/'.microtime(true).'.'.mt_rand(); - mkdir($this->workspace, 0777, true); - $this->workspace = realpath($this->workspace); - } - - /** - * @param string $file - */ - private function clean($file) - { - if (is_dir($file) && !is_link($file)) { - $dir = new \FilesystemIterator($file); - foreach ($dir as $childFile) { - $this->clean($childFile); - } - - rmdir($file); - } else { - unlink($file); - } - } - - /** - * @dataProvider getTestCreateMapTests - */ - public function testDump($directory) - { - $this->prepare_workspace(); - - $file = $this->workspace.'/file'; - - $generator = new ClassMapGenerator(); - $generator->dump($directory, $file); - $this->assertFileExists($file); - - $this->clean($this->workspace); - } - - /** - * @dataProvider getTestCreateMapTests - */ - public function testCreateMap($directory, $expected) - { - $this->assertEqualsNormalized($expected, ClassMapGenerator::createMap($directory)); - } - - public function getTestCreateMapTests() - { - $data = array( - array(__DIR__.'/Fixtures/Namespaced', array( - 'Namespaced\\Bar' => realpath(__DIR__).'/Fixtures/Namespaced/Bar.php', - 'Namespaced\\Foo' => realpath(__DIR__).'/Fixtures/Namespaced/Foo.php', - 'Namespaced\\Baz' => realpath(__DIR__).'/Fixtures/Namespaced/Baz.php', - 'Namespaced\\WithComments' => realpath(__DIR__).'/Fixtures/Namespaced/WithComments.php', - 'Namespaced\\WithStrictTypes' => realpath(__DIR__).'/Fixtures/Namespaced/WithStrictTypes.php', - 'Namespaced\\WithHaltCompiler' => realpath(__DIR__).'/Fixtures/Namespaced/WithHaltCompiler.php', - 'Namespaced\\WithDirMagic' => realpath(__DIR__).'/Fixtures/Namespaced/WithDirMagic.php', - 'Namespaced\\WithFileMagic' => realpath(__DIR__).'/Fixtures/Namespaced/WithFileMagic.php', - )), - array(__DIR__.'/Fixtures/beta/NamespaceCollision', array( - 'NamespaceCollision\\A\\B\\Bar' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Bar.php', - 'NamespaceCollision\\A\\B\\Foo' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Foo.php', - 'NamespaceCollision\\C\\B\\Bar' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/C/B/Bar.php', - 'NamespaceCollision\\C\\B\\Foo' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/C/B/Foo.php', - )), - array(__DIR__.'/Fixtures/Pearlike', array( - 'Pearlike_Foo' => realpath(__DIR__).'/Fixtures/Pearlike/Foo.php', - 'Pearlike_Bar' => realpath(__DIR__).'/Fixtures/Pearlike/Bar.php', - 'Pearlike_Baz' => realpath(__DIR__).'/Fixtures/Pearlike/Baz.php', - 'Pearlike_WithComments' => realpath(__DIR__).'/Fixtures/Pearlike/WithComments.php', - )), - array(__DIR__.'/Fixtures/classmap', array( - 'Foo\\Bar\\A' => realpath(__DIR__).'/Fixtures/classmap/sameNsMultipleClasses.php', - 'Foo\\Bar\\B' => realpath(__DIR__).'/Fixtures/classmap/sameNsMultipleClasses.php', - 'A' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', - 'Alpha\\A' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', - 'Alpha\\B' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', - 'Beta\\A' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', - 'Beta\\B' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', - 'ClassMap\\SomeInterface' => realpath(__DIR__).'/Fixtures/classmap/SomeInterface.php', - 'ClassMap\\SomeParent' => realpath(__DIR__).'/Fixtures/classmap/SomeParent.php', - 'ClassMap\\SomeClass' => realpath(__DIR__).'/Fixtures/classmap/SomeClass.php', - )), - ); - - if (\PHP_VERSION_ID >= 50400) { - $data[] = array(__DIR__.'/Fixtures/php5.4', array( - 'TFoo' => __DIR__.'/Fixtures/php5.4/traits.php', - 'CFoo' => __DIR__.'/Fixtures/php5.4/traits.php', - 'Foo\\TBar' => __DIR__.'/Fixtures/php5.4/traits.php', - 'Foo\\IBar' => __DIR__.'/Fixtures/php5.4/traits.php', - 'Foo\\TFooBar' => __DIR__.'/Fixtures/php5.4/traits.php', - 'Foo\\CBar' => __DIR__.'/Fixtures/php5.4/traits.php', - )); - } - - if (\PHP_VERSION_ID >= 50500) { - $data[] = array(__DIR__.'/Fixtures/php5.5', array( - 'ClassCons\\Foo' => __DIR__.'/Fixtures/php5.5/class_cons.php', - )); - } - - return $data; - } - - public function testCreateMapFinderSupport() - { - $finder = new \Symfony\Component\Finder\Finder(); - $finder->files()->in(__DIR__.'/Fixtures/beta/NamespaceCollision'); - - $this->assertEqualsNormalized(array( - 'NamespaceCollision\\A\\B\\Bar' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Bar.php', - 'NamespaceCollision\\A\\B\\Foo' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Foo.php', - 'NamespaceCollision\\C\\B\\Bar' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/C/B/Bar.php', - 'NamespaceCollision\\C\\B\\Foo' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/C/B/Foo.php', - ), ClassMapGenerator::createMap($finder)); - } - - protected function assertEqualsNormalized($expected, $actual, $message = null) - { - foreach ($expected as $ns => $path) { - $expected[$ns] = str_replace('\\', '/', $path); - } - foreach ($actual as $ns => $path) { - $actual[$ns] = str_replace('\\', '/', $path); - } - $this->assertEquals($expected, $actual, $message); - } -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Bar.php deleted file mode 100644 index 4259f1451e2c9..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Bar.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\Namespaced; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Baz.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Baz.php deleted file mode 100644 index 3ddb595e25164..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Baz.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\Namespaced; - -class Baz -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Foo.php deleted file mode 100644 index cf0a4b741fd9f..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\Namespaced; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/FooBar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/FooBar.php deleted file mode 100644 index bbbc81515a80f..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Namespaced/FooBar.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\Namespaced; - -class FooBar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Pearlike/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Pearlike/Bar.php deleted file mode 100644 index e774cb9bfbbae..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/Pearlike/Bar.php +++ /dev/null @@ -1,6 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\NamespaceCollision\A; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/alpha/Apc/NamespaceCollision/A/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/alpha/Apc/NamespaceCollision/A/Foo.php deleted file mode 100644 index 184a1b1daf159..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/alpha/Apc/NamespaceCollision/A/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\NamespaceCollision\A; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Bar.php deleted file mode 100644 index 3892f70683deb..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Bar.php +++ /dev/null @@ -1,6 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\NamespaceCollision\A\B; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/NamespaceCollision/A/B/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/NamespaceCollision/A/B/Foo.php deleted file mode 100644 index 450eeb50b9e34..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/NamespaceCollision/A/B/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\NamespaceCollision\A\B; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/fallback/Apc/Pearlike/FooBar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/fallback/Apc/Pearlike/FooBar.php deleted file mode 100644 index 96f2f76c6f94d..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/fallback/Apc/Pearlike/FooBar.php +++ /dev/null @@ -1,6 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Apc\Namespaced; - -class FooBar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/ClassesWithParents/A.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/ClassesWithParents/A.php deleted file mode 100644 index b0f9425950f44..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/ClassesWithParents/A.php +++ /dev/null @@ -1,7 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\Namespaced; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/Baz.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/Baz.php deleted file mode 100644 index a89fbbc313b5d..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/Baz.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\Namespaced; - -class Baz -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/Foo.php deleted file mode 100644 index 1b04072e5add5..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\Namespaced; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/FooBar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/FooBar.php deleted file mode 100644 index 16244ec0c66f2..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Namespaced/FooBar.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\Namespaced; - -class FooBar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Pearlike/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Pearlike/Bar.php deleted file mode 100644 index 8d98583078fcd..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/Pearlike/Bar.php +++ /dev/null @@ -1,6 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\NamespaceCollision\A; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/alpha/LegacyApc/NamespaceCollision/A/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/alpha/LegacyApc/NamespaceCollision/A/Foo.php deleted file mode 100644 index fb75d9a43953b..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/alpha/LegacyApc/NamespaceCollision/A/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\NamespaceCollision\A; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/beta/LegacyApc/LegacyApcPrefixCollision/A/B/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/beta/LegacyApc/LegacyApcPrefixCollision/A/B/Bar.php deleted file mode 100644 index 08834f9fd9672..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/beta/LegacyApc/LegacyApcPrefixCollision/A/B/Bar.php +++ /dev/null @@ -1,6 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\NamespaceCollision\A\B; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/beta/LegacyApc/NamespaceCollision/A/B/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/beta/LegacyApc/NamespaceCollision/A/B/Foo.php deleted file mode 100644 index ddb0a69e94926..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/beta/LegacyApc/NamespaceCollision/A/B/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\NamespaceCollision\A\B; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/fallback/LegacyApc/Pearlike/FooBar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/fallback/LegacyApc/Pearlike/FooBar.php deleted file mode 100644 index f0fac9ef25157..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/LegacyApc/fallback/LegacyApc/Pearlike/FooBar.php +++ /dev/null @@ -1,6 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace LegacyApc\Namespaced; - -class FooBar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Bar.php deleted file mode 100644 index 02b589d27faee..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Bar.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Namespaced; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Baz.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Baz.php deleted file mode 100644 index 0b0bbd057c44a..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Baz.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Namespaced; - -class Baz -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Foo.php deleted file mode 100644 index df5e1f4ce2ec3..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Namespaced; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/WithComments.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/WithComments.php deleted file mode 100644 index 361e53de1c4ee..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Namespaced/WithComments.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 Namespaced; - -class WithComments -{ - /** @Boolean */ - public static $loaded = true; -} - -$string = 'string should not be modified {$string}'; - -$heredoc = (<< - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -class Pearlike_WithComments -{ - /** @Boolean */ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Pearlike2/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/Pearlike2/Bar.php deleted file mode 100644 index 7f5f7977308b7..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/Pearlike2/Bar.php +++ /dev/null @@ -1,6 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace NamespaceCollision\A; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/alpha/NamespaceCollision/A/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/alpha/NamespaceCollision/A/Foo.php deleted file mode 100644 index aee6a080dfb76..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/alpha/NamespaceCollision/A/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace NamespaceCollision\A; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/alpha/NamespaceCollision/C/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/alpha/NamespaceCollision/C/Bar.php deleted file mode 100644 index c1b8dd65ddfa3..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/alpha/NamespaceCollision/C/Bar.php +++ /dev/null @@ -1,8 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace NamespaceCollision\A\B; - -class Bar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/beta/NamespaceCollision/A/B/Foo.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/beta/NamespaceCollision/A/B/Foo.php deleted file mode 100644 index f5f2d727ef5e6..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/beta/NamespaceCollision/A/B/Foo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace NamespaceCollision\A\B; - -class Foo -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/beta/NamespaceCollision/C/B/Bar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/beta/NamespaceCollision/C/B/Bar.php deleted file mode 100644 index 4bb03dc7fd65a..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/beta/NamespaceCollision/C/B/Bar.php +++ /dev/null @@ -1,8 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ClassMap; - -class SomeClass extends SomeParent implements SomeInterface -{ -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/SomeInterface.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/SomeInterface.php deleted file mode 100644 index 1fe5e09aa1f50..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/SomeInterface.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ClassMap; - -interface SomeInterface -{ -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/SomeParent.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/SomeParent.php deleted file mode 100644 index ce2f9fc6c478c..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/SomeParent.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ClassMap; - -abstract class SomeParent -{ -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/multipleNs.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/multipleNs.php deleted file mode 100644 index c7cec646f5f25..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/classmap/multipleNs.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Foo\Bar; - -class A -{ -} -class B -{ -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.php deleted file mode 100644 index 82b30a6f9d0b9..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.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 Namespaced; - -class FooBar -{ - public static $loaded = true; -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/fallback/Namespaced2/FooBar.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/fallback/Namespaced2/FooBar.php deleted file mode 100644 index 1036d43590065..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Fixtures/fallback/Namespaced2/FooBar.php +++ /dev/null @@ -1,8 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\ApcUniversalClassLoader; - -/** - * @group legacy - */ -class LegacyApcUniversalClassLoaderTest extends TestCase -{ - protected function setUp() - { - if (ini_get('apc.enabled') && ini_get('apc.enable_cli')) { - apcu_clear_cache(); - } else { - $this->markTestSkipped('APC is not enabled.'); - } - } - - protected function tearDown() - { - if (ini_get('apc.enabled') && ini_get('apc.enable_cli')) { - apcu_clear_cache(); - } - } - - public function testConstructor() - { - $loader = new ApcUniversalClassLoader('test.prefix.'); - $loader->registerNamespace('LegacyApc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - - $this->assertEquals($loader->findFile('\LegacyApc\Namespaced\FooBar'), apcu_fetch('test.prefix.\LegacyApc\Namespaced\FooBar'), '__construct() takes a prefix as its first argument'); - } - - /** - * @dataProvider getLoadClassTests - */ - public function testLoadClass($className, $testClassName, $message) - { - $loader = new ApcUniversalClassLoader('test.prefix.'); - $loader->registerNamespace('LegacyApc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerPrefix('LegacyApc_Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->loadClass($testClassName); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassTests() - { - return array( - array('\\LegacyApc\\Namespaced\\Foo', 'LegacyApc\\Namespaced\\Foo', '->loadClass() loads LegacyApc\Namespaced\Foo class'), - array('LegacyApc_Pearlike_Foo', 'LegacyApc_Pearlike_Foo', '->loadClass() loads LegacyApc_Pearlike_Foo class'), - ); - } - - /** - * @dataProvider getLoadClassFromFallbackTests - */ - public function testLoadClassFromFallback($className, $testClassName, $message) - { - $loader = new ApcUniversalClassLoader('test.prefix.fallback'); - $loader->registerNamespace('LegacyApc\Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerPrefix('LegacyApc_Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerNamespaceFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/fallback')); - $loader->registerPrefixFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/fallback')); - $loader->loadClass($testClassName); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassFromFallbackTests() - { - return array( - array('\\LegacyApc\\Namespaced\\Baz', 'LegacyApc\\Namespaced\\Baz', '->loadClass() loads LegacyApc\Namespaced\Baz class'), - array('LegacyApc_Pearlike_Baz', 'LegacyApc_Pearlike_Baz', '->loadClass() loads LegacyApc_Pearlike_Baz class'), - array('\\LegacyApc\\Namespaced\\FooBar', 'LegacyApc\\Namespaced\\FooBar', '->loadClass() loads LegacyApc\Namespaced\Baz class from fallback dir'), - array('LegacyApc_Pearlike_FooBar', 'LegacyApc_Pearlike_FooBar', '->loadClass() loads LegacyApc_Pearlike_Baz class from fallback dir'), - ); - } - - /** - * @dataProvider getLoadClassNamespaceCollisionTests - */ - public function testLoadClassNamespaceCollision($namespaces, $className, $message) - { - $loader = new ApcUniversalClassLoader('test.prefix.collision.'); - $loader->registerNamespaces($namespaces); - - $loader->loadClass($className); - - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassNamespaceCollisionTests() - { - return array( - array( - array( - 'LegacyApc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha', - 'LegacyApc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta', - ), - 'LegacyApc\NamespaceCollision\A\Foo', - '->loadClass() loads NamespaceCollision\A\Foo from alpha.', - ), - array( - array( - 'LegacyApc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta', - 'LegacyApc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha', - ), - 'LegacyApc\NamespaceCollision\A\Bar', - '->loadClass() loads NamespaceCollision\A\Bar from alpha.', - ), - array( - array( - 'LegacyApc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha', - 'LegacyApc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta', - ), - 'LegacyApc\NamespaceCollision\A\B\Foo', - '->loadClass() loads NamespaceCollision\A\B\Foo from beta.', - ), - array( - array( - 'LegacyApc\\NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta', - 'LegacyApc\\NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha', - ), - 'LegacyApc\NamespaceCollision\A\B\Bar', - '->loadClass() loads NamespaceCollision\A\B\Bar from beta.', - ), - ); - } - - /** - * @dataProvider getLoadClassPrefixCollisionTests - */ - public function testLoadClassPrefixCollision($prefixes, $className, $message) - { - $loader = new ApcUniversalClassLoader('test.prefix.collision.'); - $loader->registerPrefixes($prefixes); - - $loader->loadClass($className); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassPrefixCollisionTests() - { - return array( - array( - array( - 'LegacyApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha/LegacyApc', - 'LegacyApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta/LegacyApc', - ), - 'LegacyApcPrefixCollision_A_Foo', - '->loadClass() loads LegacyApcPrefixCollision_A_Foo from alpha.', - ), - array( - array( - 'LegacyApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta/LegacyApc', - 'LegacyApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha/LegacyApc', - ), - 'LegacyApcPrefixCollision_A_Bar', - '->loadClass() loads LegacyApcPrefixCollision_A_Bar from alpha.', - ), - array( - array( - 'LegacyApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha/LegacyApc', - 'LegacyApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta/LegacyApc', - ), - 'LegacyApcPrefixCollision_A_B_Foo', - '->loadClass() loads LegacyApcPrefixCollision_A_B_Foo from beta.', - ), - array( - array( - 'LegacyApcPrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/beta/LegacyApc', - 'LegacyApcPrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/LegacyApc/alpha/LegacyApc', - ), - 'LegacyApcPrefixCollision_A_B_Bar', - '->loadClass() loads LegacyApcPrefixCollision_A_B_Bar from beta.', - ), - ); - } -} diff --git a/src/Symfony/Component/ClassLoader/Tests/LegacyUniversalClassLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/LegacyUniversalClassLoaderTest.php deleted file mode 100644 index f9a8b5518b584..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/LegacyUniversalClassLoaderTest.php +++ /dev/null @@ -1,224 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\UniversalClassLoader; - -/** - * @group legacy - */ -class LegacyUniversalClassLoaderTest extends TestCase -{ - /** - * @dataProvider getLoadClassTests - */ - public function testLoadClass($className, $testClassName, $message) - { - $loader = new UniversalClassLoader(); - $loader->registerNamespace('Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerPrefix('Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $this->assertTrue($loader->loadClass($testClassName)); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassTests() - { - return array( - array('\\Namespaced\\Foo', 'Namespaced\\Foo', '->loadClass() loads Namespaced\Foo class'), - array('\\Pearlike_Foo', 'Pearlike_Foo', '->loadClass() loads Pearlike_Foo class'), - ); - } - - public function testUseIncludePath() - { - $loader = new UniversalClassLoader(); - $this->assertFalse($loader->getUseIncludePath()); - - $this->assertNull($loader->findFile('Foo')); - - $includePath = get_include_path(); - - $loader->useIncludePath(true); - $this->assertTrue($loader->getUseIncludePath()); - - set_include_path(__DIR__.'/Fixtures/includepath'.PATH_SEPARATOR.$includePath); - - $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'includepath'.DIRECTORY_SEPARATOR.'Foo.php', $loader->findFile('Foo')); - - set_include_path($includePath); - } - - public function testGetNamespaces() - { - $loader = new UniversalClassLoader(); - $loader->registerNamespace('Foo', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerNamespace('Bar', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerNamespace('Bas', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $namespaces = $loader->getNamespaces(); - $this->assertArrayHasKey('Foo', $namespaces); - $this->assertArrayNotHasKey('Foo1', $namespaces); - $this->assertArrayHasKey('Bar', $namespaces); - $this->assertArrayHasKey('Bas', $namespaces); - } - - public function testGetPrefixes() - { - $loader = new UniversalClassLoader(); - $loader->registerPrefix('Foo', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerPrefix('Bar', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerPrefix('Bas', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $prefixes = $loader->getPrefixes(); - $this->assertArrayHasKey('Foo', $prefixes); - $this->assertArrayNotHasKey('Foo1', $prefixes); - $this->assertArrayHasKey('Bar', $prefixes); - $this->assertArrayHasKey('Bas', $prefixes); - } - - /** - * @dataProvider getLoadClassFromFallbackTests - */ - public function testLoadClassFromFallback($className, $testClassName, $message) - { - $loader = new UniversalClassLoader(); - $loader->registerNamespace('Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerPrefix('Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures'); - $loader->registerNamespaceFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/fallback')); - $loader->registerPrefixFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/fallback')); - $this->assertTrue($loader->loadClass($testClassName)); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassFromFallbackTests() - { - return array( - array('\\Namespaced\\Baz', 'Namespaced\\Baz', '->loadClass() loads Namespaced\Baz class'), - array('\\Pearlike_Baz', 'Pearlike_Baz', '->loadClass() loads Pearlike_Baz class'), - array('\\Namespaced\\FooBar', 'Namespaced\\FooBar', '->loadClass() loads Namespaced\Baz class from fallback dir'), - array('\\Pearlike_FooBar', 'Pearlike_FooBar', '->loadClass() loads Pearlike_Baz class from fallback dir'), - ); - } - - public function testRegisterPrefixFallback() - { - $loader = new UniversalClassLoader(); - $loader->registerPrefixFallback(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/fallback'); - $this->assertEquals(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/fallback'), $loader->getPrefixFallbacks()); - } - - public function testRegisterNamespaceFallback() - { - $loader = new UniversalClassLoader(); - $loader->registerNamespaceFallback(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/Namespaced/fallback'); - $this->assertEquals(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/Namespaced/fallback'), $loader->getNamespaceFallbacks()); - } - - /** - * @dataProvider getLoadClassNamespaceCollisionTests - */ - public function testLoadClassNamespaceCollision($namespaces, $className, $message) - { - $loader = new UniversalClassLoader(); - $loader->registerNamespaces($namespaces); - - $this->assertTrue($loader->loadClass($className)); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassNamespaceCollisionTests() - { - return array( - array( - array( - 'NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'NamespaceCollision\A\Foo', - '->loadClass() loads NamespaceCollision\A\Foo from alpha.', - ), - array( - array( - 'NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'NamespaceCollision\A\Bar', - '->loadClass() loads NamespaceCollision\A\Bar from alpha.', - ), - array( - array( - 'NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'NamespaceCollision\A\B\Foo', - '->loadClass() loads NamespaceCollision\A\B\Foo from beta.', - ), - array( - array( - 'NamespaceCollision\\A\\B' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'NamespaceCollision\\A' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'NamespaceCollision\A\B\Bar', - '->loadClass() loads NamespaceCollision\A\B\Bar from beta.', - ), - ); - } - - /** - * @dataProvider getLoadClassPrefixCollisionTests - */ - public function testLoadClassPrefixCollision($prefixes, $className, $message) - { - $loader = new UniversalClassLoader(); - $loader->registerPrefixes($prefixes); - - $this->assertTrue($loader->loadClass($className)); - $this->assertTrue(class_exists($className), $message); - } - - public function getLoadClassPrefixCollisionTests() - { - return array( - array( - array( - 'PrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'PrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'PrefixCollision_A_Foo', - '->loadClass() loads PrefixCollision_A_Foo from alpha.', - ), - array( - array( - 'PrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'PrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'PrefixCollision_A_Bar', - '->loadClass() loads PrefixCollision_A_Bar from alpha.', - ), - array( - array( - 'PrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - 'PrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - ), - 'PrefixCollision_A_B_Foo', - '->loadClass() loads PrefixCollision_A_B_Foo from beta.', - ), - array( - array( - 'PrefixCollision_A_B_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/beta', - 'PrefixCollision_A_' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/alpha', - ), - 'PrefixCollision_A_B_Bar', - '->loadClass() loads PrefixCollision_A_B_Bar from beta.', - ), - ); - } -} diff --git a/src/Symfony/Component/ClassLoader/Tests/Psr4ClassLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/Psr4ClassLoaderTest.php deleted file mode 100644 index 8c7ef7978616a..0000000000000 --- a/src/Symfony/Component/ClassLoader/Tests/Psr4ClassLoaderTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\Psr4ClassLoader; - -class Psr4ClassLoaderTest extends TestCase -{ - /** - * @param string $className - * @dataProvider getLoadClassTests - */ - public function testLoadClass($className) - { - $loader = new Psr4ClassLoader(); - $loader->addPrefix( - 'Acme\\DemoLib', - __DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'psr-4' - ); - $loader->loadClass($className); - $this->assertTrue(class_exists($className), sprintf('loadClass() should load %s', $className)); - } - - /** - * @return array - */ - public function getLoadClassTests() - { - return array( - array('Acme\\DemoLib\\Foo'), - array('Acme\\DemoLib\\Class_With_Underscores'), - array('Acme\\DemoLib\\Lets\\Go\\Deeper\\Foo'), - array('Acme\\DemoLib\\Lets\\Go\\Deeper\\Class_With_Underscores'), - ); - } - - /** - * @param string $className - * @dataProvider getLoadNonexistentClassTests - */ - public function testLoadNonexistentClass($className) - { - $loader = new Psr4ClassLoader(); - $loader->addPrefix( - 'Acme\\DemoLib', - __DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'psr-4' - ); - $loader->loadClass($className); - $this->assertFalse(class_exists($className), sprintf('loadClass() should not load %s', $className)); - } - - /** - * @return array - */ - public function getLoadNonexistentClassTests() - { - return array( - array('Acme\\DemoLib\\I_Do_Not_Exist'), - array('UnknownVendor\\SomeLib\\I_Do_Not_Exist'), - ); - } -} diff --git a/src/Symfony/Component/ClassLoader/UniversalClassLoader.php b/src/Symfony/Component/ClassLoader/UniversalClassLoader.php deleted file mode 100644 index 961c7518016bb..0000000000000 --- a/src/Symfony/Component/ClassLoader/UniversalClassLoader.php +++ /dev/null @@ -1,307 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -@trigger_error('The '.__NAMESPACE__.'\UniversalClassLoader class is deprecated since version 2.7 and will be removed in 3.0. Use the Symfony\Component\ClassLoader\ClassLoader class instead.', E_USER_DEPRECATED); - -/** - * UniversalClassLoader implements a "universal" autoloader for PHP 5.3. - * - * It is able to load classes that use either: - * - * * The technical interoperability standards for PHP 5.3 namespaces and - * class names (https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md); - * - * * The PEAR naming convention for classes (http://pear.php.net/). - * - * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be - * looked for in a list of locations to ease the vendoring of a sub-set of - * classes for large projects. - * - * Example usage: - * - * $loader = new UniversalClassLoader(); - * - * // register classes with namespaces - * $loader->registerNamespaces(array( - * 'Symfony\Component' => __DIR__.'/component', - * 'Symfony' => __DIR__.'/framework', - * 'Sensio' => array(__DIR__.'/src', __DIR__.'/vendor'), - * )); - * - * // register a library using the PEAR naming convention - * $loader->registerPrefixes(array( - * 'Swift_' => __DIR__.'/Swift', - * )); - * - * - * // to enable searching the include path (e.g. for PEAR packages) - * $loader->useIncludePath(true); - * - * // activate the autoloader - * $loader->register(); - * - * In this example, if you try to use a class in the Symfony\Component - * namespace or one of its children (Symfony\Component\Console for instance), - * the autoloader will first look for the class under the component/ - * directory, and it will then fallback to the framework/ directory if not - * found before giving up. - * - * @author Fabien Potencier - * - * @deprecated since version 2.4, to be removed in 3.0. - * Use the {@link ClassLoader} class instead. - */ -class UniversalClassLoader -{ - private $namespaces = array(); - private $prefixes = array(); - private $namespaceFallbacks = array(); - private $prefixFallbacks = array(); - private $useIncludePath = false; - - /** - * Turns on searching the include for class files. Allows easy loading - * of installed PEAR packages. - * - * @param bool $useIncludePath - */ - public function useIncludePath($useIncludePath) - { - $this->useIncludePath = (bool) $useIncludePath; - } - - /** - * Can be used to check if the autoloader uses the include path to check - * for classes. - * - * @return bool - */ - public function getUseIncludePath() - { - return $this->useIncludePath; - } - - /** - * Gets the configured namespaces. - * - * @return array A hash with namespaces as keys and directories as values - */ - public function getNamespaces() - { - return $this->namespaces; - } - - /** - * Gets the configured class prefixes. - * - * @return array A hash with class prefixes as keys and directories as values - */ - public function getPrefixes() - { - return $this->prefixes; - } - - /** - * Gets the directory(ies) to use as a fallback for namespaces. - * - * @return array An array of directories - */ - public function getNamespaceFallbacks() - { - return $this->namespaceFallbacks; - } - - /** - * Gets the directory(ies) to use as a fallback for class prefixes. - * - * @return array An array of directories - */ - public function getPrefixFallbacks() - { - return $this->prefixFallbacks; - } - - /** - * Registers the directory to use as a fallback for namespaces. - * - * @param array $dirs An array of directories - */ - public function registerNamespaceFallbacks(array $dirs) - { - $this->namespaceFallbacks = $dirs; - } - - /** - * Registers a directory to use as a fallback for namespaces. - * - * @param string $dir A directory - */ - public function registerNamespaceFallback($dir) - { - $this->namespaceFallbacks[] = $dir; - } - - /** - * Registers directories to use as a fallback for class prefixes. - * - * @param array $dirs An array of directories - */ - public function registerPrefixFallbacks(array $dirs) - { - $this->prefixFallbacks = $dirs; - } - - /** - * Registers a directory to use as a fallback for class prefixes. - * - * @param string $dir A directory - */ - public function registerPrefixFallback($dir) - { - $this->prefixFallbacks[] = $dir; - } - - /** - * Registers an array of namespaces. - * - * @param array $namespaces An array of namespaces (namespaces as keys and locations as values) - */ - public function registerNamespaces(array $namespaces) - { - foreach ($namespaces as $namespace => $locations) { - $this->namespaces[$namespace] = (array) $locations; - } - } - - /** - * Registers a namespace. - * - * @param string $namespace The namespace - * @param array|string $paths The location(s) of the namespace - */ - public function registerNamespace($namespace, $paths) - { - $this->namespaces[$namespace] = (array) $paths; - } - - /** - * Registers an array of classes using the PEAR naming convention. - * - * @param array $classes An array of classes (prefixes as keys and locations as values) - */ - public function registerPrefixes(array $classes) - { - foreach ($classes as $prefix => $locations) { - $this->prefixes[$prefix] = (array) $locations; - } - } - - /** - * Registers a set of classes using the PEAR naming convention. - * - * @param string $prefix The classes prefix - * @param array|string $paths The location(s) of the classes - */ - public function registerPrefix($prefix, $paths) - { - $this->prefixes[$prefix] = (array) $paths; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return bool|null True, if loaded - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - require $file; - - return true; - } - } - - /** - * Finds the path to the file where the class is defined. - * - * @param string $class The name of the class - * - * @return string|null The path, if found - */ - public function findFile($class) - { - if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name - $namespace = substr($class, 0, $pos); - $className = substr($class, $pos + 1); - $normalizedClass = str_replace('\\', DIRECTORY_SEPARATOR, $namespace).DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $className).'.php'; - foreach ($this->namespaces as $ns => $dirs) { - if (0 !== strpos($namespace, $ns)) { - continue; - } - - foreach ($dirs as $dir) { - $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass; - if (is_file($file)) { - return $file; - } - } - } - - foreach ($this->namespaceFallbacks as $dir) { - $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass; - if (is_file($file)) { - return $file; - } - } - } else { - // PEAR-like class name - $normalizedClass = str_replace('_', DIRECTORY_SEPARATOR, $class).'.php'; - foreach ($this->prefixes as $prefix => $dirs) { - if (0 !== strpos($class, $prefix)) { - continue; - } - - foreach ($dirs as $dir) { - $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass; - if (is_file($file)) { - return $file; - } - } - } - - foreach ($this->prefixFallbacks as $dir) { - $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass; - if (is_file($file)) { - return $file; - } - } - } - - if ($this->useIncludePath && $file = stream_resolve_include_path($normalizedClass)) { - return $file; - } - } -} diff --git a/src/Symfony/Component/ClassLoader/WinCacheClassLoader.php b/src/Symfony/Component/ClassLoader/WinCacheClassLoader.php deleted file mode 100644 index 3e077450f1ebc..0000000000000 --- a/src/Symfony/Component/ClassLoader/WinCacheClassLoader.php +++ /dev/null @@ -1,142 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -/** - * WinCacheClassLoader implements a wrapping autoloader cached in WinCache. - * - * It expects an object implementing a findFile method to find the file. This - * allow using it as a wrapper around the other loaders of the component (the - * ClassLoader and the UniversalClassLoader for instance) but also around any - * other autoloaders following this convention (the Composer one for instance). - * - * // with a Symfony autoloader - * use Symfony\Component\ClassLoader\ClassLoader; - * - * $loader = new ClassLoader(); - * $loader->addPrefix('Symfony\Component', __DIR__.'/component'); - * $loader->addPrefix('Symfony', __DIR__.'/framework'); - * - * // or with a Composer autoloader - * use Composer\Autoload\ClassLoader; - * - * $loader = new ClassLoader(); - * $loader->add('Symfony\Component', __DIR__.'/component'); - * $loader->add('Symfony', __DIR__.'/framework'); - * - * $cachedLoader = new WinCacheClassLoader('my_prefix', $loader); - * - * // activate the cached autoloader - * $cachedLoader->register(); - * - * // eventually deactivate the non-cached loader if it was registered previously - * // to be sure to use the cached one. - * $loader->unregister(); - * - * @author Fabien Potencier - * @author Kris Wallsmith - * @author Artem Ryzhkov - */ -class WinCacheClassLoader -{ - private $prefix; - - /** - * A class loader object that implements the findFile() method. - * - * @var object - */ - protected $decorated; - - /** - * Constructor. - * - * @param string $prefix The WinCache namespace prefix to use - * @param object $decorated A class loader object that implements the findFile() method - * - * @throws \RuntimeException - * @throws \InvalidArgumentException - */ - public function __construct($prefix, $decorated) - { - if (!extension_loaded('wincache')) { - throw new \RuntimeException('Unable to use WinCacheClassLoader as WinCache is not enabled.'); - } - - if (!method_exists($decorated, 'findFile')) { - throw new \InvalidArgumentException('The class finder must implement a "findFile" method.'); - } - - $this->prefix = $prefix; - $this->decorated = $decorated; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return bool|null True, if loaded - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - require $file; - - return true; - } - } - - /** - * Finds a file by class name while caching lookups to WinCache. - * - * @param string $class A class name to resolve to file - * - * @return string|null - */ - public function findFile($class) - { - $file = wincache_ucache_get($this->prefix.$class, $success); - - if (!$success) { - wincache_ucache_set($this->prefix.$class, $file = $this->decorated->findFile($class) ?: null, 0); - } - - return $file; - } - - /** - * Passes through all unknown calls onto the decorated object. - */ - public function __call($method, $args) - { - return call_user_func_array(array($this->decorated, $method), $args); - } -} diff --git a/src/Symfony/Component/ClassLoader/XcacheClassLoader.php b/src/Symfony/Component/ClassLoader/XcacheClassLoader.php deleted file mode 100644 index aa4dc9d052b9f..0000000000000 --- a/src/Symfony/Component/ClassLoader/XcacheClassLoader.php +++ /dev/null @@ -1,143 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ClassLoader; - -/** - * XcacheClassLoader implements a wrapping autoloader cached in XCache for PHP 5.3. - * - * It expects an object implementing a findFile method to find the file. This - * allows using it as a wrapper around the other loaders of the component (the - * ClassLoader and the UniversalClassLoader for instance) but also around any - * other autoloaders following this convention (the Composer one for instance). - * - * // with a Symfony autoloader - * use Symfony\Component\ClassLoader\ClassLoader; - * - * $loader = new ClassLoader(); - * $loader->addPrefix('Symfony\Component', __DIR__.'/component'); - * $loader->addPrefix('Symfony', __DIR__.'/framework'); - * - * // or with a Composer autoloader - * use Composer\Autoload\ClassLoader; - * - * $loader = new ClassLoader(); - * $loader->add('Symfony\Component', __DIR__.'/component'); - * $loader->add('Symfony', __DIR__.'/framework'); - * - * $cachedLoader = new XcacheClassLoader('my_prefix', $loader); - * - * // activate the cached autoloader - * $cachedLoader->register(); - * - * // eventually deactivate the non-cached loader if it was registered previously - * // to be sure to use the cached one. - * $loader->unregister(); - * - * @author Fabien Potencier - * @author Kris Wallsmith - * @author Kim Hemsø Rasmussen - */ -class XcacheClassLoader -{ - private $prefix; - - /** - * A class loader object that implements the findFile() method. - * - * @var object - */ - private $decorated; - - /** - * Constructor. - * - * @param string $prefix The XCache namespace prefix to use - * @param object $decorated A class loader object that implements the findFile() method - * - * @throws \RuntimeException - * @throws \InvalidArgumentException - */ - public function __construct($prefix, $decorated) - { - if (!extension_loaded('xcache')) { - throw new \RuntimeException('Unable to use XcacheClassLoader as XCache is not enabled.'); - } - - if (!method_exists($decorated, 'findFile')) { - throw new \InvalidArgumentException('The class finder must implement a "findFile" method.'); - } - - $this->prefix = $prefix; - $this->decorated = $decorated; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return bool|null True, if loaded - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - require $file; - - return true; - } - } - - /** - * Finds a file by class name while caching lookups to Xcache. - * - * @param string $class A class name to resolve to file - * - * @return string|null - */ - public function findFile($class) - { - if (xcache_isset($this->prefix.$class)) { - $file = xcache_get($this->prefix.$class); - } else { - $file = $this->decorated->findFile($class) ?: null; - xcache_set($this->prefix.$class, $file); - } - - return $file; - } - - /** - * Passes through all unknown calls onto the decorated object. - */ - public function __call($method, $args) - { - return call_user_func_array(array($this->decorated, $method), $args); - } -} diff --git a/src/Symfony/Component/ClassLoader/composer.json b/src/Symfony/Component/ClassLoader/composer.json deleted file mode 100644 index 6f306d9712ba7..0000000000000 --- a/src/Symfony/Component/ClassLoader/composer.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "symfony/class-loader", - "type": "library", - "description": "Symfony ClassLoader Component", - "keywords": [], - "homepage": "https://symfony.com", - "license": "MIT", - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "minimum-stability": "dev", - "require": { - "php": ">=5.3.9", - "symfony/polyfill-apcu": "~1.1" - }, - "require-dev": { - "symfony/finder": "^2.0.5|~3.0.0" - }, - "autoload": { - "psr-4": { "Symfony\\Component\\ClassLoader\\": "" }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - } -} diff --git a/src/Symfony/Component/ClassLoader/phpunit.xml.dist b/src/Symfony/Component/ClassLoader/phpunit.xml.dist deleted file mode 100644 index 5158b22f27c88..0000000000000 --- a/src/Symfony/Component/ClassLoader/phpunit.xml.dist +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - ./Tests/ - - - - - - ./ - - ./Resources - ./Tests - ./vendor - - - - diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index 8e8e8335534ba..4ef4e625ba566 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -1,6 +1,23 @@ CHANGELOG ========= +3.3.0 +----- + + * added `ReflectionClassResource` class + * added second `$exists` constructor argument to `ClassExistenceResource` + * made `ClassExistenceResource` work with interfaces and traits + * added `ConfigCachePass` (originally in FrameworkBundle) + * added `castToArray()` helper to turn any config value into an array + +3.0.0 +----- + + * removed `ReferenceDumper` class + * removed the `ResourceInterface::isFresh()` method + * removed `BCResourceInterfaceChecker` class + * removed `ResourceInterface::getResource()` method + 2.8.0 ----- @@ -20,7 +37,7 @@ Before: `InvalidArgumentException` (variable must contain at least two distinct elements). After: the code will work as expected and it will restrict the values of the `variable` option to just `value`. - + * deprecated the `ResourceInterface::isFresh()` method. If you implement custom resource types and they can be validated that way, make them implement the new `SelfCheckingResourceInterface`. * deprecated the getResource() method in ResourceInterface. You can still call this method @@ -33,16 +50,16 @@ After: the code will work as expected and it will restrict the values of the * added `ConfigCacheInterface`, `ConfigCacheFactoryInterface` and a basic `ConfigCacheFactory` implementation to delegate creation of ConfigCache instances - + 2.2.0 ----- - * added ArrayNodeDefinition::canBeEnabled() and ArrayNodeDefinition::canBeDisabled() + * added `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()` to ease configuration when some sections are respectively disabled / enabled by default. * added a `normalizeKeys()` method for array nodes (to avoid key normalization) * added numerical type handling for config definitions - * added convenience methods for optional configuration sections to ArrayNodeDefinition + * added convenience methods for optional configuration sections to `ArrayNodeDefinition` * added a utils class for XML manipulations 2.1.0 @@ -50,5 +67,5 @@ After: the code will work as expected and it will restrict the values of the * added a way to add documentation on configuration * implemented `Serializable` on resources - * LoaderResolverInterface is now used instead of LoaderResolver for type + * `LoaderResolverInterface` is now used instead of `LoaderResolver` for type hinting diff --git a/src/Symfony/Component/Config/ConfigCache.php b/src/Symfony/Component/Config/ConfigCache.php index 5ede07b55230b..591c89bc4ff02 100644 --- a/src/Symfony/Component/Config/ConfigCache.php +++ b/src/Symfony/Component/Config/ConfigCache.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Config; -use Symfony\Component\Config\Resource\BCResourceInterfaceChecker; use Symfony\Component\Config\Resource\SelfCheckingResourceChecker; /** @@ -21,11 +20,6 @@ * \Symfony\Component\Config\Resource\SelfCheckingResourceInterface will * be used to check cache freshness. * - * During a transition period, also instances of - * \Symfony\Component\Config\Resource\ResourceInterface will be checked - * by means of the isFresh() method. This behaviour is deprecated since 2.8 - * and will be removed in 3.0. - * * @author Fabien Potencier * @author Matthias Pigulla */ @@ -39,25 +33,14 @@ class ConfigCache extends ResourceCheckerConfigCache */ public function __construct($file, $debug) { - parent::__construct($file, array( - new SelfCheckingResourceChecker(), - new BCResourceInterfaceChecker(), - )); $this->debug = (bool) $debug; - } - /** - * Gets the cache file path. - * - * @return string The cache file path - * - * @deprecated since 2.7, to be removed in 3.0. Use getPath() instead. - */ - public function __toString() - { - @trigger_error('ConfigCache::__toString() is deprecated since version 2.7 and will be removed in 3.0. Use the getPath() method instead.', E_USER_DEPRECATED); + $checkers = array(); + if (true === $this->debug) { + $checkers = array(new SelfCheckingResourceChecker()); + } - return $this->getPath(); + parent::__construct($file, $checkers); } /** diff --git a/src/Symfony/Component/Config/Definition/ArrayNode.php b/src/Symfony/Component/Config/Definition/ArrayNode.php index 8320f4aac315e..457e7a8c92e34 100644 --- a/src/Symfony/Component/Config/Definition/ArrayNode.php +++ b/src/Symfony/Component/Config/Definition/ArrayNode.php @@ -332,9 +332,7 @@ protected function normalizeValue($value) */ protected function remapXml($value) { - foreach ($this->xmlRemappings as $transformation) { - list($singular, $plural) = $transformation; - + foreach ($this->xmlRemappings as list($singular, $plural)) { if (!isset($value[$singular])) { continue; } diff --git a/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php index a1fc1fab0ef78..cac53044a9a10 100644 --- a/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php @@ -79,6 +79,62 @@ public function prototype($type) return $this->prototype = $this->getNodeBuilder()->node(null, $type)->setParent($this); } + /** + * @return VariableNodeDefinition + */ + public function variablePrototype() + { + return $this->prototype('variable'); + } + + /** + * @return ScalarNodeDefinition + */ + public function scalarPrototype() + { + return $this->prototype('scalar'); + } + + /** + * @return BooleanNodeDefinition + */ + public function booleanPrototype() + { + return $this->prototype('boolean'); + } + + /** + * @return IntegerNodeDefinition + */ + public function integerPrototype() + { + return $this->prototype('integer'); + } + + /** + * @return FloatNodeDefinition + */ + public function floatPrototype() + { + return $this->prototype('float'); + } + + /** + * @return ArrayNodeDefinition + */ + public function arrayPrototype() + { + return $this->prototype('array'); + } + + /** + * @return EnumNodeDefinition + */ + public function enumPrototype() + { + return $this->prototype('enum'); + } + /** * Adds the default value if the node is not set in the configuration. * @@ -372,7 +428,7 @@ protected function createNode() $node->setKeyAttribute($this->key, $this->removeKeyItem); } - if (true === $this->atLeastOne) { + if (true === $this->atLeastOne || false === $this->allowEmptyValue) { $node->setMinNumberOfElements(1); } @@ -434,6 +490,12 @@ protected function validateConcreteNode(ArrayNode $node) ); } + if (false === $this->allowEmptyValue) { + throw new InvalidDefinitionException( + sprintf('->cannotBeEmpty() is not applicable to concrete nodes at path "%s"', $path) + ); + } + if (true === $this->atLeastOne) { throw new InvalidDefinitionException( sprintf('->requiresAtLeastOneElement() is not applicable to concrete nodes at path "%s"', $path) diff --git a/src/Symfony/Component/Config/Definition/Builder/BooleanNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/BooleanNodeDefinition.php index 7f8eb2681d9e9..28e56579ada52 100644 --- a/src/Symfony/Component/Config/Definition/Builder/BooleanNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/BooleanNodeDefinition.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\BooleanNode; +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; /** * This class provides a fluent interface for defining a node. @@ -31,24 +32,22 @@ public function __construct($name, NodeParentInterface $parent = null) } /** - * {@inheritdoc} + * Instantiate a Node. * - * @deprecated Deprecated since version 2.8, to be removed in 3.0. + * @return BooleanNode The node */ - public function cannotBeEmpty() + protected function instantiateNode() { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - - return parent::cannotBeEmpty(); + return new BooleanNode($this->name, $this->parent); } /** - * Instantiate a Node. + * {@inheritdoc} * - * @return BooleanNode The node + * @throws InvalidDefinitionException */ - protected function instantiateNode() + public function cannotBeEmpty() { - return new BooleanNode($this->name, $this->parent); + throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to BooleanNodeDefinition.'); } } diff --git a/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php b/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php index 10112a813df7a..bc83b760e810d 100644 --- a/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php +++ b/src/Symfony/Component/Config/Definition/Builder/ExprBuilder.php @@ -97,6 +97,18 @@ public function ifNull() return $this; } + /** + * Tests if the value is empty. + * + * @return ExprBuilder + */ + public function ifEmpty() + { + $this->ifPart = function ($v) { return empty($v); }; + + return $this; + } + /** * Tests if the value is an array. * @@ -137,6 +149,19 @@ public function ifNotInArray(array $array) return $this; } + /** + * Transforms variables of any type into an array. + * + * @return $this + */ + public function castToArray() + { + $this->ifPart = function ($v) { return !is_array($v); }; + $this->thenPart = function ($v) { return array($v); }; + + return $this; + } + /** * Sets the closure to run if the test pass. * diff --git a/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php index 70874d62de883..0d0207ca4fc79 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Config\Definition\Builder; +use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; + /** * Abstract class that contains common code of integer and float node definitions. * @@ -62,12 +64,10 @@ public function min($min) /** * {@inheritdoc} * - * @deprecated Deprecated since version 2.8, to be removed in 3.0. + * @throws InvalidDefinitionException */ public function cannotBeEmpty() { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - - return parent::cannotBeEmpty(); + throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to NumericNodeDefinition.'); } } diff --git a/src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php b/src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php index 2cc71b344e35f..ec5460f2f53c7 100644 --- a/src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php +++ b/src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php @@ -96,7 +96,10 @@ private function writeNode(NodeInterface $node, $depth = 0, $root = false, $name $rootAttributes[$key] = str_replace('-', ' ', $rootName).' '.$key; } - if ($prototype instanceof ArrayNode) { + if ($prototype instanceof PrototypedArrayNode) { + $prototype->setName($key); + $children = array($key => $prototype); + } elseif ($prototype instanceof ArrayNode) { $children = $prototype->getChildren(); } else { if ($prototype->hasDefaultValue()) { diff --git a/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php b/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php index 31c3f8e0161fc..16076354db515 100644 --- a/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php +++ b/src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php @@ -16,6 +16,7 @@ use Symfony\Component\Config\Definition\ArrayNode; use Symfony\Component\Config\Definition\EnumNode; use Symfony\Component\Config\Definition\PrototypedArrayNode; +use Symfony\Component\Config\Definition\ScalarNode; use Symfony\Component\Yaml\Inline; /** @@ -32,6 +33,32 @@ public function dump(ConfigurationInterface $configuration) return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree()); } + public function dumpAtPath(ConfigurationInterface $configuration, $path) + { + $rootNode = $node = $configuration->getConfigTreeBuilder()->buildTree(); + + foreach (explode('.', $path) as $step) { + if (!$node instanceof ArrayNode) { + throw new \UnexpectedValueException(sprintf('Unable to find node at path "%s.%s"', $rootNode->getName(), $path)); + } + + /** @var NodeInterface[] $children */ + $children = $node instanceof PrototypedArrayNode ? $this->getPrototypeChildren($node) : $node->getChildren(); + + foreach ($children as $child) { + if ($child->getName() === $step) { + $node = $child; + + continue 2; + } + } + + throw new \UnexpectedValueException(sprintf('Unable to find node at path "%s.%s"', $rootNode->getName(), $path)); + } + + return $this->dumpNode($node); + } + public function dumpNode(NodeInterface $node) { $this->reference = ''; @@ -45,8 +72,9 @@ public function dumpNode(NodeInterface $node) /** * @param NodeInterface $node * @param int $depth + * @param bool $prototypedArray */ - private function writeNode(NodeInterface $node, $depth = 0) + private function writeNode(NodeInterface $node, $depth = 0, $prototypedArray = false) { $comments = array(); $default = ''; @@ -59,29 +87,7 @@ private function writeNode(NodeInterface $node, $depth = 0) $children = $node->getChildren(); if ($node instanceof PrototypedArrayNode) { - $prototype = $node->getPrototype(); - - if ($prototype instanceof ArrayNode) { - $children = $prototype->getChildren(); - } - - // check for attribute as key - if ($key = $node->getKeyAttribute()) { - $keyNodeClass = 'Symfony\Component\Config\Definition\\'.($prototype instanceof ArrayNode ? 'ArrayNode' : 'ScalarNode'); - $keyNode = new $keyNodeClass($key, $node); - - $info = 'Prototype'; - if (null !== $prototype->getInfo()) { - $info .= ': '.$prototype->getInfo(); - } - $keyNode->setInfo($info); - - // add children - foreach ($children as $childNode) { - $keyNode->addChild($childNode); - } - $children = array($key => $keyNode); - } + $children = $this->getPrototypeChildren($node); } if (!$children) { @@ -125,7 +131,8 @@ private function writeNode(NodeInterface $node, $depth = 0) $default = (string) $default != '' ? ' '.$default : ''; $comments = count($comments) ? '# '.implode(', ', $comments) : ''; - $text = rtrim(sprintf('%-21s%s %s', $node->getName().':', $default, $comments), ' '); + $key = $prototypedArray ? '-' : $node->getName().':'; + $text = rtrim(sprintf('%-21s%s %s', $key, $default, $comments), ' '); if ($info = $node->getInfo()) { $this->writeLine(''); @@ -159,7 +166,7 @@ private function writeNode(NodeInterface $node, $depth = 0) if ($children) { foreach ($children as $childNode) { - $this->writeNode($childNode, $depth + 1); + $this->writeNode($childNode, $depth + 1, $node instanceof PrototypedArrayNode && !$node->getKeyAttribute()); } } } @@ -200,4 +207,44 @@ private function writeArray(array $array, $depth) } } } + + /** + * @param PrototypedArrayNode $node + * + * @return array + */ + private function getPrototypeChildren(PrototypedArrayNode $node) + { + $prototype = $node->getPrototype(); + $key = $node->getKeyAttribute(); + + // Do not expand prototype if it isn't an array node nor uses attribute as key + if (!$key && !$prototype instanceof ArrayNode) { + return $node->getChildren(); + } + + if ($prototype instanceof ArrayNode) { + $keyNode = new ArrayNode($key, $node); + $children = $prototype->getChildren(); + + if ($prototype instanceof PrototypedArrayNode && $prototype->getKeyAttribute()) { + $children = $this->getPrototypeChildren($prototype); + } + + // add children + foreach ($children as $childNode) { + $keyNode->addChild($childNode); + } + } else { + $keyNode = new ScalarNode($key, $node); + } + + $info = 'Prototype'; + if (null !== $prototype->getInfo()) { + $info .= ': '.$prototype->getInfo(); + } + $keyNode->setInfo($info); + + return array($key => $keyNode); + } } diff --git a/src/Symfony/Component/Config/Definition/ReferenceDumper.php b/src/Symfony/Component/Config/Definition/ReferenceDumper.php deleted file mode 100644 index 09526cfe07ba8..0000000000000 --- a/src/Symfony/Component/Config/Definition/ReferenceDumper.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Config\Definition; - -@trigger_error('The '.__NAMESPACE__.'\ReferenceDumper class is deprecated since version 2.4 and will be removed in 3.0. Use the Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper class instead.', E_USER_DEPRECATED); - -use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; - -/** - * @deprecated since version 2.4, to be removed in 3.0. - * Use {@link \Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper} instead. - */ -class ReferenceDumper extends YamlReferenceDumper -{ -} diff --git a/src/Symfony/Component/Config/DependencyInjection/ConfigCachePass.php b/src/Symfony/Component/Config/DependencyInjection/ConfigCachePass.php new file mode 100644 index 0000000000000..02cae0d2b2b65 --- /dev/null +++ b/src/Symfony/Component/Config/DependencyInjection/ConfigCachePass.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Adds services tagged config_cache.resource_checker to the config_cache_factory service, ordering them by priority. + * + * @author Matthias Pigulla + * @author Benjamin Klotz + */ +class ConfigCachePass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + private $factoryServiceId; + private $resourceCheckerTag; + + public function __construct($factoryServiceId = 'config_cache_factory', $resourceCheckerTag = 'config_cache.resource_checker') + { + $this->factoryServiceId = $factoryServiceId; + $this->resourceCheckerTag = $resourceCheckerTag; + } + + public function process(ContainerBuilder $container) + { + $resourceCheckers = $this->findAndSortTaggedServices($this->resourceCheckerTag, $container); + + if (empty($resourceCheckers)) { + return; + } + + $container->getDefinition($this->factoryServiceId)->replaceArgument(0, new IteratorArgument($resourceCheckers)); + } +} diff --git a/src/Symfony/Component/Config/Exception/FileLoaderLoadException.php b/src/Symfony/Component/Config/Exception/FileLoaderLoadException.php index 6af3dd0a6d618..564f75ce60b8c 100644 --- a/src/Symfony/Component/Config/Exception/FileLoaderLoadException.php +++ b/src/Symfony/Component/Config/Exception/FileLoaderLoadException.php @@ -23,8 +23,9 @@ class FileLoaderLoadException extends \Exception * @param string $sourceResource The original resource importing the new resource * @param int $code The error code * @param \Exception $previous A previous exception + * @param string $type The type of resource */ - public function __construct($resource, $sourceResource = null, $code = null, $previous = null) + public function __construct($resource, $sourceResource = null, $code = null, $previous = null, $type = null) { $message = ''; if ($previous) { @@ -60,6 +61,13 @@ public function __construct($resource, $sourceResource = null, $code = null, $pr $bundle = substr($parts[0], 1); $message .= sprintf(' Make sure the "%s" bundle is correctly registered and loaded in the application kernel class.', $bundle); $message .= sprintf(' If the bundle is registered, make sure the bundle path "%s" is not empty.', $resource); + } elseif (null !== $type) { + // maybe there is no loader for this specific type + if ('annotation' === $type) { + $message .= ' Make sure annotations are enabled.'; + } else { + $message .= sprintf(' Make sure there is a loader supporting the "%s" type.', $type); + } } parent::__construct($message, $code, $previous); diff --git a/src/Symfony/Component/Config/Exception/FileLocatorFileNotFoundException.php b/src/Symfony/Component/Config/Exception/FileLocatorFileNotFoundException.php new file mode 100644 index 0000000000000..af764eb4718d8 --- /dev/null +++ b/src/Symfony/Component/Config/Exception/FileLocatorFileNotFoundException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Exception; + +/** + * File locator exception if a file does not exist. + * + * @author Leo Feyer + */ +class FileLocatorFileNotFoundException extends \InvalidArgumentException +{ + private $paths; + + public function __construct($message = '', $code = 0, $previous = null, array $paths = array()) + { + parent::__construct($message, $code, $previous); + + $this->paths = $paths; + } + + public function getPaths() + { + return $this->paths; + } +} diff --git a/src/Symfony/Component/Config/FileLocator.php b/src/Symfony/Component/Config/FileLocator.php index 4816724c8190e..16bb5d48d7383 100644 --- a/src/Symfony/Component/Config/FileLocator.php +++ b/src/Symfony/Component/Config/FileLocator.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Config; +use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; + /** * FileLocator uses an array of pre-defined paths to find files. * @@ -41,7 +43,7 @@ public function locate($name, $currentPath = null, $first = true) if ($this->isAbsolutePath($name)) { if (!file_exists($name)) { - throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $name)); + throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist.', $name), 0, null, array($name)); } return $name; @@ -54,7 +56,7 @@ public function locate($name, $currentPath = null, $first = true) } $paths = array_unique($paths); - $filepaths = array(); + $filepaths = $notfound = array(); foreach ($paths as $path) { if (@file_exists($file = $path.DIRECTORY_SEPARATOR.$name)) { @@ -62,11 +64,13 @@ public function locate($name, $currentPath = null, $first = true) return $file; } $filepaths[] = $file; + } else { + $notfound[] = $file; } } if (!$filepaths) { - throw new \InvalidArgumentException(sprintf('The file "%s" does not exist (in: %s).', $name, implode(', ', $paths))); + throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist (in: %s).', $name, implode(', ', $paths)), 0, null, $notfound); } return $filepaths; diff --git a/src/Symfony/Component/Config/FileLocatorInterface.php b/src/Symfony/Component/Config/FileLocatorInterface.php index 66057982db893..cf3c2e594df85 100644 --- a/src/Symfony/Component/Config/FileLocatorInterface.php +++ b/src/Symfony/Component/Config/FileLocatorInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Config; +use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; + /** * @author Fabien Potencier */ @@ -25,7 +27,8 @@ interface FileLocatorInterface * * @return string|array The full path to the file or an array of file paths * - * @throws \InvalidArgumentException When file is not found + * @throws \InvalidArgumentException If $name is empty + * @throws FileLocatorFileNotFoundException If a file is not found */ public function locate($name, $currentPath = null, $first = true); } diff --git a/src/Symfony/Component/Config/Loader/DelegatingLoader.php b/src/Symfony/Component/Config/Loader/DelegatingLoader.php index 3097878bf0bf2..23b625652af2d 100644 --- a/src/Symfony/Component/Config/Loader/DelegatingLoader.php +++ b/src/Symfony/Component/Config/Loader/DelegatingLoader.php @@ -39,7 +39,7 @@ public function __construct(LoaderResolverInterface $resolver) public function load($resource, $type = null) { if (false === $loader = $this->resolver->resolve($resource, $type)) { - throw new FileLoaderLoadException($resource); + throw new FileLoaderLoadException($resource, null, null, null, $type); } return $loader->load($resource, $type); diff --git a/src/Symfony/Component/Config/Loader/FileLoader.php b/src/Symfony/Component/Config/Loader/FileLoader.php index 2b19b52584bdd..02aa811fc5497 100644 --- a/src/Symfony/Component/Config/Loader/FileLoader.php +++ b/src/Symfony/Component/Config/Loader/FileLoader.php @@ -14,6 +14,9 @@ use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Config\Exception\FileLoaderLoadException; use Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException; +use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; +use Symfony\Component\Config\Resource\FileExistenceResource; +use Symfony\Component\Config\Resource\GlobResource; /** * FileLoader is the abstract class used by all built-in loaders that are file based. @@ -76,23 +79,72 @@ public function getLocator() * * @throws FileLoaderLoadException * @throws FileLoaderImportCircularReferenceException + * @throws FileLocatorFileNotFoundException */ public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null) + { + if (is_string($resource) && strlen($resource) !== $i = strcspn($resource, '*?{[')) { + $ret = array(); + $isSubpath = 0 !== $i && false !== strpos(substr($resource, 0, $i), '/'); + foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath) as $path => $info) { + if (null !== $res = $this->doImport($path, $type, $ignoreErrors, $sourceResource)) { + $ret[] = $res; + } + $isSubpath = true; + } + + if ($isSubpath) { + return isset($ret[1]) ? $ret : (isset($ret[0]) ? $ret[0] : null); + } + } + + return $this->doImport($resource, $type, $ignoreErrors, $sourceResource); + } + + /** + * @internal + */ + protected function glob($pattern, $recursive, &$resource = null, $ignoreErrors = false) + { + if (strlen($pattern) === $i = strcspn($pattern, '*?{[')) { + $prefix = $pattern; + $pattern = ''; + } elseif (0 === $i || false === strpos(substr($pattern, 0, $i), '/')) { + $prefix = '.'; + $pattern = '/'.$pattern; + } else { + $prefix = dirname(substr($pattern, 0, 1 + $i)); + $pattern = substr($pattern, strlen($prefix)); + } + + try { + $prefix = $this->locator->locate($prefix, $this->currentDir, true); + } catch (FileLocatorFileNotFoundException $e) { + if (!$ignoreErrors) { + throw $e; + } + + $resource = array(); + foreach ($e->getPaths() as $path) { + $resource[] = new FileExistenceResource($path); + } + + return; + } + $resource = new GlobResource($prefix, $pattern, $recursive); + + foreach ($resource as $path => $info) { + yield $path => $info; + } + } + + private function doImport($resource, $type = null, $ignoreErrors = false, $sourceResource = null) { try { $loader = $this->resolve($resource, $type); if ($loader instanceof self && null !== $this->currentDir) { - // we fallback to the current locator to keep BC - // as some some loaders do not call the parent __construct() - // @deprecated should be removed in 3.0 - $locator = $loader->getLocator(); - if (null === $locator) { - @trigger_error('Not calling the parent constructor in '.get_class($loader).' which extends '.__CLASS__.' is deprecated since version 2.7 and will not be supported anymore in 3.0.', E_USER_DEPRECATED); - $locator = $this->locator; - } - - $resource = $locator->locate($resource, $this->currentDir, false); + $resource = $loader->getLocator()->locate($resource, $this->currentDir, false); } $resources = is_array($resource) ? $resource : array($resource); @@ -110,16 +162,10 @@ public function import($resource, $type = null, $ignoreErrors = false, $sourceRe try { $ret = $loader->load($resource, $type); - } catch (\Exception $e) { + } finally { unset(self::$loading[$resource]); - throw $e; - } catch (\Throwable $e) { - unset(self::$loading[$resource]); - throw $e; } - unset(self::$loading[$resource]); - return $ret; } catch (FileLoaderImportCircularReferenceException $e) { throw $e; @@ -130,7 +176,7 @@ public function import($resource, $type = null, $ignoreErrors = false, $sourceRe throw $e; } - throw new FileLoaderLoadException($resource, $sourceResource, null, $e); + throw new FileLoaderLoadException($resource, $sourceResource, null, $e, $type); } } } diff --git a/src/Symfony/Component/Config/Loader/GlobFileLoader.php b/src/Symfony/Component/Config/Loader/GlobFileLoader.php new file mode 100644 index 0000000000000..f432b45ba4fe8 --- /dev/null +++ b/src/Symfony/Component/Config/Loader/GlobFileLoader.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Loader; + +/** + * GlobFileLoader loads files from a glob pattern. + * + * @author Fabien Potencier + */ +class GlobFileLoader extends FileLoader +{ + /** + * {@inheritdoc} + */ + public function load($resource, $type = null) + { + return $this->import($resource); + } + + /** + * {@inheritdoc} + */ + public function supports($resource, $type = null) + { + return 'glob' === $type; + } +} diff --git a/src/Symfony/Component/Config/Loader/Loader.php b/src/Symfony/Component/Config/Loader/Loader.php index a6f8d9c66454c..d2f2ec90b9b03 100644 --- a/src/Symfony/Component/Config/Loader/Loader.php +++ b/src/Symfony/Component/Config/Loader/Loader.php @@ -70,7 +70,7 @@ public function resolve($resource, $type = null) $loader = null === $this->resolver ? false : $this->resolver->resolve($resource, $type); if (false === $loader) { - throw new FileLoaderLoadException($resource); + throw new FileLoaderLoadException($resource, null, null, null, $type); } return $loader; diff --git a/src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php b/src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php deleted file mode 100644 index 565ff8bb50e8b..0000000000000 --- a/src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Config\Resource; - -/** - * Resource checker for the ResourceInterface. Exists for BC. - * - * @author Matthias Pigulla - * - * @deprecated since 2.8, to be removed in 3.0. - */ -class BCResourceInterfaceChecker extends SelfCheckingResourceChecker -{ - public function supports(ResourceInterface $metadata) - { - /* As all resources must be instanceof ResourceInterface, - we support them all. */ - return true; - } - - public function isFresh(ResourceInterface $resource, $timestamp) - { - @trigger_error(sprintf('The class "%s" is performing resource checking through ResourceInterface::isFresh(), which is deprecated since 2.8 and will be removed in 3.0', get_class($resource)), E_USER_DEPRECATED); - - return parent::isFresh($resource, $timestamp); // For now, $metadata features the isFresh() method, so off we go (quack quack) - } -} diff --git a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php new file mode 100644 index 0000000000000..e3fd095b6008d --- /dev/null +++ b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * ClassExistenceResource represents a class existence. + * Freshness is only evaluated against resource existence. + * + * The resource must be a fully-qualified class name. + * + * @author Fabien Potencier + */ +class ClassExistenceResource implements SelfCheckingResourceInterface, \Serializable +{ + private $resource; + private $exists; + + private static $autoloadLevel = 0; + private static $autoloadedClass; + private static $existsCache = array(); + + /** + * @param string $resource The fully-qualified class name + * @param bool|null $exists Boolean when the existency check has already been done + */ + public function __construct($resource, $exists = null) + { + $this->resource = $resource; + if (null !== $exists) { + $this->exists = (bool) $exists; + } + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->resource; + } + + /** + * @return string The file path to the resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * {@inheritdoc} + * + * @throws \ReflectionException when a parent class/interface/trait is not found + */ + public function isFresh($timestamp) + { + $loaded = class_exists($this->resource, false) || interface_exists($this->resource, false) || trait_exists($this->resource, false); + + if (null !== $exists = &self::$existsCache[(int) (0 >= $timestamp)][$this->resource]) { + $exists = $exists || $loaded; + } elseif (!$exists = $loaded) { + if (!self::$autoloadLevel++) { + spl_autoload_register(__CLASS__.'::throwOnRequiredClass'); + } + $autoloadedClass = self::$autoloadedClass; + self::$autoloadedClass = $this->resource; + + try { + $exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false); + } catch (\ReflectionException $e) { + if (0 >= $timestamp) { + unset(self::$existsCache[1][$this->resource]); + throw $e; + } + } finally { + self::$autoloadedClass = $autoloadedClass; + if (!--self::$autoloadLevel) { + spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass'); + } + } + } + + if (null === $this->exists) { + $this->exists = $exists; + } + + return $this->exists xor !$exists; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + if (null === $this->exists) { + $this->isFresh(0); + } + + return serialize(array($this->resource, $this->exists)); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list($this->resource, $this->exists) = unserialize($serialized); + } + + /** + * @throws \ReflectionException When $class is not found and is required + */ + private static function throwOnRequiredClass($class) + { + if (self::$autoloadedClass === $class) { + return; + } + $e = new \ReflectionException("Class $class not found"); + $trace = $e->getTrace(); + $autoloadFrame = array( + 'function' => 'spl_autoload_call', + 'args' => array($class), + ); + $i = 1 + array_search($autoloadFrame, $trace, true); + + if (isset($trace[$i]['function']) && !isset($trace[$i]['class'])) { + switch ($trace[$i]['function']) { + case 'get_class_methods': + case 'get_class_vars': + case 'get_parent_class': + case 'is_a': + case 'is_subclass_of': + case 'class_exists': + case 'class_implements': + case 'class_parents': + case 'trait_exists': + case 'defined': + case 'interface_exists': + case 'method_exists': + case 'property_exists': + case 'is_callable': + return; + } + + $props = array( + 'file' => $trace[$i]['file'], + 'line' => $trace[$i]['line'], + 'trace' => array_slice($trace, 1 + $i), + ); + + foreach ($props as $p => $v) { + $r = new \ReflectionProperty('Exception', $p); + $r->setAccessible(true); + $r->setValue($e, $v); + } + } + + throw $e; + } +} diff --git a/src/Symfony/Component/Config/Resource/ComposerResource.php b/src/Symfony/Component/Config/Resource/ComposerResource.php new file mode 100644 index 0000000000000..64288ea1db2d6 --- /dev/null +++ b/src/Symfony/Component/Config/Resource/ComposerResource.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * ComposerResource tracks the PHP version and Composer dependencies. + * + * @author Nicolas Grekas + */ +class ComposerResource implements SelfCheckingResourceInterface, \Serializable +{ + private $vendors; + + private static $runtimeVendors; + + public function __construct() + { + self::refresh(); + $this->vendors = self::$runtimeVendors; + } + + public function getVendors() + { + return array_keys($this->vendors); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return __CLASS__; + } + + /** + * {@inheritdoc} + */ + public function isFresh($timestamp) + { + self::refresh(); + + return self::$runtimeVendors === $this->vendors; + } + + public function serialize() + { + return serialize($this->vendors); + } + + public function unserialize($serialized) + { + $this->vendors = unserialize($serialized); + } + + private static function refresh() + { + self::$runtimeVendors = array(); + + foreach (get_declared_classes() as $class) { + if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $v = dirname(dirname($r->getFileName())); + if (file_exists($v.'/composer/installed.json')) { + self::$runtimeVendors[$v] = @filemtime($v.'/composer/installed.json'); + } + } + } + } +} diff --git a/src/Symfony/Component/Config/Resource/DirectoryResource.php b/src/Symfony/Component/Config/Resource/DirectoryResource.php index ebc930c090742..07280b4b877f2 100644 --- a/src/Symfony/Component/Config/Resource/DirectoryResource.php +++ b/src/Symfony/Component/Config/Resource/DirectoryResource.php @@ -26,11 +26,17 @@ class DirectoryResource implements SelfCheckingResourceInterface, \Serializable * * @param string $resource The file path to the resource * @param string|null $pattern A pattern to restrict monitored files + * + * @throws \InvalidArgumentException */ public function __construct($resource, $pattern = null) { - $this->resource = $resource; + $this->resource = realpath($resource) ?: (file_exists($resource) ? $resource : false); $this->pattern = $pattern; + + if (false === $this->resource || !is_dir($this->resource)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $resource)); + } } /** @@ -42,7 +48,7 @@ public function __toString() } /** - * {@inheritdoc} + * @return string The file path to the resource */ public function getResource() { diff --git a/src/Symfony/Component/Config/Resource/FileExistenceResource.php b/src/Symfony/Component/Config/Resource/FileExistenceResource.php index ba1584638186b..349402edf0494 100644 --- a/src/Symfony/Component/Config/Resource/FileExistenceResource.php +++ b/src/Symfony/Component/Config/Resource/FileExistenceResource.php @@ -45,7 +45,7 @@ public function __toString() } /** - * {@inheritdoc} + * @return string The file path to the resource */ public function getResource() { diff --git a/src/Symfony/Component/Config/Resource/FileResource.php b/src/Symfony/Component/Config/Resource/FileResource.php index 00b2957ca5191..6ad130518854c 100644 --- a/src/Symfony/Component/Config/Resource/FileResource.php +++ b/src/Symfony/Component/Config/Resource/FileResource.php @@ -29,10 +29,16 @@ class FileResource implements SelfCheckingResourceInterface, \Serializable * Constructor. * * @param string $resource The file path to the resource + * + * @throws \InvalidArgumentException */ public function __construct($resource) { $this->resource = realpath($resource) ?: (file_exists($resource) ? $resource : false); + + if (false === $this->resource) { + throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $resource)); + } } /** @@ -40,11 +46,11 @@ public function __construct($resource) */ public function __toString() { - return (string) $this->resource; + return $this->resource; } /** - * {@inheritdoc} + * @return string The canonicalized, absolute path to the resource */ public function getResource() { @@ -56,11 +62,7 @@ public function getResource() */ public function isFresh($timestamp) { - if (false === $this->resource || !file_exists($this->resource)) { - return false; - } - - return filemtime($this->resource) <= $timestamp; + return file_exists($this->resource) && @filemtime($this->resource) <= $timestamp; } public function serialize() diff --git a/src/Symfony/Component/Config/Resource/GlobResource.php b/src/Symfony/Component/Config/Resource/GlobResource.php new file mode 100644 index 0000000000000..20cab1e81d55d --- /dev/null +++ b/src/Symfony/Component/Config/Resource/GlobResource.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\Glob; + +/** + * GlobResource represents a set of resources stored on the filesystem. + * + * Only existence/removal is tracked (not mtimes.) + * + * @author Nicolas Grekas + */ +class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface, \Serializable +{ + private $prefix; + private $pattern; + private $recursive; + private $hash; + + /** + * Constructor. + * + * @param string $prefix A directory prefix + * @param string $pattern A glob pattern + * @param bool $recursive Whether directories should be scanned recursively or not + * + * @throws \InvalidArgumentException + */ + public function __construct($prefix, $pattern, $recursive) + { + $this->prefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false); + $this->pattern = $pattern; + $this->recursive = $recursive; + + if (false === $this->prefix) { + throw new \InvalidArgumentException(sprintf('The path "%s" does not exist.', $prefix)); + } + } + + public function getPrefix() + { + return $this->prefix; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return 'glob.'.$this->prefix.$this->pattern.(int) $this->recursive; + } + + /** + * {@inheritdoc} + */ + public function isFresh($timestamp) + { + $hash = $this->computeHash(); + + if (null === $this->hash) { + $this->hash = $hash; + } + + return $this->hash === $hash; + } + + public function serialize() + { + if (null === $this->hash) { + $this->hash = $this->computeHash(); + } + + return serialize(array($this->prefix, $this->pattern, $this->recursive, $this->hash)); + } + + public function unserialize($serialized) + { + list($this->prefix, $this->pattern, $this->recursive, $this->hash) = unserialize($serialized); + } + + public function getIterator() + { + if (!file_exists($this->prefix) || (!$this->recursive && '' === $this->pattern)) { + return; + } + + if (false === strpos($this->pattern, '/**/') && (defined('GLOB_BRACE') || false === strpos($this->pattern, '{'))) { + foreach (glob($this->prefix.$this->pattern, defined('GLOB_BRACE') ? GLOB_BRACE : 0) as $path) { + if ($this->recursive && is_dir($path)) { + $files = iterator_to_array(new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + function (\SplFileInfo $file) { return '.' !== $file->getBasename()[0]; } + ), + \RecursiveIteratorIterator::LEAVES_ONLY + )); + uasort($files, function (\SplFileInfo $a, \SplFileInfo $b) { + return (string) $a > (string) $b ? 1 : -1; + }); + + foreach ($files as $path => $info) { + if ($info->isFile()) { + yield $path => $info; + } + } + } elseif (is_file($path)) { + yield $path => new \SplFileInfo($path); + } + } + + return; + } + + if (!class_exists(Finder::class)) { + throw new \LogicException(sprintf('Extended glob pattern "%s" cannot be used as the Finder component is not installed.', $this->pattern)); + } + + $finder = new Finder(); + $regex = Glob::toRegex($this->pattern); + if ($this->recursive) { + $regex = substr_replace($regex, '(/|$)', -2, 1); + } + + $prefixLen = strlen($this->prefix); + foreach ($finder->followLinks()->sortByName()->in($this->prefix) as $path => $info) { + if (preg_match($regex, substr('\\' === \DIRECTORY_SEPARATOR ? str_replace('\\', '/', $path) : $path, $prefixLen)) && $info->isFile()) { + yield $path => $info; + } + } + } + + private function computeHash() + { + $hash = hash_init('md5'); + + foreach ($this->getIterator() as $path => $info) { + hash_update($hash, $path."\n"); + } + + return hash_final($hash); + } +} diff --git a/src/Symfony/Component/Config/Resource/ReflectionClassResource.php b/src/Symfony/Component/Config/Resource/ReflectionClassResource.php new file mode 100644 index 0000000000000..b65991a0b755a --- /dev/null +++ b/src/Symfony/Component/Config/Resource/ReflectionClassResource.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * @author Nicolas Grekas + */ +class ReflectionClassResource implements SelfCheckingResourceInterface, \Serializable +{ + private $files = array(); + private $className; + private $classReflector; + private $excludedVendors = array(); + private $hash; + + public function __construct(\ReflectionClass $classReflector, $excludedVendors = array()) + { + $this->className = $classReflector->name; + $this->classReflector = $classReflector; + $this->excludedVendors = $excludedVendors; + } + + public function isFresh($timestamp) + { + if (null === $this->hash) { + $this->hash = $this->computeHash(); + $this->loadFiles($this->classReflector); + } + + foreach ($this->files as $file => $v) { + if (!file_exists($file)) { + return false; + } + + if (@filemtime($file) > $timestamp) { + return $this->hash === $this->computeHash(); + } + } + + return true; + } + + public function __toString() + { + return 'reflection.'.$this->className; + } + + public function serialize() + { + if (null === $this->hash) { + $this->hash = $this->computeHash(); + $this->loadFiles($this->classReflector); + } + + return serialize(array($this->files, $this->className, $this->hash)); + } + + public function unserialize($serialized) + { + list($this->files, $this->className, $this->hash) = unserialize($serialized); + } + + private function loadFiles(\ReflectionClass $class) + { + foreach ($class->getInterfaces() as $v) { + $this->loadFiles($v); + } + do { + $file = $class->getFileName(); + if (false !== $file && file_exists($file)) { + foreach ($this->excludedVendors as $vendor) { + if (0 === strpos($file, $vendor) && false !== strpbrk(substr($file, strlen($vendor), 1), '/'.DIRECTORY_SEPARATOR)) { + $file = false; + break; + } + } + if ($file) { + $this->files[$file] = null; + } + } + foreach ($class->getTraits() as $v) { + $this->loadFiles($v); + } + } while ($class = $class->getParentClass()); + } + + private function computeHash() + { + if (null === $this->classReflector) { + try { + $this->classReflector = new \ReflectionClass($this->className); + } catch (\ReflectionException $e) { + // the class does not exist anymore + return false; + } + } + $hash = hash_init('md5'); + + foreach ($this->generateSignature($this->classReflector) as $info) { + hash_update($hash, $info); + } + + return hash_final($hash); + } + + private function generateSignature(\ReflectionClass $class) + { + yield $class->getDocComment().$class->getModifiers(); + + if ($class->isTrait()) { + yield print_r(class_uses($class->name), true); + } else { + yield print_r(class_parents($class->name), true); + yield print_r(class_implements($class->name), true); + yield print_r($class->getConstants(), true); + } + + if (!$class->isInterface()) { + $defaults = $class->getDefaultProperties(); + + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $p) { + yield $p->getDocComment().$p; + yield print_r($defaults[$p->name], true); + } + } + + foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) { + yield preg_replace('/^ @@.*/m', '', $m); + + $defaults = array(); + foreach ($m->getParameters() as $p) { + $defaults[$p->name] = $p->isDefaultValueAvailable() ? $p->getDefaultValue() : null; + } + yield print_r($defaults, true); + } + } +} diff --git a/src/Symfony/Component/Config/Resource/ResourceInterface.php b/src/Symfony/Component/Config/Resource/ResourceInterface.php index 55b3e09648a6b..d98fd427a25eb 100644 --- a/src/Symfony/Component/Config/Resource/ResourceInterface.php +++ b/src/Symfony/Component/Config/Resource/ResourceInterface.php @@ -30,29 +30,4 @@ interface ResourceInterface * @return string A string representation unique to the underlying Resource */ public function __toString(); - - /** - * Returns true if the resource has not been updated since the given timestamp. - * - * @param int $timestamp The last time the resource was loaded - * - * @return bool True if the resource has not been updated, false otherwise - * - * @deprecated since 2.8, to be removed in 3.0. If your resource can check itself for - * freshness implement the SelfCheckingResourceInterface instead. - */ - public function isFresh($timestamp); - - /** - * Returns the tied resource. - * - * @return mixed The resource - * - * @deprecated since 2.8, to be removed in 3.0. As there are many different kinds of resource, - * a single getResource() method does not make sense at the interface level. You - * can still call getResource() on implementing classes, probably after performing - * a type check. If you know the concrete type of Resource at hand, the return value - * of this method may make sense to you. - */ - public function getResource(); } diff --git a/src/Symfony/Component/Config/ResourceCheckerConfigCache.php b/src/Symfony/Component/Config/ResourceCheckerConfigCache.php index 0e3a411b07552..c0aef1d8f9560 100644 --- a/src/Symfony/Component/Config/ResourceCheckerConfigCache.php +++ b/src/Symfony/Component/Config/ResourceCheckerConfigCache.php @@ -29,15 +29,15 @@ class ResourceCheckerConfigCache implements ConfigCacheInterface private $file; /** - * @var ResourceCheckerInterface[] + * @var iterable|ResourceCheckerInterface[] */ private $resourceCheckers; /** - * @param string $file The absolute cache path - * @param ResourceCheckerInterface[] $resourceCheckers The ResourceCheckers to use for the freshness check + * @param string $file The absolute cache path + * @param iterable|ResourceCheckerInterface[] $resourceCheckers The ResourceCheckers to use for the freshness check */ - public function __construct($file, array $resourceCheckers = array()) + public function __construct($file, $resourceCheckers = array()) { $this->file = $file; $this->resourceCheckers = $resourceCheckers; diff --git a/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php b/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php index 61d732cc1c452..ee75efb9c7ed5 100644 --- a/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php +++ b/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php @@ -20,14 +20,14 @@ class ResourceCheckerConfigCacheFactory implements ConfigCacheFactoryInterface { /** - * @var ResourceCheckerInterface[] + * @var iterable|ResourceCheckerInterface[] */ private $resourceCheckers = array(); /** - * @param ResourceCheckerInterface[] $resourceCheckers + * @param iterable|ResourceCheckerInterface[] $resourceCheckers */ - public function __construct(array $resourceCheckers = array()) + public function __construct($resourceCheckers = array()) { $this->resourceCheckers = $resourceCheckers; } diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php index 63efd719b5f59..4c2a397cff6df 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/ArrayNodeDefinitionTest.php @@ -54,6 +54,7 @@ public function providePrototypeNodeSpecificCalls() array('defaultValue', array(array())), array('addDefaultChildrenIfNoneSet', array()), array('requiresAtLeastOneElement', array()), + array('cannotBeEmpty', array()), array('useAttributeAsKey', array('foo')), ); } @@ -231,6 +232,48 @@ public function testNormalizeKeys() $this->assertFalse($this->getField($node, 'normalizeKeys')); } + public function testPrototypeVariable() + { + $node = new ArrayNodeDefinition('root'); + $this->assertEquals($node->prototype('variable'), $node->variablePrototype()); + } + + public function testPrototypeScalar() + { + $node = new ArrayNodeDefinition('root'); + $this->assertEquals($node->prototype('scalar'), $node->scalarPrototype()); + } + + public function testPrototypeBoolean() + { + $node = new ArrayNodeDefinition('root'); + $this->assertEquals($node->prototype('boolean'), $node->booleanPrototype()); + } + + public function testPrototypeInteger() + { + $node = new ArrayNodeDefinition('root'); + $this->assertEquals($node->prototype('integer'), $node->integerPrototype()); + } + + public function testPrototypeFloat() + { + $node = new ArrayNodeDefinition('root'); + $this->assertEquals($node->prototype('float'), $node->floatPrototype()); + } + + public function testPrototypeArray() + { + $node = new ArrayNodeDefinition('root'); + $this->assertEquals($node->prototype('array'), $node->arrayPrototype()); + } + + public function testPrototypeEnum() + { + $node = new ArrayNodeDefinition('root'); + $this->assertEquals($node->prototype('enum'), $node->enumPrototype()); + } + public function getEnableableNodeFixtures() { return array( @@ -243,6 +286,32 @@ public function getEnableableNodeFixtures() ); } + public function testRequiresAtLeastOneElement() + { + $node = new ArrayNodeDefinition('root'); + $node + ->requiresAtLeastOneElement() + ->integerPrototype(); + + $node->getNode()->finalize(array(1)); + + $this->addToAssertionCount(1); + } + + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException + * @expectedExceptionMessage The path "root" should have at least 1 element(s) defined. + */ + public function testCannotBeEmpty() + { + $node = new ArrayNodeDefinition('root'); + $node + ->cannotBeEmpty() + ->integerPrototype(); + + $node->getNode()->finalize(array()); + } + protected function getField($object, $field) { $reflection = new \ReflectionProperty($object, $field); diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/BooleanNodeDefinitionTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/BooleanNodeDefinitionTest.php new file mode 100644 index 0000000000000..291dd602d9393 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/BooleanNodeDefinitionTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Definition\Builder; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\Builder\BooleanNodeDefinition; + +class BooleanNodeDefinitionTest extends TestCase +{ + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidDefinitionException + * @expectedExceptionMessage ->cannotBeEmpty() is not applicable to BooleanNodeDefinition. + */ + public function testCannotBeEmptyThrowsAnException() + { + $def = new BooleanNodeDefinition('foo'); + $def->cannotBeEmpty(); + } +} diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php index 1b90ebfeb82bc..99a10413768b4 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/ExprBuilderTest.php @@ -76,6 +76,21 @@ public function testIfNullExpression() $this->assertFinalizedValueIs('value', $test); } + public function testIfEmptyExpression() + { + $test = $this->getTestBuilder() + ->ifEmpty() + ->then($this->returnClosure('new_value')) + ->end(); + $this->assertFinalizedValueIs('new_value', $test, array('key' => array())); + + $test = $this->getTestBuilder() + ->ifEmpty() + ->then($this->returnClosure('new_value')) + ->end(); + $this->assertFinalizedValueIs('value', $test); + } + public function testIfArrayExpression() { $test = $this->getTestBuilder() @@ -130,6 +145,25 @@ public function testThenEmptyArrayExpression() $this->assertFinalizedValueIs(array(), $test); } + /** + * @dataProvider castToArrayValues + */ + public function testcastToArrayExpression($configValue, $expectedValue) + { + $test = $this->getTestBuilder() + ->castToArray() + ->end(); + $this->assertFinalizedValueIs($expectedValue, $test, array('key' => $configValue)); + } + + public function castToArrayValues() + { + yield array('value', array('value')); + yield array(-3.14, array(-3.14)); + yield array(null, array(null)); + yield array(array('value'), array('value')); + } + /** * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException */ diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php index efe0f19482cb2..5a86e1ef6108b 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/NumericNodeDefinitionTest.php @@ -91,4 +91,14 @@ public function testFloatValidMinMaxAssertion() $node = $def->min(3.0)->max(7e2)->getNode(); $this->assertEquals(4.5, $node->finalize(4.5)); } + + /** + * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidDefinitionException + * @expectedExceptionMessage ->cannotBeEmpty() is not applicable to NumericNodeDefinition. + */ + public function testCannotBeEmptyThrowsAnException() + { + $def = new NumericNodeDefinition('foo'); + $def->cannotBeEmpty(); + } } diff --git a/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php index 61f5d3c4c6c33..fd5f9c8cde841 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php @@ -68,6 +68,9 @@ enum="" child3="" /> + + scalar value + scalar value @@ -77,6 +80,28 @@ enum="" pass="" /> + + + + + + + + + + + + + + + + + + EOL diff --git a/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php index 9dda4155492cc..ea5687d656a77 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php @@ -23,8 +23,62 @@ public function testDumper() $dumper = new YamlReferenceDumper(); - $this->assertContains($this->getConfigurationAsString(), $dumper->dump($configuration)); - $this->markTestIncomplete('The Yaml Dumper currently does not support prototyped arrays'); + $this->assertEquals($this->getConfigurationAsString(), $dumper->dump($configuration)); + } + + public function provideDumpAtPath() + { + return array( + 'Regular node' => array('scalar_true', << array('array', << array('array.child2', << array('cms_pages.page', << array('cms_pages.page.locale', <<assertSame(trim($expected), trim($dumper->dumpAtPath($configuration, $path))); } private function getConfigurationAsString() @@ -57,10 +111,31 @@ enum: ~ # One of "this"; "that" # multi-line info text # which should be indented child3: ~ # Example: example setting + scalar_prototyped: [] parameters: # Prototype: Parameter name name: ~ + connections: + + # Prototype + - + user: ~ + pass: ~ + cms_pages: + + # Prototype + page: + + # Prototype + locale: + title: ~ # Required + path: ~ # Required + pipou: + + # Prototype + name: [] + EOL; } } diff --git a/src/Symfony/Component/Config/Tests/DependencyInjection/ConfigCachePassTest.php b/src/Symfony/Component/Config/Tests/DependencyInjection/ConfigCachePassTest.php new file mode 100644 index 0000000000000..7452755f50884 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/DependencyInjection/ConfigCachePassTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Config\DependencyInjection\ConfigCachePass; + +class ConfigCachePassTest extends TestCase +{ + public function testThatCheckersAreProcessedInPriorityOrder() + { + $container = new ContainerBuilder(); + + $definition = $container->register('config_cache_factory')->addArgument(null); + $container->register('checker_2')->addTag('config_cache.resource_checker', array('priority' => 100)); + $container->register('checker_1')->addTag('config_cache.resource_checker', array('priority' => 200)); + $container->register('checker_3')->addTag('config_cache.resource_checker'); + + $pass = new ConfigCachePass(); + $pass->process($container); + + $expected = new IteratorArgument(array( + new Reference('checker_1'), + new Reference('checker_2'), + new Reference('checker_3'), + )); + $this->assertEquals($expected, $definition->getArgument(0)); + } + + public function testThatCheckersCanBeMissing() + { + $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')->setMethods(array('findTaggedServiceIds'))->getMock(); + + $container->expects($this->atLeastOnce()) + ->method('findTaggedServiceIds') + ->will($this->returnValue(array())); + + $pass = new ConfigCachePass(); + $pass->process($container); + } +} diff --git a/src/Symfony/Component/Config/Tests/Exception/FileLoaderLoadExceptionTest.php b/src/Symfony/Component/Config/Tests/Exception/FileLoaderLoadExceptionTest.php index c1ad9150e6a99..7c5e167c44086 100644 --- a/src/Symfony/Component/Config/Tests/Exception/FileLoaderLoadExceptionTest.php +++ b/src/Symfony/Component/Config/Tests/Exception/FileLoaderLoadExceptionTest.php @@ -22,6 +22,18 @@ public function testMessageCannotLoadResource() $this->assertEquals('Cannot load resource "resource".', $exception->getMessage()); } + public function testMessageCannotLoadResourceWithType() + { + $exception = new FileLoaderLoadException('resource', null, null, null, 'foobar'); + $this->assertEquals('Cannot load resource "resource". Make sure there is a loader supporting the "foobar" type.', $exception->getMessage()); + } + + public function testMessageCannotLoadResourceWithAnnotationType() + { + $exception = new FileLoaderLoadException('resource', null, null, null, 'annotation'); + $this->assertEquals('Cannot load resource "resource". Make sure annotations are enabled.', $exception->getMessage()); + } + public function testMessageCannotImportResourceFromSource() { $exception = new FileLoaderLoadException('resource', 'sourceResource'); diff --git a/src/Symfony/Component/Config/Tests/FileLocatorTest.php b/src/Symfony/Component/Config/Tests/FileLocatorTest.php index 5b69f452697b4..049c00905b604 100644 --- a/src/Symfony/Component/Config/Tests/FileLocatorTest.php +++ b/src/Symfony/Component/Config/Tests/FileLocatorTest.php @@ -87,7 +87,7 @@ public function testLocate() } /** - * @expectedException \InvalidArgumentException + * @expectedException \Symfony\Component\Config\Exception\FileLocatorFileNotFoundException * @expectedExceptionMessage The file "foobar.xml" does not exist */ public function testLocateThrowsAnExceptionIfTheFileDoesNotExists() @@ -98,7 +98,7 @@ public function testLocateThrowsAnExceptionIfTheFileDoesNotExists() } /** - * @expectedException \InvalidArgumentException + * @expectedException \Symfony\Component\Config\Exception\FileLocatorFileNotFoundException */ public function testLocateThrowsAnExceptionIfTheFileDoesNotExistsInAbsolutePath() { diff --git a/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php b/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php new file mode 100644 index 0000000000000..68d7296ed8696 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php @@ -0,0 +1,7 @@ +fixXmlConfig('parameter') ->fixXmlConfig('connection') + ->fixXmlConfig('cms_page') ->children() ->booleanNode('boolean')->defaultTrue()->end() ->scalarNode('scalar_empty')->end() @@ -53,6 +54,9 @@ public function getConfigTreeBuilder() ->end() ->end() ->end() + ->arrayNode('scalar_prototyped') + ->prototype('scalar')->end() + ->end() ->arrayNode('parameters') ->useAttributeAsKey('name') ->prototype('scalar')->info('Parameter name')->end() @@ -65,6 +69,29 @@ public function getConfigTreeBuilder() ->end() ->end() ->end() + ->arrayNode('cms_pages') + ->useAttributeAsKey('page') + ->prototype('array') + ->useAttributeAsKey('locale') + ->prototype('array') + ->children() + ->scalarNode('title')->isRequired()->end() + ->scalarNode('path')->isRequired()->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('pipou') + ->useAttributeAsKey('name') + ->prototype('array') + ->prototype('array') + ->children() + ->scalarNode('didou') + ->end() + ->end() + ->end() + ->end() + ->end() ->end() ; diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Resource/.hiddenFile b/src/Symfony/Component/Config/Tests/Fixtures/Resource/.hiddenFile new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Resource/ConditionalClass.php b/src/Symfony/Component/Config/Tests/Fixtures/Resource/ConditionalClass.php new file mode 100644 index 0000000000000..2ba48c5b05b58 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/Resource/ConditionalClass.php @@ -0,0 +1,9 @@ +assertInstanceOf('Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException', $e, '->import() throws a FileLoaderImportCircularReferenceException if the resource is already loading'); } } + + public function testImportWithGlobLikeResource() + { + $locatorMock = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $loader = new TestFileLoader($locatorMock); + + $this->assertSame('[foo]', $loader->import('[foo]')); + } + + public function testImportWithNoGlobMatch() + { + $locatorMock = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $loader = new TestFileLoader($locatorMock); + + $this->assertNull($loader->import('./*.abc')); + } + + public function testImportWithSimpleGlob() + { + $loader = new TestFileLoader(new FileLocator(__DIR__)); + + $this->assertSame(__FILE__, strtr($loader->import('FileLoaderTest.*'), '/', DIRECTORY_SEPARATOR)); + } } class TestFileLoader extends FileLoader diff --git a/src/Symfony/Component/Config/Tests/Resource/ClassExistenceResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/ClassExistenceResourceTest.php new file mode 100644 index 0000000000000..010b6561a2357 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Resource/ClassExistenceResourceTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Resource; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Resource\ClassExistenceResource; +use Symfony\Component\Config\Tests\Fixtures\Resource\ConditionalClass; +use Symfony\Component\Config\Tests\Fixtures\BadParent; + +class ClassExistenceResourceTest extends TestCase +{ + public function testToString() + { + $res = new ClassExistenceResource('BarClass'); + $this->assertSame('BarClass', (string) $res); + } + + public function testGetResource() + { + $res = new ClassExistenceResource('BarClass'); + $this->assertSame('BarClass', $res->getResource()); + } + + public function testIsFreshWhenClassDoesNotExist() + { + $res = new ClassExistenceResource('Symfony\Component\Config\Tests\Fixtures\BarClass'); + + $this->assertTrue($res->isFresh(time())); + + eval(<<assertFalse($res->isFresh(time())); + } + + public function testIsFreshWhenClassExists() + { + $res = new ClassExistenceResource('Symfony\Component\Config\Tests\Resource\ClassExistenceResourceTest'); + + $this->assertTrue($res->isFresh(time())); + } + + public function testExistsKo() + { + spl_autoload_register($autoloader = function ($class) use (&$loadedClass) { $loadedClass = $class; }); + + try { + $res = new ClassExistenceResource('MissingFooClass'); + $this->assertTrue($res->isFresh(0)); + + $this->assertSame('MissingFooClass', $loadedClass); + + $loadedClass = 123; + + $res = new ClassExistenceResource('MissingFooClass', false); + + $this->assertSame(123, $loadedClass); + } finally { + spl_autoload_unregister($autoloader); + } + } + + public function testBadParentWithTimestamp() + { + $res = new ClassExistenceResource(BadParent::class, false); + $this->assertTrue($res->isFresh(time())); + } + + /** + * @expectedException \ReflectionException + * @expectedExceptionMessage Class Symfony\Component\Config\Tests\Fixtures\MissingParent not found + */ + public function testBadParentWithNoTimestamp() + { + $res = new ClassExistenceResource(BadParent::class, false); + $res->isFresh(0); + } + + public function testConditionalClass() + { + $res = new ClassExistenceResource(ConditionalClass::class, false); + + $this->assertFalse($res->isFresh(0)); + } +} diff --git a/src/Symfony/Component/Config/Tests/Resource/ComposerResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/ComposerResourceTest.php new file mode 100644 index 0000000000000..6857c766d1347 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Resource/ComposerResourceTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Resource; + +use Composer\Autoload\ClassLoader; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Resource\ComposerResource; + +class ComposerResourceTest extends TestCase +{ + public function testGetVendor() + { + $res = new ComposerResource(); + + $r = new \ReflectionClass(ClassLoader::class); + $found = false; + + foreach ($res->getVendors() as $vendor) { + if ($vendor && 0 === strpos($r->getFileName(), $vendor)) { + $found = true; + break; + } + } + + $this->assertTrue($found); + } + + public function testSerializeUnserialize() + { + $res = new ComposerResource(); + $ser = unserialize(serialize($res)); + + $this->assertTrue($res->isFresh(0)); + $this->assertTrue($ser->isFresh(0)); + + $this->assertEquals($res, $ser); + } +} diff --git a/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php index 60bd616a41de5..99c75f1047576 100644 --- a/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/DirectoryResourceTest.php @@ -54,22 +54,36 @@ protected function removeDirectory($directory) public function testGetResource() { $resource = new DirectoryResource($this->directory); - $this->assertSame($this->directory, $resource->getResource(), '->getResource() returns the path to the resource'); + $this->assertSame(realpath($this->directory), $resource->getResource(), '->getResource() returns the path to the resource'); } public function testGetPattern() { - $resource = new DirectoryResource('foo', 'bar'); + $resource = new DirectoryResource($this->directory, 'bar'); $this->assertEquals('bar', $resource->getPattern()); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The directory ".*" does not exist./ + */ + public function testResourceDoesNotExist() + { + $resource = new DirectoryResource('/____foo/foobar'.mt_rand(1, 999999)); + } + public function testIsFresh() { $resource = new DirectoryResource($this->directory); $this->assertTrue($resource->isFresh(time() + 10), '->isFresh() returns true if the resource has not changed'); $this->assertFalse($resource->isFresh(time() - 86400), '->isFresh() returns false if the resource has been updated'); + } + + public function testIsFreshForDeletedResources() + { + $resource = new DirectoryResource($this->directory); + $this->removeDirectory($this->directory); - $resource = new DirectoryResource('/____foo/foobar'.mt_rand(1, 999999)); $this->assertFalse($resource->isFresh(time()), '->isFresh() returns false if the resource does not exist'); } @@ -155,7 +169,7 @@ public function testSerializeUnserialize() $unserialized = unserialize(serialize($resource)); - $this->assertSame($this->directory, $resource->getResource()); + $this->assertSame(realpath($this->directory), $resource->getResource()); $this->assertSame('/\.(foo|xml)$/', $resource->getPattern()); } diff --git a/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php index 9e77c9480b4c3..97781ffabfcf1 100644 --- a/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/FileResourceTest.php @@ -22,7 +22,7 @@ class FileResourceTest extends TestCase protected function setUp() { - $this->file = realpath(sys_get_temp_dir()).'/tmp.xml'; + $this->file = sys_get_temp_dir().'/tmp.xml'; $this->time = time(); touch($this->file, $this->time); $this->resource = new FileResource($this->file); @@ -30,6 +30,10 @@ protected function setUp() protected function tearDown() { + if (!file_exists($this->file)) { + return; + } + unlink($this->file); } @@ -38,19 +42,38 @@ public function testGetResource() $this->assertSame(realpath($this->file), $this->resource->getResource(), '->getResource() returns the path to the resource'); } + public function testGetResourceWithScheme() + { + $resource = new FileResource('file://'.$this->file); + $this->assertSame('file://'.$this->file, $resource->getResource(), '->getResource() returns the path to the schemed resource'); + } + public function testToString() { $this->assertSame(realpath($this->file), (string) $this->resource); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The file ".*" does not exist./ + */ + public function testResourceDoesNotExist() + { + $resource = new FileResource('/____foo/foobar'.mt_rand(1, 999999)); + } + public function testIsFresh() { $this->assertTrue($this->resource->isFresh($this->time), '->isFresh() returns true if the resource has not changed in same second'); $this->assertTrue($this->resource->isFresh($this->time + 10), '->isFresh() returns true if the resource has not changed'); $this->assertFalse($this->resource->isFresh($this->time - 86400), '->isFresh() returns false if the resource has been updated'); + } - $resource = new FileResource('/____foo/foobar'.mt_rand(1, 999999)); - $this->assertFalse($resource->isFresh($this->time), '->isFresh() returns false if the resource does not exist'); + public function testIsFreshForDeletedResources() + { + unlink($this->file); + + $this->assertFalse($this->resource->isFresh($this->time), '->isFresh() returns false if the resource does not exist'); } public function testSerializeUnserialize() diff --git a/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php new file mode 100644 index 0000000000000..bf7291fdd66b8 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Resource/GlobResourceTest.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Resource; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Resource\GlobResource; + +class GlobResourceTest extends TestCase +{ + protected function tearDown() + { + $dir = dirname(__DIR__).'/Fixtures'; + @rmdir($dir.'/TmpGlob'); + @unlink($dir.'/TmpGlob'); + @unlink($dir.'/Resource/TmpGlob'); + touch($dir.'/Resource/.hiddenFile'); + } + + public function testIterator() + { + $dir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'; + $resource = new GlobResource($dir, '/Resource', true); + + $paths = iterator_to_array($resource); + + $file = $dir.'/Resource'.DIRECTORY_SEPARATOR.'ConditionalClass.php'; + $this->assertEquals(array($file => new \SplFileInfo($file)), $paths); + $this->assertInstanceOf('SplFileInfo', current($paths)); + $this->assertSame($dir, $resource->getPrefix()); + + $resource = new GlobResource($dir, '/**/Resource', true); + + $paths = iterator_to_array($resource); + + $file = $dir.DIRECTORY_SEPARATOR.'Resource'.DIRECTORY_SEPARATOR.'ConditionalClass.php'; + $this->assertEquals(array($file => $file), $paths); + $this->assertInstanceOf('SplFileInfo', current($paths)); + $this->assertSame($dir, $resource->getPrefix()); + } + + public function testIsFreshNonRecursiveDetectsNewFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', false); + + $this->assertTrue($resource->isFresh(0)); + + mkdir($dir.'/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + rmdir($dir.'/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + touch($dir.'/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + + unlink($dir.'/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + } + + public function testIsFreshNonRecursiveDetectsRemovedFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', false); + + touch($dir.'/TmpGlob'); + touch($dir.'/.TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/.TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + } + + public function testIsFreshRecursiveDetectsRemovedFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', true); + + touch($dir.'/Resource/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/Resource/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + + touch($dir.'/Resource/TmpGlob'); + $this->assertTrue($resource->isFresh(0)); + + unlink($dir.'/Resource/.hiddenFile'); + $this->assertTrue($resource->isFresh(0)); + } + + public function testIsFreshRecursiveDetectsNewFile() + { + $dir = dirname(__DIR__).'/Fixtures'; + $resource = new GlobResource($dir, '/*', true); + + $this->assertTrue($resource->isFresh(0)); + + touch($dir.'/Resource/TmpGlob'); + $this->assertFalse($resource->isFresh(0)); + } +} diff --git a/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php new file mode 100644 index 0000000000000..299b593d71dff --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Resource; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Resource\ReflectionClassResource; + +class ReflectionClassResourceTest extends TestCase +{ + public function testToString() + { + $res = new ReflectionClassResource(new \ReflectionClass('ErrorException')); + + $this->assertSame('reflection.ErrorException', (string) $res); + } + + public function testSerializeUnserialize() + { + $res = new ReflectionClassResource(new \ReflectionClass(DummyInterface::class)); + $ser = unserialize(serialize($res)); + + $this->assertTrue($res->isFresh(0)); + $this->assertTrue($ser->isFresh(0)); + + $this->assertSame((string) $res, (string) $ser); + } + + public function testIsFresh() + { + $res = new ReflectionClassResource(new \ReflectionClass(__CLASS__)); + $mtime = filemtime(__FILE__); + + $this->assertTrue($res->isFresh($mtime), '->isFresh() returns true if the resource has not changed in same second'); + $this->assertTrue($res->isFresh($mtime + 10), '->isFresh() returns true if the resource has not changed'); + $this->assertTrue($res->isFresh($mtime - 86400), '->isFresh() returns true if the resource has not changed'); + } + + public function testIsFreshForDeletedResources() + { + $now = time(); + $tmp = sys_get_temp_dir().'/tmp.php'; + file_put_contents($tmp, 'assertTrue($res->isFresh($now)); + + unlink($tmp); + $this->assertFalse($res->isFresh($now), '->isFresh() returns false if the resource does not exist'); + } + + /** + * @dataProvider provideHashedSignature + */ + public function testHashedSignature($changeExpected, $changedLine, $changedCode) + { + $code = <<<'EOPHP' +/* 0*/ +/* 1*/ class %s extends ErrorException +/* 2*/ { +/* 3*/ const FOO = 123; +/* 4*/ +/* 5*/ public $pub = array(); +/* 6*/ +/* 7*/ protected $prot; +/* 8*/ +/* 9*/ private $priv; +/*10*/ +/*11*/ public function pub($arg = null) {} +/*12*/ +/*13*/ protected function prot($a = array()) {} +/*14*/ +/*15*/ private function priv() {} +/*16*/ } +EOPHP; + + static $expectedSignature, $generateSignature; + + if (null === $expectedSignature) { + eval(sprintf($code, $class = 'Foo'.str_replace('.', '_', uniqid('', true)))); + $r = new \ReflectionClass(ReflectionClassResource::class); + $generateSignature = $r->getMethod('generateSignature'); + $generateSignature->setAccessible(true); + $generateSignature = $generateSignature->getClosure($r->newInstanceWithoutConstructor()); + $expectedSignature = implode("\n", iterator_to_array($generateSignature(new \ReflectionClass($class)))); + } + + $code = explode("\n", $code); + $code[$changedLine] = $changedCode; + eval(sprintf(implode("\n", $code), $class = 'Foo'.str_replace('.', '_', uniqid('', true)))); + $signature = implode("\n", iterator_to_array($generateSignature(new \ReflectionClass($class)))); + + if ($changeExpected) { + $this->assertTrue($expectedSignature !== $signature); + } else { + $this->assertSame($expectedSignature, $signature); + } + } + + public function provideHashedSignature() + { + yield array(0, 0, "// line change\n\n"); + yield array(1, 0, '/** class docblock */'); + yield array(1, 1, 'abstract class %s'); + yield array(1, 1, 'final class %s'); + yield array(1, 1, 'class %s extends Exception'); + yield array(1, 1, 'class %s implements '.DummyInterface::class); + yield array(1, 3, 'const FOO = 456;'); + yield array(1, 3, 'const BAR = 123;'); + yield array(1, 4, '/** pub docblock */'); + yield array(1, 5, 'protected $pub = array();'); + yield array(1, 5, 'public $pub = array(123);'); + yield array(1, 6, '/** prot docblock */'); + yield array(1, 7, 'private $prot;'); + yield array(0, 8, '/** priv docblock */'); + yield array(0, 9, 'private $priv = 123;'); + yield array(1, 10, '/** pub docblock */'); + yield array(1, 11, 'public function pub(...$arg) {}'); + yield array(1, 11, 'public function pub($arg = null): Foo {}'); + yield array(0, 11, "public function pub(\$arg = null) {\nreturn 123;\n}"); + yield array(1, 12, '/** prot docblock */'); + yield array(1, 13, 'protected function prot($a = array(123)) {}'); + yield array(0, 14, '/** priv docblock */'); + yield array(0, 15, ''); + } +} + +interface DummyInterface +{ +} diff --git a/src/Symfony/Component/Config/Tests/Resource/ResourceStub.php b/src/Symfony/Component/Config/Tests/Resource/ResourceStub.php index 78799d7b91967..b01729cbff853 100644 --- a/src/Symfony/Component/Config/Tests/Resource/ResourceStub.php +++ b/src/Symfony/Component/Config/Tests/Resource/ResourceStub.php @@ -31,9 +31,4 @@ public function isFresh($timestamp) { return $this->fresh; } - - public function getResource() - { - return 'stub'; - } } diff --git a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php index 29d64dba84205..161dc61721c12 100644 --- a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php +++ b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php @@ -180,16 +180,11 @@ public function testLoadWrongEmptyXMLWithErrorHandler() } catch (\InvalidArgumentException $e) { $this->assertEquals(sprintf('File %s does not contain valid XML, it is empty.', $file), $e->getMessage()); } - } catch (\Exception $e) { + } finally { restore_error_handler(); error_reporting($errorReporting); - - throw $e; } - restore_error_handler(); - error_reporting($errorReporting); - $disableEntities = libxml_disable_entity_loader(true); libxml_disable_entity_loader($disableEntities); diff --git a/src/Symfony/Component/Config/composer.json b/src/Symfony/Component/Config/composer.json index e6d6d3615a8b0..508371af5d7ca 100644 --- a/src/Symfony/Component/Config/composer.json +++ b/src/Symfony/Component/Config/composer.json @@ -16,11 +16,17 @@ } ], "require": { - "php": ">=5.3.9", - "symfony/filesystem": "~2.3|~3.0.0" + "php": "^7.1.3", + "symfony/filesystem": "~3.4|~4.0" }, "require-dev": { - "symfony/yaml": "~2.7|~3.0.0" + "symfony/finder": "~3.4|~4.0", + "symfony/yaml": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0" + }, + "conflict": { + "symfony/finder": "<3.4", + "symfony/dependency-injection": "<3.4" }, "suggest": { "symfony/yaml": "To use the yaml reference dumper" @@ -34,7 +40,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 6dfa571a31822..f6cba6c8dce4c 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -11,8 +11,7 @@ namespace Symfony\Component\Console; -use Symfony\Component\Console\Descriptor\TextDescriptor; -use Symfony\Component\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\DebugFormatterHelper; @@ -20,13 +19,13 @@ use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputAwareInterface; -use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -35,11 +34,8 @@ use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; -use Symfony\Component\Console\Helper\DialogHelper; -use Symfony\Component\Console\Helper\ProgressHelper; -use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Event\ConsoleCommandEvent; -use Symfony\Component\Console\Event\ConsoleExceptionEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\LogicException; @@ -68,17 +64,17 @@ class Application private $runningCommand; private $name; private $version; + private $commandLoader; private $catchExceptions = true; private $autoExit = true; private $definition; private $helperSet; private $dispatcher; - private $terminalDimensions; + private $terminal; private $defaultCommand; + private $singleCommand; /** - * Constructor. - * * @param string $name The name of the application * @param string $version The version of the application */ @@ -86,6 +82,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { $this->name = $name; $this->version = $version; + $this->terminal = new Terminal(); $this->defaultCommand = 'list'; $this->helperSet = $this->getDefaultHelperSet(); $this->definition = $this->getDefaultInputDefinition(); @@ -100,6 +97,11 @@ public function setDispatcher(EventDispatcherInterface $dispatcher) $this->dispatcher = $dispatcher; } + public function setCommandLoader(CommandLoaderInterface $commandLoader) + { + $this->commandLoader = $commandLoader; + } + /** * Runs the current application. * @@ -112,6 +114,9 @@ public function setDispatcher(EventDispatcherInterface $dispatcher) */ public function run(InputInterface $input = null, OutputInterface $output = null) { + putenv('LINES='.$this->terminal->getHeight()); + putenv('COLUMNS='.$this->terminal->getWidth()); + if (null === $input) { $input = new ArgvInput(); } @@ -174,17 +179,17 @@ public function run(InputInterface $input = null, OutputInterface $output = null */ public function doRun(InputInterface $input, OutputInterface $output) { - if (true === $input->hasParameterOption(array('--version', '-V'))) { + if (true === $input->hasParameterOption(array('--version', '-V'), true)) { $output->writeln($this->getLongVersion()); return 0; } $name = $this->getCommandName($input); - if (true === $input->hasParameterOption(array('--help', '-h'))) { + if (true === $input->hasParameterOption(array('--help', '-h'), true)) { if (!$name) { $name = 'help'; - $input = new ArrayInput(array('command' => 'help')); + $input = new ArrayInput(array('command_name' => $this->defaultCommand)); } else { $this->wantHelps = true; } @@ -200,9 +205,23 @@ public function doRun(InputInterface $input, OutputInterface $output) )); } - $this->runningCommand = null; - // the command name MUST be the first element of the input - $command = $this->find($name); + try { + $this->runningCommand = null; + // the command name MUST be the first element of the input + $command = $this->find($name); + } catch (\Throwable $e) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event); + $e = $event->getError(); + + if (0 === $event->getExitCode()) { + return 0; + } + } + + throw $e; + } $this->runningCommand = $command; $exitCode = $this->doRunCommand($command, $input, $output); @@ -248,6 +267,13 @@ public function setDefinition(InputDefinition $definition) */ public function getDefinition() { + if ($this->singleCommand) { + $inputDefinition = $this->definition; + $inputDefinition->setArguments(); + + return $inputDefinition; + } + return $this->definition; } @@ -261,6 +287,16 @@ public function getHelp() return $this->getLongVersion(); } + /** + * Gets whether to catch exceptions or not during commands execution. + * + * @return bool Whether to catch exceptions or not during commands execution + */ + public function areExceptionsCaught() + { + return $this->catchExceptions; + } + /** * Sets whether to catch exceptions or not during commands execution. * @@ -271,6 +307,16 @@ public function setCatchExceptions($boolean) $this->catchExceptions = (bool) $boolean; } + /** + * Gets whether to automatically exit after a command execution or not. + * + * @return bool Whether to automatically exit after a command execution or not + */ + public function isAutoExitEnabled() + { + return $this->autoExit; + } + /** * Sets whether to automatically exit after a command execution or not. * @@ -330,13 +376,13 @@ public function getLongVersion() { if ('UNKNOWN' !== $this->getName()) { if ('UNKNOWN' !== $this->getVersion()) { - return sprintf('%s version %s', $this->getName(), $this->getVersion()); + return sprintf('%s %s', $this->getName(), $this->getVersion()); } - return sprintf('%s', $this->getName()); + return $this->getName(); } - return 'Console Tool'; + return 'Console Tool'; } /** @@ -389,6 +435,10 @@ public function add(Command $command) throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); } + if (!$command->getName()) { + throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($command))); + } + $this->commands[$command->getName()] = $command; foreach ($command->getAliases() as $alias) { @@ -409,12 +459,15 @@ public function add(Command $command) */ public function get($name) { - if (!isset($this->commands[$name])) { + if (isset($this->commands[$name])) { + $command = $this->commands[$name]; + } elseif ($this->commandLoader && $this->commandLoader->has($name)) { + $command = $this->commandLoader->get($name); + $this->add($command); + } else { throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); } - $command = $this->commands[$name]; - if ($this->wantHelps) { $this->wantHelps = false; @@ -436,7 +489,7 @@ public function get($name) */ public function has($name) { - return isset($this->commands[$name]); + return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name)); } /** @@ -493,7 +546,7 @@ public function findNamespace($namespace) $exact = in_array($namespace, $namespaces, true); if (count($namespaces) > 1 && !$exact) { - throw new CommandNotFoundException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + throw new CommandNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); } return $exact ? $namespace : reset($namespaces); @@ -513,11 +566,16 @@ public function findNamespace($namespace) */ public function find($name) { - $allCommands = array_keys($this->commands); + $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name); $commands = preg_grep('{^'.$expr.'}', $allCommands); - if (empty($commands) || count(preg_grep('{^'.$expr.'$}', $commands)) < 1) { + if (empty($commands)) { + $commands = preg_grep('{^'.$expr.'}i', $allCommands); + } + + // if no commands matched or we just matched namespaces + if (empty($commands) || count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { if (false !== $pos = strrpos($name, ':')) { // check if a namespace exists and contains commands $this->findNamespace(substr($name, 0, $pos)); @@ -539,19 +597,33 @@ public function find($name) // filter out aliases for commands which are already on the list if (count($commands) > 1) { - $commandList = $this->commands; - $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { - $commandName = $commandList[$nameOrAlias]->getName(); + $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; + $commands = array_unique(array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { + $commandName = $commandList[$nameOrAlias] instanceof Command ? $commandList[$nameOrAlias]->getName() : $nameOrAlias; return $commandName === $nameOrAlias || !in_array($commandName, $commands); - }); + })); } $exact = in_array($name, $commands, true); if (count($commands) > 1 && !$exact) { - $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); + $usableWidth = $this->terminal->getWidth() - 10; + $abbrevs = array_values($commands); + $maxLen = 0; + foreach ($abbrevs as $abbrev) { + $maxLen = max(Helper::strlen($abbrev), $maxLen); + } + $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) { + if (!$commandList[$cmd] instanceof Command) { + return $cmd; + } + $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); + + return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; + }, array_values($commands)); + $suggestions = $this->getAbbreviationSuggestions($abbrevs); - throw new CommandNotFoundException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions), array_values($commands)); + throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands)); } return $this->get($exact ? $name : reset($commands)); @@ -569,7 +641,18 @@ public function find($name) public function all($namespace = null) { if (null === $namespace) { - return $this->commands; + if (!$this->commandLoader) { + return $this->commands; + } + + $commands = $this->commands; + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name])) { + $commands[$name] = $this->get($name); + } + } + + return $commands; } $commands = array(); @@ -579,6 +662,14 @@ public function all($namespace = null) } } + if ($this->commandLoader) { + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { + $commands[$name] = $this->get($name); + } + } + } + return $commands; } @@ -603,72 +694,35 @@ public static function getAbbreviations($names) } /** - * Returns a text representation of the Application. - * - * @param string $namespace An optional namespace name - * @param bool $raw Whether to return raw command list - * - * @return string A string representing the Application - * - * @deprecated since version 2.3, to be removed in 3.0. - */ - public function asText($namespace = null, $raw = false) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); - - $descriptor = new TextDescriptor(); - $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, !$raw); - $descriptor->describe($output, $this, array('namespace' => $namespace, 'raw_output' => true)); - - return $output->fetch(); - } - - /** - * Returns an XML representation of the Application. - * - * @param string $namespace An optional namespace name - * @param bool $asDom Whether to return a DOM or an XML string - * - * @return string|\DOMDocument An XML string representing the Application + * Renders a caught exception. * - * @deprecated since version 2.3, to be removed in 3.0. + * @param \Exception $e An exception instance + * @param OutputInterface $output An OutputInterface instance */ - public function asXml($namespace = null, $asDom = false) + public function renderException(\Exception $e, OutputInterface $output) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); + $output->writeln('', OutputInterface::VERBOSITY_QUIET); - $descriptor = new XmlDescriptor(); + $this->doRenderException($e, $output); - if ($asDom) { - return $descriptor->getApplicationDocument($this, $namespace); + if (null !== $this->runningCommand) { + $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET); + $output->writeln('', OutputInterface::VERBOSITY_QUIET); } - - $output = new BufferedOutput(); - $descriptor->describe($output, $this, array('namespace' => $namespace)); - - return $output->fetch(); } - /** - * Renders a caught exception. - * - * @param \Exception $e An exception instance - * @param OutputInterface $output An OutputInterface instance - */ - public function renderException($e, $output) + protected function doRenderException(\Exception $e, OutputInterface $output) { - $output->writeln('', OutputInterface::VERBOSITY_QUIET); - do { - $title = sprintf(' [%s] ', get_class($e)); + $title = sprintf( + ' [%s%s] ', + get_class($e), + $output->isVerbose() && 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '' + ); $len = Helper::strlen($title); - $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; - // HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327 - if (defined('HHVM_VERSION') && $width > 1 << 31) { - $width = 1 << 31; - } + $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX; $lines = array(); foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { foreach ($this->splitStringByWidth($line, $width - 4) as $line) { @@ -716,88 +770,6 @@ public function renderException($e, $output) $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } while ($e = $e->getPrevious()); - - if (null !== $this->runningCommand) { - $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET); - $output->writeln('', OutputInterface::VERBOSITY_QUIET); - } - } - - /** - * Tries to figure out the terminal width in which this application runs. - * - * @return int|null - */ - protected function getTerminalWidth() - { - $dimensions = $this->getTerminalDimensions(); - - return $dimensions[0]; - } - - /** - * Tries to figure out the terminal height in which this application runs. - * - * @return int|null - */ - protected function getTerminalHeight() - { - $dimensions = $this->getTerminalDimensions(); - - return $dimensions[1]; - } - - /** - * Tries to figure out the terminal dimensions based on the current environment. - * - * @return array Array containing width and height - */ - public function getTerminalDimensions() - { - if ($this->terminalDimensions) { - return $this->terminalDimensions; - } - - if ('\\' === DIRECTORY_SEPARATOR) { - // extract [w, H] from "wxh (WxH)" - if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { - return array((int) $matches[1], (int) $matches[2]); - } - // extract [w, h] from "wxh" - if (preg_match('/^(\d+)x(\d+)$/', $this->getConsoleMode(), $matches)) { - return array((int) $matches[1], (int) $matches[2]); - } - } - - if ($sttyString = $this->getSttyColumns()) { - // extract [w, h] from "rows h; columns w;" - if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { - return array((int) $matches[2], (int) $matches[1]); - } - // extract [w, h] from "; h rows; w columns" - if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { - return array((int) $matches[2], (int) $matches[1]); - } - } - - return array(null, null); - } - - /** - * Sets terminal dimensions. - * - * Can be useful to force terminal dimensions for functional tests. - * - * @param int $width The width - * @param int $height The height - * - * @return $this - */ - public function setTerminalDimensions($width, $height) - { - $this->terminalDimensions = array($width, $height); - - return $this; } /** @@ -808,30 +780,35 @@ public function setTerminalDimensions($width, $height) */ protected function configureIO(InputInterface $input, OutputInterface $output) { - if (true === $input->hasParameterOption(array('--ansi'))) { + if (true === $input->hasParameterOption(array('--ansi'), true)) { $output->setDecorated(true); - } elseif (true === $input->hasParameterOption(array('--no-ansi'))) { + } elseif (true === $input->hasParameterOption(array('--no-ansi'), true)) { $output->setDecorated(false); } - if (true === $input->hasParameterOption(array('--no-interaction', '-n'))) { + if (true === $input->hasParameterOption(array('--no-interaction', '-n'), true)) { $input->setInteractive(false); - } elseif (function_exists('posix_isatty') && $this->getHelperSet()->has('question')) { - $inputStream = $this->getHelperSet()->get('question')->getInputStream(); + } elseif (function_exists('posix_isatty')) { + $inputStream = null; + + if ($input instanceof StreamableInputInterface) { + $inputStream = $input->getStream(); + } + if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) { $input->setInteractive(false); } } - if (true === $input->hasParameterOption(array('--quiet', '-q'))) { + if (true === $input->hasParameterOption(array('--quiet', '-q'), true)) { $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); $input->setInteractive(false); } else { - if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { + if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || $input->getParameterOption('--verbose', false, true) === 3) { $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); - } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { + } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || $input->getParameterOption('--verbose', false, true) === 2) { $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); - } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { + } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) { $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); } } @@ -880,25 +857,17 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } else { $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED; } - } catch (\Exception $e) { } catch (\Throwable $e) { - } - if (null !== $e) { - $x = $e instanceof \Exception ? $e : new FatalThrowableError($e); - $event = new ConsoleExceptionEvent($command, $input, $output, $x, $x->getCode()); - $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event); + $event = new ConsoleErrorEvent($input, $output, $e, $command); + $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event); + $e = $event->getError(); - if ($x !== $event->getException()) { - $e = $event->getException(); + if (0 !== $exitCode = $event->getExitCode()) { + throw $e; } - $exitCode = $e->getCode(); - } - - $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); - $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); - - if (null !== $e) { - throw $e; + } finally { + $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); + $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); } return $event->getExitCode(); @@ -913,7 +882,7 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI */ protected function getCommandName(InputInterface $input) { - return $input->getFirstArgument(); + return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument(); } /** @@ -955,63 +924,12 @@ protected function getDefaultHelperSet() { return new HelperSet(array( new FormatterHelper(), - new DialogHelper(false), - new ProgressHelper(false), - new TableHelper(false), new DebugFormatterHelper(), new ProcessHelper(), new QuestionHelper(), )); } - /** - * Runs and parses stty -a if it's available, suppressing any error output. - * - * @return string - */ - private function getSttyColumns() - { - if (!function_exists('proc_open')) { - return; - } - - $descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); - $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); - if (is_resource($process)) { - $info = stream_get_contents($pipes[1]); - fclose($pipes[1]); - fclose($pipes[2]); - proc_close($process); - - return $info; - } - } - - /** - * Runs and parses mode CON if it's available, suppressing any error output. - * - * @return string|null x or null if it could not be parsed - */ - private function getConsoleMode() - { - if (!function_exists('proc_open')) { - return; - } - - $descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); - $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); - if (is_resource($process)) { - $info = stream_get_contents($pipes[1]); - fclose($pipes[1]); - fclose($pipes[2]); - proc_close($process); - - if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { - return $matches[2].'x'.$matches[1]; - } - } - } - /** * Returns abbreviated suggestions in string format. * @@ -1021,7 +939,7 @@ private function getConsoleMode() */ private function getAbbreviationSuggestions($abbrevs) { - return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + return ' '.implode("\n ", $abbrevs); } /** @@ -1088,7 +1006,7 @@ private function findAlternatives($name, $collection) } $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); - asort($alternatives); + ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE); return array_keys($alternatives); } @@ -1096,11 +1014,23 @@ private function findAlternatives($name, $collection) /** * Sets the default Command name. * - * @param string $commandName The Command name + * @param string $commandName The Command name + * @param bool $isSingleCommand Set to true if there is only one command in this application + * + * @return self */ - public function setDefaultCommand($commandName) + public function setDefaultCommand($commandName, $isSingleCommand = false) { $this->defaultCommand = $commandName; + + if ($isSingleCommand) { + // Ensure the command exist + $this->find($commandName); + + $this->singleCommand = true; + } + + return $this; } private function splitStringByWidth($string, $width) diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 8021068edfb13..90eb1152e44be 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,6 +1,50 @@ CHANGELOG ========= +4.0.0 +----- + + * `OutputFormatter` throws an exception when unknown options are used + * removed `QuestionHelper::setInputStream()/getInputStream()` + * removed `Application::getTerminalWidth()/getTerminalHeight()` and + `Application::setTerminalDimensions()/getTerminalDimensions()` +* removed `ConsoleExceptionEvent` +* removed `ConsoleEvents::EXCEPTION` + +3.4.0 +----- + + * added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11 + `ContainerCommandLoader` for commands lazy-loading + * added a case-insensitive command name matching fallback + +3.3.0 +----- + +* added `ExceptionListener` +* added `AddConsoleCommandPass` (originally in FrameworkBundle) +* [BC BREAK] `Input::getOption()` no longer returns the default value for options + with value optional explicitly passed empty +* added console.error event to catch exceptions thrown by other listeners +* deprecated console.exception event in favor of console.error +* added ability to handle `CommandNotFoundException` through the + `console.error` event +* deprecated default validation in `SymfonyQuestionHelper::ask` + +3.2.0 +------ + +* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs +* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface) +* added StreamableInputInterface +* added LockableTrait + +3.1.0 +----- + + * added truncate method to FormatterHelper + * added setColumnWidth(s) method to Table + 2.8.3 ----- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 67297fc044a46..f3a5d716f2c33 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -11,14 +11,11 @@ namespace Symfony\Component\Console\Command; -use Symfony\Component\Console\Descriptor\TextDescriptor; -use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\HelperSet; @@ -37,6 +34,7 @@ class Command private $processTitle; private $aliases = array(); private $definition; + private $hidden = false; private $help; private $description; private $ignoreValidationErrors = false; @@ -63,10 +61,6 @@ public function __construct($name = null) } $this->configure(); - - if (!$this->name) { - throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); - } } /** @@ -283,24 +277,12 @@ public function run(InputInterface $input, OutputInterface $output) * * @see execute() */ - public function setCode($code) + public function setCode(callable $code) { - if (!is_callable($code)) { - throw new InvalidArgumentException('Invalid callable provided to Command::setCode.'); - } - - if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) { + if ($code instanceof \Closure) { $r = new \ReflectionFunction($code); if (null === $r->getClosureThis()) { - if (PHP_VERSION_ID < 70000) { - // Bug in PHP5: https://bugs.php.net/bug.php?id=64761 - // This means that we cannot bind static closures and therefore we must - // ignore any errors here. There is no way to test if the closure is - // bindable. - $code = @\Closure::bind($code, $this); - } else { - $code = \Closure::bind($code, $this); - } + $code = \Closure::bind($code, $this); } } @@ -367,7 +349,7 @@ public function getDefinition() } /** - * Gets the InputDefinition to be used to create XML and Text representations of this Command. + * Gets the InputDefinition to be used to create representations of this Command. * * Can be overridden to provide the original command representation when it would otherwise * be changed by merging with the application InputDefinition. @@ -468,6 +450,26 @@ public function getName() return $this->name; } + /** + * @param bool $hidden Whether or not the command should be hidden from the list of commands + * + * @return Command The current instance + */ + public function setHidden($hidden) + { + $this->hidden = (bool) $hidden; + + return $this; + } + + /** + * @return bool Whether the command should be publicly shown or not. + */ + public function isHidden() + { + return $this->hidden; + } + /** * Sets the description for the command. * @@ -637,49 +639,6 @@ public function getHelper($name) return $this->helperSet->get($name); } - /** - * Returns a text representation of the command. - * - * @return string A string representing the command - * - * @deprecated since version 2.3, to be removed in 3.0. - */ - public function asText() - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); - - $descriptor = new TextDescriptor(); - $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); - $descriptor->describe($output, $this, array('raw_output' => true)); - - return $output->fetch(); - } - - /** - * Returns an XML representation of the command. - * - * @param bool $asDom Whether to return a DOM or an XML string - * - * @return string|\DOMDocument An XML string representing the command - * - * @deprecated since version 2.3, to be removed in 3.0. - */ - public function asXml($asDom = false) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); - - $descriptor = new XmlDescriptor(); - - if ($asDom) { - return $descriptor->getCommandDocument($this); - } - - $output = new BufferedOutput(); - $descriptor->describe($output, $this); - - return $output->fetch(); - } - /** * Validates a command name. * diff --git a/src/Symfony/Component/Console/Command/HelpCommand.php b/src/Symfony/Component/Console/Command/HelpCommand.php index c0e7b38843902..b8fd911ad4082 100644 --- a/src/Symfony/Component/Console/Command/HelpCommand.php +++ b/src/Symfony/Component/Console/Command/HelpCommand.php @@ -37,7 +37,6 @@ protected function configure() ->setName('help') ->setDefinition(array( new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), - new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), )) @@ -76,12 +75,6 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->command = $this->getApplication()->find($input->getArgument('command_name')); } - if ($input->getOption('xml')) { - @trigger_error('The --xml option was deprecated in version 2.7 and will be removed in version 3.0. Use the --format option instead.', E_USER_DEPRECATED); - - $input->setOption('format', 'xml'); - } - $helper = new DescriptorHelper(); $helper->describe($output, $this->command, array( 'format' => $input->getOption('format'), diff --git a/src/Symfony/Component/Console/Command/ListCommand.php b/src/Symfony/Component/Console/Command/ListCommand.php index 5e1b926aedfbe..179ddea5dc216 100644 --- a/src/Symfony/Component/Console/Command/ListCommand.php +++ b/src/Symfony/Component/Console/Command/ListCommand.php @@ -68,12 +68,6 @@ public function getNativeDefinition() */ protected function execute(InputInterface $input, OutputInterface $output) { - if ($input->getOption('xml')) { - @trigger_error('The --xml option was deprecated in version 2.7 and will be removed in version 3.0. Use the --format option instead.', E_USER_DEPRECATED); - - $input->setOption('format', 'xml'); - } - $helper = new DescriptorHelper(); $helper->describe($output, $this->getApplication(), array( 'format' => $input->getOption('format'), @@ -89,7 +83,6 @@ private function createDefinition() { return new InputDefinition(array( new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), - new InputOption('xml', null, InputOption::VALUE_NONE, 'To output list as XML'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), )); diff --git a/src/Symfony/Component/Console/Command/LockableTrait.php b/src/Symfony/Component/Console/Command/LockableTrait.php new file mode 100644 index 0000000000000..b521f3b7708b9 --- /dev/null +++ b/src/Symfony/Component/Console/Command/LockableTrait.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Lock\Factory; +use Symfony\Component\Lock\Lock; +use Symfony\Component\Lock\Store\FlockStore; +use Symfony\Component\Lock\Store\SemaphoreStore; + +/** + * Basic lock feature for commands. + * + * @author Geoffrey Brier + */ +trait LockableTrait +{ + /** @var Lock */ + private $lock; + + /** + * Locks a command. + * + * @return bool + */ + private function lock($name = null, $blocking = false) + { + if (!class_exists(SemaphoreStore::class)) { + throw new RuntimeException('To enable the locking feature you must install the symfony/lock component.'); + } + + if (null !== $this->lock) { + throw new LogicException('A lock is already in place.'); + } + + if (SemaphoreStore::isSupported($blocking)) { + $store = new SemaphoreStore(); + } else { + $store = new FlockStore(sys_get_temp_dir()); + } + + $this->lock = (new Factory($store))->createLock($name ?: $this->getName()); + if (!$this->lock->acquire($blocking)) { + $this->lock = null; + + return false; + } + + return true; + } + + /** + * Releases the command lock if there is one. + */ + private function release() + { + if ($this->lock) { + $this->lock->release(); + $this->lock = null; + } + } +} diff --git a/src/Symfony/Component/Console/CommandLoader/CommandLoaderInterface.php b/src/Symfony/Component/Console/CommandLoader/CommandLoaderInterface.php new file mode 100644 index 0000000000000..9462996f6d2af --- /dev/null +++ b/src/Symfony/Component/Console/CommandLoader/CommandLoaderInterface.php @@ -0,0 +1,37 @@ + + */ +interface CommandLoaderInterface +{ + /** + * Loads a command. + * + * @param string $name + * + * @return Command + * + * @throws CommandNotFoundException + */ + public function get($name); + + /** + * Checks if a command exists. + * + * @param string $name + * + * @return bool + */ + public function has($name); + + /** + * @return string[] All registered command names + */ + public function getNames(); +} diff --git a/src/Symfony/Component/Console/CommandLoader/ContainerCommandLoader.php b/src/Symfony/Component/Console/CommandLoader/ContainerCommandLoader.php new file mode 100644 index 0000000000000..753ad0fb705c2 --- /dev/null +++ b/src/Symfony/Component/Console/CommandLoader/ContainerCommandLoader.php @@ -0,0 +1,55 @@ + + */ +class ContainerCommandLoader implements CommandLoaderInterface +{ + private $container; + private $commandMap; + + /** + * @param ContainerInterface $container A container from which to load command services + * @param array $commandMap An array with command names as keys and service ids as values + */ + public function __construct(ContainerInterface $container, array $commandMap) + { + $this->container = $container; + $this->commandMap = $commandMap; + } + + /** + * {@inheritdoc} + */ + public function get($name) + { + if (!$this->has($name)) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + return $this->container->get($this->commandMap[$name]); + } + + /** + * {@inheritdoc} + */ + public function has($name) + { + return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); + } + + /** + * {@inheritdoc} + */ + public function getNames() + { + return array_keys($this->commandMap); + } +} diff --git a/src/Symfony/Component/Console/CommandLoader/FactoryCommandLoader.php b/src/Symfony/Component/Console/CommandLoader/FactoryCommandLoader.php new file mode 100644 index 0000000000000..d9c2055710968 --- /dev/null +++ b/src/Symfony/Component/Console/CommandLoader/FactoryCommandLoader.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * A simple command loader using factories to instantiate commands lazily. + * + * @author Maxime Steinhausser + */ +class FactoryCommandLoader implements CommandLoaderInterface +{ + private $factories; + + /** + * @param callable[] $factories Indexed by command names + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + */ + public function has($name) + { + return isset($this->factories[$name]); + } + + /** + * {@inheritdoc} + */ + public function get($name) + { + if (!isset($this->factories[$name])) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + $factory = $this->factories[$name]; + + return $factory(); + } + + /** + * {@inheritdoc} + */ + public function getNames() + { + return array_keys($this->factories); + } +} diff --git a/src/Symfony/Component/Console/ConsoleEvents.php b/src/Symfony/Component/Console/ConsoleEvents.php index 1ed41b7daa9c0..a777936fbc677 100644 --- a/src/Symfony/Component/Console/ConsoleEvents.php +++ b/src/Symfony/Component/Console/ConsoleEvents.php @@ -23,10 +23,7 @@ final class ConsoleEvents * executed by the console. It also allows you to modify the command, input and output * before they are handled to the command. * - * The event listener method receives a Symfony\Component\Console\Event\ConsoleCommandEvent - * instance. - * - * @Event + * @Event("Symfony\Component\Console\Event\ConsoleCommandEvent") * * @var string */ @@ -36,26 +33,21 @@ final class ConsoleEvents * The TERMINATE event allows you to attach listeners after a command is * executed by the console. * - * The event listener method receives a Symfony\Component\Console\Event\ConsoleTerminateEvent - * instance. - * - * @Event + * @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent") * * @var string */ const TERMINATE = 'console.terminate'; /** - * The EXCEPTION event occurs when an uncaught exception appears. + * The ERROR event occurs when an uncaught exception or error appears. * - * This event allows you to deal with the exception or - * to modify the thrown exception. The event listener method receives - * a Symfony\Component\Console\Event\ConsoleExceptionEvent - * instance. + * This event allows you to deal with the exception/error or + * to modify the thrown exception. * - * @Event + * @Event("Symfony\Component\Console\Event\ConsoleErrorEvent") * * @var string */ - const EXCEPTION = 'console.exception'; + const ERROR = 'console.error'; } diff --git a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php new file mode 100644 index 0000000000000..67034098c218a --- /dev/null +++ b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.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\Component\Console\DependencyInjection; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\TypedReference; + +/** + * Registers console commands. + * + * @author Grégoire Pineau + */ +class AddConsoleCommandPass implements CompilerPassInterface +{ + private $commandLoaderServiceId; + private $commandTag; + + public function __construct($commandLoaderServiceId = 'console.command_loader', $commandTag = 'console.command') + { + $this->commandLoaderServiceId = $commandLoaderServiceId; + $this->commandTag = $commandTag; + } + + public function process(ContainerBuilder $container) + { + $commandServices = $container->findTaggedServiceIds($this->commandTag, true); + $lazyCommandMap = array(); + $lazyCommandRefs = array(); + $serviceIds = array(); + + foreach ($commandServices as $id => $tags) { + $definition = $container->getDefinition($id); + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(Command::class)) { + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); + } + + $commandId = 'console.command.'.strtolower(str_replace('\\', '_', $class)); + + if (!isset($tags[0]['command'])) { + if (isset($serviceIds[$commandId]) || $container->hasAlias($commandId)) { + $commandId = $commandId.'_'.$id; + } + if (!$definition->isPublic()) { + $container->setAlias($commandId, $id); + $id = $commandId; + } + $serviceIds[$commandId] = $id; + + continue; + } + + $serviceIds[$commandId] = false; + $commandName = $tags[0]['command']; + unset($tags[0]); + $lazyCommandMap[$commandName] = $id; + $lazyCommandRefs[$id] = new TypedReference($id, $class); + $aliases = array(); + + foreach ($tags as $tag) { + if (isset($tag['command'])) { + $aliases[] = $tag['command']; + $lazyCommandMap[$tag['command']] = $id; + } + } + + $definition->addMethodCall('setName', array($commandName)); + + if ($aliases) { + $definition->addMethodCall('setAliases', array($aliases)); + } + } + + $container + ->register($this->commandLoaderServiceId, ContainerCommandLoader::class) + ->setArguments(array(ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap)); + + $container->setParameter('console.command.ids', $serviceIds); + } +} diff --git a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php index 89961b9cae7da..a9740fe09808c 100644 --- a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php +++ b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php @@ -49,16 +49,23 @@ class ApplicationDescription */ private $aliases; + /** + * @var bool + */ + private $showHidden; + /** * Constructor. * * @param Application $application * @param string|null $namespace + * @param bool $showHidden */ - public function __construct(Application $application, $namespace = null) + public function __construct(Application $application, $namespace = null, $showHidden = false) { $this->application = $application; $this->namespace = $namespace; + $this->showHidden = $showHidden; } /** @@ -112,7 +119,7 @@ private function inspectApplication() /** @var Command $command */ foreach ($commands as $name => $command) { - if (!$command->getName()) { + if (!$command->getName() || (!$this->showHidden && $command->isHidden())) { continue; } diff --git a/src/Symfony/Component/Console/Descriptor/Descriptor.php b/src/Symfony/Component/Console/Descriptor/Descriptor.php index 43a7a0a1fec0e..50dd86ce23f85 100644 --- a/src/Symfony/Component/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Component/Console/Descriptor/Descriptor.php @@ -29,7 +29,7 @@ abstract class Descriptor implements DescriptorInterface /** * @var OutputInterface */ - private $output; + protected $output; /** * {@inheritdoc} diff --git a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php index 942fdc96226ac..453577cd67907 100644 --- a/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/JsonDescriptor.php @@ -64,16 +64,28 @@ protected function describeCommand(Command $command, array $options = array()) protected function describeApplication(Application $application, array $options = array()) { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; - $description = new ApplicationDescription($application, $describedNamespace); + $description = new ApplicationDescription($application, $describedNamespace, true); $commands = array(); foreach ($description->getCommands() as $command) { $commands[] = $this->getCommandData($command); } - $data = $describedNamespace - ? array('commands' => $commands, 'namespace' => $describedNamespace) - : array('commands' => $commands, 'namespaces' => array_values($description->getNamespaces())); + $data = array(); + if ('UNKNOWN' !== $application->getName()) { + $data['application']['name'] = $application->getName(); + if ('UNKNOWN' !== $application->getVersion()) { + $data['application']['version'] = $application->getVersion(); + } + } + + $data['commands'] = $commands; + + if ($describedNamespace) { + $data['namespace'] = $describedNamespace; + } else { + $data['namespaces'] = array_values($description->getNamespaces()); + } $this->writeData($data, $options); } @@ -161,6 +173,7 @@ private function getCommandData(Command $command) 'description' => $command->getDescription(), 'help' => $command->getProcessedHelp(), 'definition' => $this->getInputDefinitionData($command->getNativeDefinition()), + 'hidden' => $command->isHidden(), ); } } diff --git a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php index c2d6243e280cc..106bff5114992 100644 --- a/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; /** * Markdown descriptor. @@ -27,17 +28,37 @@ */ class MarkdownDescriptor extends Descriptor { + /** + * {@inheritdoc} + */ + public function describe(OutputInterface $output, $object, array $options = array()) + { + $decorated = $output->isDecorated(); + $output->setDecorated(false); + + parent::describe($output, $object, $options); + + $output->setDecorated($decorated); + } + + /** + * {@inheritdoc} + */ + protected function write($content, $decorated = true) + { + parent::write($content, $decorated); + } + /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = array()) { $this->write( - '**'.$argument->getName().':**'."\n\n" - .'* Name: '.($argument->getName() ?: '')."\n" + '#### `'.($argument->getName() ?: '')."`\n\n" + .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '') .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n" .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n" - .'* Description: '.preg_replace('/\s*[\r\n]\s*/', "\n ", $argument->getDescription() ?: '')."\n" .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`' ); } @@ -47,14 +68,17 @@ protected function describeInputArgument(InputArgument $argument, array $options */ protected function describeInputOption(InputOption $option, array $options = array()) { + $name = '--'.$option->getName(); + if ($option->getShortcut()) { + $name .= '|-'.implode('|-', explode('|', $option->getShortcut())).''; + } + $this->write( - '**'.$option->getName().':**'."\n\n" - .'* Name: `--'.$option->getName().'`'."\n" - .'* Shortcut: '.($option->getShortcut() ? '`-'.implode('|-', explode('|', $option->getShortcut())).'`' : '')."\n" + '#### `'.$name.'`'."\n\n" + .($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '') .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n" .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n" .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n" - .'* Description: '.preg_replace('/\s*[\r\n]\s*/', "\n ", $option->getDescription() ?: '')."\n" .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`' ); } @@ -65,7 +89,7 @@ protected function describeInputOption(InputOption $option, array $options = arr protected function describeInputDefinition(InputDefinition $definition, array $options = array()) { if ($showArguments = count($definition->getArguments()) > 0) { - $this->write('### Arguments:'); + $this->write('### Arguments'); foreach ($definition->getArguments() as $argument) { $this->write("\n\n"); $this->write($this->describeInputArgument($argument)); @@ -77,7 +101,7 @@ protected function describeInputDefinition(InputDefinition $definition, array $o $this->write("\n\n"); } - $this->write('### Options:'); + $this->write('### Options'); foreach ($definition->getOptions() as $option) { $this->write("\n\n"); $this->write($this->describeInputOption($option)); @@ -94,12 +118,12 @@ protected function describeCommand(Command $command, array $options = array()) $command->mergeApplicationDefinition(false); $this->write( - $command->getName()."\n" - .str_repeat('-', Helper::strlen($command->getName()))."\n\n" - .'* Description: '.($command->getDescription() ?: '')."\n" - .'* Usage:'."\n\n" + '`'.$command->getName()."`\n" + .str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + .'### Usage'."\n\n" .array_reduce(array_merge(array($command->getSynopsis()), $command->getAliases(), $command->getUsages()), function ($carry, $usage) { - return $carry.' * `'.$usage.'`'."\n"; + return $carry.'* `'.$usage.'`'."\n"; }) ); @@ -121,8 +145,9 @@ protected function describeApplication(Application $application, array $options { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); + $title = $this->getApplicationTitle($application); - $this->write($application->getName()."\n".str_repeat('=', Helper::strlen($application->getName()))); + $this->write($title."\n".str_repeat('=', Helper::strlen($title))); foreach ($description->getNamespaces() as $namespace) { if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { @@ -131,8 +156,8 @@ protected function describeApplication(Application $application, array $options } $this->write("\n\n"); - $this->write(implode("\n", array_map(function ($commandName) { - return '* '.$commandName; + $this->write(implode("\n", array_map(function ($commandName) use ($description) { + return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())); }, $namespace['commands']))); } @@ -141,4 +166,17 @@ protected function describeApplication(Application $application, array $options $this->write($this->describeCommand($command)); } } + + private function getApplicationTitle(Application $application) + { + if ('UNKNOWN' !== $application->getName()) { + if ('UNKNOWN' !== $application->getVersion()) { + return sprintf('%s %s', $application->getName(), $application->getVersion()); + } + + return $application->getName(); + } + + return 'Console Tool'; + } } diff --git a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php index 5b82b3deefa8e..ddb97b60e2a06 100644 --- a/src/Symfony/Component/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/TextDescriptor.php @@ -191,7 +191,20 @@ protected function describeApplication(Application $application, array $options $this->writeText("\n"); $this->writeText("\n"); - $width = $this->getColumnWidth($description->getCommands()); + $commands = $description->getCommands(); + $namespaces = $description->getNamespaces(); + if ($describedNamespace && $namespaces) { + // make sure all alias commands are included when describing a specific namespace + $describedNamespaceInfo = reset($namespaces); + foreach ($describedNamespaceInfo['commands'] as $name) { + $commands[$name] = $description->getCommand($name); + } + } + + // calculate max. width based on available commands per namespace + $width = $this->getColumnWidth(call_user_func_array('array_merge', array_map(function ($namespace) use ($commands) { + return array_intersect($namespace['commands'], array_keys($commands)); + }, $namespaces))); if ($describedNamespace) { $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); @@ -199,8 +212,15 @@ protected function describeApplication(Application $application, array $options $this->writeText('Available commands:', $options); } - // add commands by namespace - foreach ($description->getNamespaces() as $namespace) { + foreach ($namespaces as $namespace) { + $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) { + return isset($commands[$name]); + }); + + if (!$namespace['commands']) { + continue; + } + if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { $this->writeText("\n"); $this->writeText(' '.$namespace['id'].'', $options); @@ -209,7 +229,9 @@ protected function describeApplication(Application $application, array $options foreach ($namespace['commands'] as $name) { $this->writeText("\n"); $spacingWidth = $width - Helper::strlen($name); - $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)->getDescription()), $options); + $command = $commands[$name]; + $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : ''; + $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options); } } @@ -228,6 +250,25 @@ private function writeText($content, array $options = array()) ); } + /** + * Formats command aliases to show them in the command description. + * + * @param Command $command + * + * @return string + */ + private function getCommandAliasesText($command) + { + $text = ''; + $aliases = $command->getAliases(); + + if ($aliases) { + $text = '['.implode('|', $aliases).'] '; + } + + return $text; + } + /** * Formats input option/argument default value. * @@ -251,15 +292,11 @@ private function formatDefaultValue($default) } } - if (\PHP_VERSION_ID < 50400) { - return str_replace(array('\/', '\\\\'), array('/', '\\'), json_encode($default)); - } - return str_replace('\\\\', '\\', json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } /** - * @param Command[] $commands + * @param (Command|string)[] $commands * * @return int */ @@ -268,13 +305,17 @@ private function getColumnWidth(array $commands) $widths = array(); foreach ($commands as $command) { - $widths[] = Helper::strlen($command->getName()); - foreach ($command->getAliases() as $alias) { - $widths[] = Helper::strlen($alias); + if ($command instanceof Command) { + $widths[] = Helper::strlen($command->getName()); + foreach ($command->getAliases() as $alias) { + $widths[] = Helper::strlen($alias); + } + } else { + $widths[] = Helper::strlen($command); } } - return max($widths) + 2; + return $widths ? max($widths) + 2 : 0; } /** @@ -287,7 +328,7 @@ private function calculateTotalWidthForOptions($options) $totalWidth = 0; foreach ($options as $option) { // "-" + shortcut + ", --" + name - $nameLength = 1 + max(strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName()); + $nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName()); if ($option->acceptValue()) { $valueLength = 1 + Helper::strlen($option->getName()); // = + value diff --git a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php index b5676beb3766f..03a26cc83ab35 100644 --- a/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Component/Console/Descriptor/XmlDescriptor.php @@ -64,6 +64,7 @@ public function getCommandDocument(Command $command) $commandXML->setAttribute('id', $command->getName()); $commandXML->setAttribute('name', $command->getName()); + $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0); $commandXML->appendChild($usagesXML = $dom->createElement('usages')); @@ -103,7 +104,7 @@ public function getApplicationDocument(Application $application, $namespace = nu $rootXml->appendChild($commandsXML = $dom->createElement('commands')); - $description = new ApplicationDescription($application, $namespace); + $description = new ApplicationDescription($application, $namespace, true); if ($namespace) { $commandsXML->setAttribute('namespace', $namespace); diff --git a/src/Symfony/Component/Console/Event/ConsoleErrorEvent.php b/src/Symfony/Component/Console/Event/ConsoleErrorEvent.php new file mode 100644 index 0000000000000..49edb723d212d --- /dev/null +++ b/src/Symfony/Component/Console/Event/ConsoleErrorEvent.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Allows to handle throwables thrown while running a command. + * + * @author Wouter de Jong + */ +final class ConsoleErrorEvent extends ConsoleEvent +{ + private $error; + private $exitCode; + + public function __construct(InputInterface $input, OutputInterface $output, $error, Command $command = null) + { + parent::__construct($command, $input, $output); + + $this->setError($error); + } + + /** + * Returns the thrown error/exception. + * + * @return \Throwable + */ + public function getError() + { + return $this->error; + } + + /** + * Replaces the thrown error/exception. + * + * @param \Throwable $error + */ + public function setError($error) + { + if (!$error instanceof \Throwable && !$error instanceof \Exception) { + throw new InvalidArgumentException(sprintf('The error passed to ConsoleErrorEvent must be an instance of \Throwable or \Exception, "%s" was passed instead.', is_object($error) ? get_class($error) : gettype($error))); + } + + $this->error = $error; + } + + /** + * Sets the exit code. + * + * @param int $exitCode The command exit code + */ + public function setExitCode($exitCode) + { + $this->exitCode = (int) $exitCode; + + $r = new \ReflectionProperty($this->error, 'code'); + $r->setAccessible(true); + $r->setValue($this->error, $this->exitCode); + } + + /** + * Gets the exit code. + * + * @return int The command exit code + */ + public function getExitCode() + { + return null !== $this->exitCode ? $this->exitCode : ($this->error->getCode() ?: 1); + } +} diff --git a/src/Symfony/Component/Console/Event/ConsoleEvent.php b/src/Symfony/Component/Console/Event/ConsoleEvent.php index ab620c4609a20..5440da216c96f 100644 --- a/src/Symfony/Component/Console/Event/ConsoleEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleEvent.php @@ -28,7 +28,7 @@ class ConsoleEvent extends Event private $input; private $output; - public function __construct(Command $command, InputInterface $input, OutputInterface $output) + public function __construct(Command $command = null, InputInterface $input, OutputInterface $output) { $this->command = $command; $this->input = $input; @@ -38,7 +38,7 @@ public function __construct(Command $command, InputInterface $input, OutputInter /** * Gets the command that is executed. * - * @return Command A Command instance + * @return Command|null A Command instance */ public function getCommand() { diff --git a/src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php b/src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php deleted file mode 100644 index 603b7eed78cd8..0000000000000 --- a/src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Console\Event; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Allows to handle exception thrown in a command. - * - * @author Fabien Potencier - */ -class ConsoleExceptionEvent extends ConsoleEvent -{ - private $exception; - private $exitCode; - - public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode) - { - parent::__construct($command, $input, $output); - - $this->setException($exception); - $this->exitCode = (int) $exitCode; - } - - /** - * Returns the thrown exception. - * - * @return \Exception The thrown exception - */ - public function getException() - { - return $this->exception; - } - - /** - * Replaces the thrown exception. - * - * This exception will be thrown if no response is set in the event. - * - * @param \Exception $exception The thrown exception - */ - public function setException(\Exception $exception) - { - $this->exception = $exception; - } - - /** - * Gets the exit code. - * - * @return int The command exit code - */ - public function getExitCode() - { - return $this->exitCode; - } -} diff --git a/src/Symfony/Component/Console/EventListener/ErrorListener.php b/src/Symfony/Component/Console/EventListener/ErrorListener.php new file mode 100644 index 0000000000000..3774f9e6666d4 --- /dev/null +++ b/src/Symfony/Component/Console/EventListener/ErrorListener.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * @author James Halsall + * @author Robin Chalas + */ +class ErrorListener implements EventSubscriberInterface +{ + private $logger; + + public function __construct(LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + public function onConsoleError(ConsoleErrorEvent $event) + { + if (null === $this->logger) { + return; + } + + $error = $event->getError(); + + if (!$inputString = $this->getInputString($event)) { + return $this->logger->error('An error occurred while using the console. Message: "{message}"', array('error' => $error, 'message' => $error->getMessage())); + } + + $this->logger->error('Error thrown while running command "{command}". Message: "{message}"', array('error' => $error, 'command' => $inputString, 'message' => $error->getMessage())); + } + + public function onConsoleTerminate(ConsoleTerminateEvent $event) + { + if (null === $this->logger) { + return; + } + + $exitCode = $event->getExitCode(); + + if (0 === $exitCode) { + return; + } + + if (!$inputString = $this->getInputString($event)) { + return $this->logger->debug('The console exited with code "{code}"', array('code' => $exitCode)); + } + + $this->logger->debug('Command "{command}" exited with code "{code}"', array('command' => $inputString, 'code' => $exitCode)); + } + + public static function getSubscribedEvents() + { + return array( + ConsoleEvents::ERROR => array('onConsoleError', -128), + ConsoleEvents::TERMINATE => array('onConsoleTerminate', -128), + ); + } + + private static function getInputString(ConsoleEvent $event) + { + $commandName = $event->getCommand() ? $event->getCommand()->getName() : null; + $input = $event->getInput(); + + if (method_exists($input, '__toString')) { + if ($commandName) { + return str_replace(array("'$commandName'", "\"$commandName\""), $commandName, (string) $input); + } + + return (string) $input; + } + + return $commandName; + } +} diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatter.php b/src/Symfony/Component/Console/Formatter/OutputFormatter.php index dd67ed267cb1f..2befd74ea7f8c 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatter.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatter.php @@ -132,7 +132,7 @@ public function format($message) $message = (string) $message; $offset = 0; $output = ''; - $tagRegex = '[a-z][a-z0-9_=;-]*+'; + $tagRegex = '[a-z][a-z0-9,_=;-]*+'; preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; @@ -195,7 +195,7 @@ private function createStyleFromString($string) return $this->styles[$string]; } - if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, PREG_SET_ORDER)) { return false; } @@ -207,12 +207,14 @@ private function createStyleFromString($string) $style->setForeground($match[1]); } elseif ('bg' == $match[0]) { $style->setBackground($match[1]); - } else { - try { - $style->setOption($match[1]); - } catch (\InvalidArgumentException $e) { - return false; + } elseif ('options' === $match[0]) { + preg_match_all('([^,;]+)', $match[1], $options); + $options = array_shift($options); + foreach ($options as $option) { + $style->setOption($option); } + } else { + return false; } } diff --git a/src/Symfony/Component/Console/Helper/DialogHelper.php b/src/Symfony/Component/Console/Helper/DialogHelper.php deleted file mode 100644 index 9ce9f6616884e..0000000000000 --- a/src/Symfony/Component/Console/Helper/DialogHelper.php +++ /dev/null @@ -1,502 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Console\Helper; - -use Symfony\Component\Console\Exception\InvalidArgumentException; -use Symfony\Component\Console\Exception\RuntimeException; -use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Formatter\OutputFormatterStyle; - -/** - * The Dialog class provides helpers to interact with the user. - * - * @author Fabien Potencier - * - * @deprecated since version 2.5, to be removed in 3.0. - * Use {@link \Symfony\Component\Console\Helper\QuestionHelper} instead. - */ -class DialogHelper extends InputAwareHelper -{ - private $inputStream; - private static $shell; - private static $stty; - - public function __construct($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('"Symfony\Component\Console\Helper\DialogHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\QuestionHelper" instead.', E_USER_DEPRECATED); - } - } - - /** - * Asks the user to select a value. - * - * @param OutputInterface $output An Output instance - * @param string|array $question The question to ask - * @param array $choices List of choices to pick from - * @param bool|string $default The default answer if the user enters nothing - * @param bool|int $attempts Max number of times to ask before giving up (false by default, which means infinite) - * @param string $errorMessage Message which will be shown if invalid value from choice list would be picked - * @param bool $multiselect Select more than one value separated by comma - * - * @return int|string|array The selected value or values (the key of the choices array) - * - * @throws InvalidArgumentException - */ - public function select(OutputInterface $output, $question, $choices, $default = null, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) - { - if ($output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - - $width = max(array_map('strlen', array_keys($choices))); - - $messages = (array) $question; - foreach ($choices as $key => $value) { - $messages[] = sprintf(" [%-{$width}s] %s", $key, $value); - } - - $output->writeln($messages); - - $result = $this->askAndValidate($output, '> ', function ($picked) use ($choices, $errorMessage, $multiselect) { - // Collapse all spaces. - $selectedChoices = str_replace(' ', '', $picked); - - if ($multiselect) { - // Check for a separated comma values - if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { - throw new InvalidArgumentException(sprintf($errorMessage, $picked)); - } - $selectedChoices = explode(',', $selectedChoices); - } else { - $selectedChoices = array($picked); - } - - $multiselectChoices = array(); - - foreach ($selectedChoices as $value) { - if (empty($choices[$value])) { - throw new InvalidArgumentException(sprintf($errorMessage, $value)); - } - $multiselectChoices[] = $value; - } - - if ($multiselect) { - return $multiselectChoices; - } - - return $picked; - }, $attempts, $default); - - return $result; - } - - /** - * Asks a question to the user. - * - * @param OutputInterface $output An Output instance - * @param string|array $question The question to ask - * @param string $default The default answer if none is given by the user - * @param array $autocomplete List of values to autocomplete - * - * @return string The user answer - * - * @throws RuntimeException If there is no data to read in the input stream - */ - public function ask(OutputInterface $output, $question, $default = null, array $autocomplete = null) - { - if ($this->input && !$this->input->isInteractive()) { - return $default; - } - - if ($output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - - $output->write($question); - - $inputStream = $this->inputStream ?: STDIN; - - if (null === $autocomplete || !$this->hasSttyAvailable()) { - $ret = fgets($inputStream, 4096); - if (false === $ret) { - throw new RuntimeException('Aborted'); - } - $ret = trim($ret); - } else { - $ret = ''; - - $i = 0; - $ofs = -1; - $matches = $autocomplete; - $numMatches = count($matches); - - $sttyMode = shell_exec('stty -g'); - - // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) - shell_exec('stty -icanon -echo'); - - // Add highlighted text style - $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); - - // Read a keypress - while (!feof($inputStream)) { - $c = fread($inputStream, 1); - - // Backspace Character - if ("\177" === $c) { - if (0 === $numMatches && 0 !== $i) { - --$i; - // Move cursor backwards - $output->write("\033[1D"); - } - - if ($i === 0) { - $ofs = -1; - $matches = $autocomplete; - $numMatches = count($matches); - } else { - $numMatches = 0; - } - - // Pop the last character off the end of our string - $ret = substr($ret, 0, $i); - } elseif ("\033" === $c) { - // Did we read an escape sequence? - $c .= fread($inputStream, 2); - - // A = Up Arrow. B = Down Arrow - if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { - if ('A' === $c[2] && -1 === $ofs) { - $ofs = 0; - } - - if (0 === $numMatches) { - continue; - } - - $ofs += ('A' === $c[2]) ? -1 : 1; - $ofs = ($numMatches + $ofs) % $numMatches; - } - } elseif (ord($c) < 32) { - if ("\t" === $c || "\n" === $c) { - if ($numMatches > 0 && -1 !== $ofs) { - $ret = $matches[$ofs]; - // Echo out remaining chars for current match - $output->write(substr($ret, $i)); - $i = strlen($ret); - } - - if ("\n" === $c) { - $output->write($c); - break; - } - - $numMatches = 0; - } - - continue; - } else { - $output->write($c); - $ret .= $c; - ++$i; - - $numMatches = 0; - $ofs = 0; - - foreach ($autocomplete as $value) { - // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) - if (0 === strpos($value, $ret) && $i !== strlen($value)) { - $matches[$numMatches++] = $value; - } - } - } - - // Erase characters from cursor to end of line - $output->write("\033[K"); - - if ($numMatches > 0 && -1 !== $ofs) { - // Save cursor position - $output->write("\0337"); - // Write highlighted text - $output->write(''.substr($matches[$ofs], $i).''); - // Restore cursor position - $output->write("\0338"); - } - } - - // Reset stty so it behaves normally again - shell_exec(sprintf('stty %s', $sttyMode)); - } - - return strlen($ret) > 0 ? $ret : $default; - } - - /** - * Asks a confirmation to the user. - * - * The question will be asked until the user answers by nothing, yes, or no. - * - * @param OutputInterface $output An Output instance - * @param string|array $question The question to ask - * @param bool $default The default answer if the user enters nothing - * - * @return bool true if the user has confirmed, false otherwise - */ - public function askConfirmation(OutputInterface $output, $question, $default = true) - { - $answer = 'z'; - while ($answer && !in_array(strtolower($answer[0]), array('y', 'n'))) { - $answer = $this->ask($output, $question); - } - - if (false === $default) { - return $answer && 'y' == strtolower($answer[0]); - } - - return !$answer || 'y' == strtolower($answer[0]); - } - - /** - * Asks a question to the user, the response is hidden. - * - * @param OutputInterface $output An Output instance - * @param string|array $question The question - * @param bool $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not - * - * @return string The answer - * - * @throws RuntimeException In case the fallback is deactivated and the response can not be hidden - */ - public function askHiddenResponse(OutputInterface $output, $question, $fallback = true) - { - if ($output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - - if ('\\' === DIRECTORY_SEPARATOR) { - $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; - - // handle code running from a phar - if ('phar:' === substr(__FILE__, 0, 5)) { - $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; - copy($exe, $tmpExe); - $exe = $tmpExe; - } - - $output->write($question); - $value = rtrim(shell_exec($exe)); - $output->writeln(''); - - if (isset($tmpExe)) { - unlink($tmpExe); - } - - return $value; - } - - if ($this->hasSttyAvailable()) { - $output->write($question); - - $sttyMode = shell_exec('stty -g'); - - shell_exec('stty -echo'); - $value = fgets($this->inputStream ?: STDIN, 4096); - shell_exec(sprintf('stty %s', $sttyMode)); - - if (false === $value) { - throw new RuntimeException('Aborted'); - } - - $value = trim($value); - $output->writeln(''); - - return $value; - } - - if (false !== $shell = $this->getShell()) { - $output->write($question); - $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; - $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); - $value = rtrim(shell_exec($command)); - $output->writeln(''); - - return $value; - } - - if ($fallback) { - return $this->ask($output, $question); - } - - throw new RuntimeException('Unable to hide the response'); - } - - /** - * Asks for a value and validates the response. - * - * The validator receives the data to validate. It must return the - * validated data when the data is valid and throw an exception - * otherwise. - * - * @param OutputInterface $output An Output instance - * @param string|array $question The question to ask - * @param callable $validator A PHP callback - * @param int|false $attempts Max number of times to ask before giving up (false by default, which means infinite) - * @param string $default The default answer if none is given by the user - * @param array $autocomplete List of values to autocomplete - * - * @return mixed - * - * @throws \Exception When any of the validators return an error - */ - public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null, array $autocomplete = null) - { - $that = $this; - - $interviewer = function () use ($output, $question, $default, $autocomplete, $that) { - return $that->ask($output, $question, $default, $autocomplete); - }; - - return $this->validateAttempts($interviewer, $output, $validator, $attempts); - } - - /** - * Asks for a value, hide and validates the response. - * - * The validator receives the data to validate. It must return the - * validated data when the data is valid and throw an exception - * otherwise. - * - * @param OutputInterface $output An Output instance - * @param string|array $question The question to ask - * @param callable $validator A PHP callback - * @param int|false $attempts Max number of times to ask before giving up (false by default, which means infinite) - * @param bool $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not - * - * @return string The response - * - * @throws \Exception When any of the validators return an error - * @throws RuntimeException In case the fallback is deactivated and the response can not be hidden - */ - public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true) - { - $that = $this; - - $interviewer = function () use ($output, $question, $fallback, $that) { - return $that->askHiddenResponse($output, $question, $fallback); - }; - - return $this->validateAttempts($interviewer, $output, $validator, $attempts); - } - - /** - * Sets the input stream to read from when interacting with the user. - * - * This is mainly useful for testing purpose. - * - * @param resource $stream The input stream - */ - public function setInputStream($stream) - { - $this->inputStream = $stream; - } - - /** - * Returns the helper's input stream. - * - * @return resource|null The input stream or null if the default STDIN is used - */ - public function getInputStream() - { - return $this->inputStream; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'dialog'; - } - - /** - * Return a valid Unix shell. - * - * @return string|bool The valid shell name, false in case no valid shell is found - */ - private function getShell() - { - if (null !== self::$shell) { - return self::$shell; - } - - self::$shell = false; - - if (file_exists('/usr/bin/env')) { - // handle other OSs with bash/zsh/ksh/csh if available to hide the answer - $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; - foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) { - if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { - self::$shell = $sh; - break; - } - } - } - - return self::$shell; - } - - private function hasSttyAvailable() - { - if (null !== self::$stty) { - return self::$stty; - } - - exec('stty 2>&1', $output, $exitcode); - - return self::$stty = $exitcode === 0; - } - - /** - * Validate an attempt. - * - * @param callable $interviewer A callable that will ask for a question and return the result - * @param OutputInterface $output An Output instance - * @param callable $validator A PHP callback - * @param int|false $attempts Max number of times to ask before giving up; false will ask infinitely - * - * @return string The validated response - * - * @throws \Exception In case the max number of attempts has been reached and no valid response has been given - */ - private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts) - { - if ($output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - - $e = null; - while (false === $attempts || $attempts--) { - if (null !== $e) { - $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($e->getMessage(), 'error')); - } - - try { - return call_user_func($validator, $interviewer()); - } catch (\Exception $e) { - } - } - - throw $e; - } -} diff --git a/src/Symfony/Component/Console/Helper/FormatterHelper.php b/src/Symfony/Component/Console/Helper/FormatterHelper.php index ac736f982e5c1..6a48a77f26901 100644 --- a/src/Symfony/Component/Console/Helper/FormatterHelper.php +++ b/src/Symfony/Component/Console/Helper/FormatterHelper.php @@ -72,6 +72,30 @@ public function formatBlock($messages, $style, $large = false) return implode("\n", $messages); } + /** + * Truncates a message to the given length. + * + * @param string $message + * @param int $length + * @param string $suffix + * + * @return string + */ + public function truncate($message, $length, $suffix = '...') + { + $computedLength = $length - $this->strlen($suffix); + + if ($computedLength > $this->strlen($message)) { + return $message; + } + + if (false === $encoding = mb_detect_encoding($message, null, true)) { + return substr($message, 0, $length).$suffix; + } + + return mb_substr($message, 0, $length, $encoding).$suffix; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 4fb176527a1d2..44bc2da239997 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -58,6 +58,24 @@ public static function strlen($string) return mb_strwidth($string, $encoding); } + /** + * Returns the subset of a string, using mb_substr if it is available. + * + * @param string $string String to subset + * @param int $from Start offset + * @param int|null $length Length to read + * + * @return string The string subset + */ + public static function substr($string, $from, $length = null) + { + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return substr($string, $from, $length); + } + + return mb_substr($string, $from, $length, $encoding); + } + public static function formatTime($secs) { static $timeFormats = array( diff --git a/src/Symfony/Component/Console/Helper/HelperSet.php b/src/Symfony/Component/Console/Helper/HelperSet.php index 896326ee3eca2..6f12b39d98cad 100644 --- a/src/Symfony/Component/Console/Helper/HelperSet.php +++ b/src/Symfony/Component/Console/Helper/HelperSet.php @@ -82,14 +82,6 @@ public function get($name) throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); } - if ('dialog' === $name && $this->helpers[$name] instanceof DialogHelper) { - @trigger_error('"Symfony\Component\Console\Helper\DialogHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\QuestionHelper" instead.', E_USER_DEPRECATED); - } elseif ('progress' === $name && $this->helpers[$name] instanceof ProgressHelper) { - @trigger_error('"Symfony\Component\Console\Helper\ProgressHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\ProgressBar" instead.', E_USER_DEPRECATED); - } elseif ('table' === $name && $this->helpers[$name] instanceof TableHelper) { - @trigger_error('"Symfony\Component\Console\Helper\TableHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\Table" instead.', E_USER_DEPRECATED); - } - return $this->helpers[$name]; } diff --git a/src/Symfony/Component/Console/Helper/ProcessHelper.php b/src/Symfony/Component/Console/Helper/ProcessHelper.php index a811eb48e6798..82935baeaa81d 100644 --- a/src/Symfony/Component/Console/Helper/ProcessHelper.php +++ b/src/Symfony/Component/Console/Helper/ProcessHelper.php @@ -15,7 +15,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; -use Symfony\Component\Process\ProcessBuilder; /** * The ProcessHelper class provides helpers to run external processes. @@ -36,7 +35,7 @@ class ProcessHelper extends Helper * * @return Process The process that ran */ - public function run(OutputInterface $output, $cmd, $error = null, $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE) + public function run(OutputInterface $output, $cmd, $error = null, callable $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); @@ -44,9 +43,7 @@ public function run(OutputInterface $output, $cmd, $error = null, $callback = nu $formatter = $this->getHelperSet()->get('debug_formatter'); - if (is_array($cmd)) { - $process = ProcessBuilder::create($cmd)->getProcess(); - } elseif ($cmd instanceof Process) { + if ($cmd instanceof Process) { $process = $cmd; } else { $process = new Process($cmd); @@ -92,7 +89,7 @@ public function run(OutputInterface $output, $cmd, $error = null, $callback = nu * * @see run() */ - public function mustRun(OutputInterface $output, $cmd, $error = null, $callback = null) + public function mustRun(OutputInterface $output, $cmd, $error = null, callable $callback = null) { $process = $this->run($output, $cmd, $error, $callback); @@ -112,7 +109,7 @@ public function mustRun(OutputInterface $output, $cmd, $error = null, $callback * * @return callable */ - public function wrapCallback(OutputInterface $output, Process $process, $callback = null) + public function wrapCallback(OutputInterface $output, Process $process, callable $callback = null) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); @@ -120,10 +117,8 @@ public function wrapCallback(OutputInterface $output, Process $process, $callbac $formatter = $this->getHelperSet()->get('debug_formatter'); - $that = $this; - - return function ($type, $buffer) use ($output, $process, $callback, $formatter, $that) { - $output->write($formatter->progress(spl_object_hash($process), $that->escapeString($buffer), Process::ERR === $type)); + return function ($type, $buffer) use ($output, $process, $callback, $formatter) { + $output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type)); if (null !== $callback) { call_user_func($callback, $type, $buffer); @@ -131,12 +126,7 @@ public function wrapCallback(OutputInterface $output, Process $process, $callbac }; } - /** - * This method is public for PHP 5.3 compatibility, it should be private. - * - * @internal - */ - public function escapeString($str) + private function escapeString($str) { return str_replace('<', '\\<', $str); } diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 89ca85d2f1071..31c49ef730f02 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Terminal; /** * The ProgressBar provides helpers to display progress output. @@ -21,7 +22,7 @@ * @author Fabien Potencier * @author Chris Jones */ -class ProgressBar +final class ProgressBar { // options private $barWidth = 28; @@ -44,14 +45,13 @@ class ProgressBar private $formatLineCount; private $messages = array(); private $overwrite = true; + private $terminal; private $firstRun = true; private static $formatters; private static $formats; /** - * Constructor. - * * @param OutputInterface $output An OutputInterface instance * @param int $max Maximum steps (0 if unknown) */ @@ -63,6 +63,7 @@ public function __construct(OutputInterface $output, $max = 0) $this->output = $output; $this->setMaxSteps($max); + $this->terminal = new Terminal(); if (!$this->output->isDecorated()) { // disable overwrite when output does not support ANSI codes. @@ -83,7 +84,7 @@ public function __construct(OutputInterface $output, $max = 0) * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ - public static function setPlaceholderFormatterDefinition($name, $callable) + public static function setPlaceholderFormatterDefinition($name, callable $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); @@ -181,20 +182,6 @@ public function getMaxSteps() return $this->max; } - /** - * Gets the progress bar step. - * - * @deprecated since version 2.6, to be removed in 3.0. Use {@link getProgress()} instead. - * - * @return int The progress bar step - */ - public function getStep() - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the getProgress() method instead.', E_USER_DEPRECATED); - - return $this->getProgress(); - } - /** * Gets the current step position. * @@ -208,11 +195,9 @@ public function getProgress() /** * Gets the progress bar step width. * - * @internal This method is public for PHP 5.3 compatibility, it should not be used. - * * @return int The progress bar step width */ - public function getStepWidth() + private function getStepWidth() { return $this->stepWidth; } @@ -234,7 +219,7 @@ public function getProgressPercent() */ public function setBarWidth($size) { - $this->barWidth = (int) $size; + $this->barWidth = max(1, (int) $size); } /** @@ -354,30 +339,12 @@ public function start($max = null) * Advances the progress output X steps. * * @param int $step Number of steps to advance - * - * @throws LogicException */ public function advance($step = 1) { $this->setProgress($this->step + $step); } - /** - * Sets the current progress. - * - * @deprecated since version 2.6, to be removed in 3.0. Use {@link setProgress()} instead. - * - * @param int $step The current progress - * - * @throws LogicException - */ - public function setCurrent($step) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setProgress() method instead.', E_USER_DEPRECATED); - - $this->setProgress($step); - } - /** * Sets whether to overwrite the progressbar, false for new line. * @@ -392,18 +359,15 @@ public function setOverwrite($overwrite) * Sets the current progress. * * @param int $step The current progress - * - * @throws LogicException */ public function setProgress($step) { $step = (int) $step; - if ($step < $this->step) { - throw new LogicException('You can\'t regress the progress bar.'); - } if ($this->max && $step > $this->max) { $this->max = $step; + } elseif ($step < 0) { + $step = 0; } $prevPeriod = (int) ($this->step / $this->redrawFreq); @@ -445,25 +409,7 @@ public function display() $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); } - // these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped. - $self = $this; - $output = $this->output; - $messages = $this->messages; - $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) { - if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { - $text = call_user_func($formatter, $self, $output); - } elseif (isset($messages[$matches[1]])) { - $text = $messages[$matches[1]]; - } else { - return $matches[0]; - } - - if (isset($matches[2])) { - $text = sprintf('%'.$matches[2], $text); - } - - return $text; - }, $this->format)); + $this->overwrite($this->buildLine()); } /** @@ -633,4 +579,44 @@ private static function initFormats() 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%', ); } + + /** + * @return string + */ + private function buildLine() + { + $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; + $callback = function ($matches) { + if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) { + $text = call_user_func($formatter, $this, $this->output); + } elseif (isset($this->messages[$matches[1]])) { + $text = $this->messages[$matches[1]]; + } else { + return $matches[0]; + } + + if (isset($matches[2])) { + $text = sprintf('%'.$matches[2], $text); + } + + return $text; + }; + $line = preg_replace_callback($regex, $callback, $this->format); + + // gets string length for each sub line with multiline format + $linesLength = array_map(function ($subLine) { + return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r")); + }, explode("\n", $line)); + + $linesWidth = max($linesLength); + + $terminalWidth = $this->terminal->getWidth(); + if ($linesWidth <= $terminalWidth) { + return $line; + } + + $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth); + + return preg_replace_callback($regex, $callback, $this->format); + } } diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php deleted file mode 100644 index eaac2df125007..0000000000000 --- a/src/Symfony/Component/Console/Helper/ProgressHelper.php +++ /dev/null @@ -1,469 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Console\Helper; - -use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Exception\LogicException; - -/** - * The Progress class provides helpers to display progress output. - * - * @author Chris Jones - * @author Fabien Potencier - * - * @deprecated since version 2.5, to be removed in 3.0 - * Use {@link ProgressBar} instead. - */ -class ProgressHelper extends Helper -{ - const FORMAT_QUIET = ' %percent%%'; - const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent%%'; - const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent%% Elapsed: %elapsed%'; - const FORMAT_QUIET_NOMAX = ' %current%'; - const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]'; - const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed%'; - - // options - private $barWidth = 28; - private $barChar = '='; - private $emptyBarChar = '-'; - private $progressChar = '>'; - private $format = null; - private $redrawFreq = 1; - - private $lastMessagesLength; - private $barCharOriginal; - - /** - * @var OutputInterface - */ - private $output; - - /** - * Current step. - * - * @var int - */ - private $current; - - /** - * Maximum number of steps. - * - * @var int - */ - private $max; - - /** - * Start time of the progress bar. - * - * @var int - */ - private $startTime; - - /** - * List of formatting variables. - * - * @var array - */ - private $defaultFormatVars = array( - 'current', - 'max', - 'bar', - 'percent', - 'elapsed', - ); - - /** - * Available formatting variables. - * - * @var array - */ - private $formatVars; - - /** - * Stored format part widths (used for padding). - * - * @var array - */ - private $widths = array( - 'current' => 4, - 'max' => 4, - 'percent' => 3, - 'elapsed' => 6, - ); - - /** - * Various time formats. - * - * @var array - */ - private $timeFormats = array( - array(0, '???'), - array(2, '1 sec'), - array(59, 'secs', 1), - array(60, '1 min'), - array(3600, 'mins', 60), - array(5400, '1 hr'), - array(86400, 'hrs', 3600), - array(129600, '1 day'), - array(604800, 'days', 86400), - ); - - public function __construct($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__CLASS__.' class is deprecated since version 2.5 and will be removed in 3.0. Use the Symfony\Component\Console\Helper\ProgressBar class instead.', E_USER_DEPRECATED); - } - } - - /** - * Sets the progress bar width. - * - * @param int $size The progress bar size - */ - public function setBarWidth($size) - { - $this->barWidth = (int) $size; - } - - /** - * Sets the bar character. - * - * @param string $char A character - */ - public function setBarCharacter($char) - { - $this->barChar = $char; - } - - /** - * Sets the empty bar character. - * - * @param string $char A character - */ - public function setEmptyBarCharacter($char) - { - $this->emptyBarChar = $char; - } - - /** - * Sets the progress bar character. - * - * @param string $char A character - */ - public function setProgressCharacter($char) - { - $this->progressChar = $char; - } - - /** - * Sets the progress bar format. - * - * @param string $format The format - */ - public function setFormat($format) - { - $this->format = $format; - } - - /** - * Sets the redraw frequency. - * - * @param int $freq The frequency in steps - */ - public function setRedrawFrequency($freq) - { - $this->redrawFreq = (int) $freq; - } - - /** - * Starts the progress output. - * - * @param OutputInterface $output An Output instance - * @param int|null $max Maximum steps - */ - public function start(OutputInterface $output, $max = null) - { - if ($output instanceof ConsoleOutputInterface) { - $output = $output->getErrorOutput(); - } - - $this->startTime = time(); - $this->current = 0; - $this->max = (int) $max; - - // Disabling output when it does not support ANSI codes as it would result in a broken display anyway. - $this->output = $output->isDecorated() ? $output : new NullOutput(); - $this->lastMessagesLength = 0; - $this->barCharOriginal = ''; - - if (null === $this->format) { - switch ($output->getVerbosity()) { - case OutputInterface::VERBOSITY_QUIET: - $this->format = self::FORMAT_QUIET_NOMAX; - if ($this->max > 0) { - $this->format = self::FORMAT_QUIET; - } - break; - case OutputInterface::VERBOSITY_VERBOSE: - case OutputInterface::VERBOSITY_VERY_VERBOSE: - case OutputInterface::VERBOSITY_DEBUG: - $this->format = self::FORMAT_VERBOSE_NOMAX; - if ($this->max > 0) { - $this->format = self::FORMAT_VERBOSE; - } - break; - default: - $this->format = self::FORMAT_NORMAL_NOMAX; - if ($this->max > 0) { - $this->format = self::FORMAT_NORMAL; - } - break; - } - } - - $this->initialize(); - } - - /** - * Advances the progress output X steps. - * - * @param int $step Number of steps to advance - * @param bool $redraw Whether to redraw or not - * - * @throws LogicException - */ - public function advance($step = 1, $redraw = false) - { - $this->setCurrent($this->current + $step, $redraw); - } - - /** - * Sets the current progress. - * - * @param int $current The current progress - * @param bool $redraw Whether to redraw or not - * - * @throws LogicException - */ - public function setCurrent($current, $redraw = false) - { - if (null === $this->startTime) { - throw new LogicException('You must start the progress bar before calling setCurrent().'); - } - - $current = (int) $current; - - if ($current < $this->current) { - throw new LogicException('You can\'t regress the progress bar'); - } - - if (0 === $this->current) { - $redraw = true; - } - - $prevPeriod = (int) ($this->current / $this->redrawFreq); - - $this->current = $current; - - $currPeriod = (int) ($this->current / $this->redrawFreq); - if ($redraw || $prevPeriod !== $currPeriod || $this->max === $this->current) { - $this->display(); - } - } - - /** - * Outputs the current progress string. - * - * @param bool $finish Forces the end result - * - * @throws LogicException - */ - public function display($finish = false) - { - if (null === $this->startTime) { - throw new LogicException('You must start the progress bar before calling display().'); - } - - $message = $this->format; - foreach ($this->generate($finish) as $name => $value) { - $message = str_replace("%{$name}%", $value, $message); - } - $this->overwrite($this->output, $message); - } - - /** - * Removes the progress bar from the current line. - * - * This is useful if you wish to write some output - * while a progress bar is running. - * Call display() to show the progress bar again. - */ - public function clear() - { - $this->overwrite($this->output, ''); - } - - /** - * Finishes the progress output. - */ - public function finish() - { - if (null === $this->startTime) { - throw new LogicException('You must start the progress bar before calling finish().'); - } - - if (null !== $this->startTime) { - if (!$this->max) { - $this->barChar = $this->barCharOriginal; - $this->display(true); - } - $this->startTime = null; - $this->output->writeln(''); - $this->output = null; - } - } - - /** - * Initializes the progress helper. - */ - private function initialize() - { - $this->formatVars = array(); - foreach ($this->defaultFormatVars as $var) { - if (false !== strpos($this->format, "%{$var}%")) { - $this->formatVars[$var] = true; - } - } - - if ($this->max > 0) { - $this->widths['max'] = $this->strlen($this->max); - $this->widths['current'] = $this->widths['max']; - } else { - $this->barCharOriginal = $this->barChar; - $this->barChar = $this->emptyBarChar; - } - } - - /** - * Generates the array map of format variables to values. - * - * @param bool $finish Forces the end result - * - * @return array Array of format vars and values - */ - private function generate($finish = false) - { - $vars = array(); - $percent = 0; - if ($this->max > 0) { - $percent = (float) $this->current / $this->max; - } - - if (isset($this->formatVars['bar'])) { - if ($this->max > 0) { - $completeBars = floor($percent * $this->barWidth); - } else { - if (!$finish) { - $completeBars = floor($this->current % $this->barWidth); - } else { - $completeBars = $this->barWidth; - } - } - - $emptyBars = $this->barWidth - $completeBars - $this->strlen($this->progressChar); - $bar = str_repeat($this->barChar, $completeBars); - if ($completeBars < $this->barWidth) { - $bar .= $this->progressChar; - $bar .= str_repeat($this->emptyBarChar, $emptyBars); - } - - $vars['bar'] = $bar; - } - - if (isset($this->formatVars['elapsed'])) { - $elapsed = time() - $this->startTime; - $vars['elapsed'] = str_pad($this->humaneTime($elapsed), $this->widths['elapsed'], ' ', STR_PAD_LEFT); - } - - if (isset($this->formatVars['current'])) { - $vars['current'] = str_pad($this->current, $this->widths['current'], ' ', STR_PAD_LEFT); - } - - if (isset($this->formatVars['max'])) { - $vars['max'] = $this->max; - } - - if (isset($this->formatVars['percent'])) { - $vars['percent'] = str_pad(floor($percent * 100), $this->widths['percent'], ' ', STR_PAD_LEFT); - } - - return $vars; - } - - /** - * Converts seconds into human-readable format. - * - * @param int $secs Number of seconds - * - * @return string Time in readable format - */ - private function humaneTime($secs) - { - $text = ''; - foreach ($this->timeFormats as $format) { - if ($secs < $format[0]) { - if (count($format) == 2) { - $text = $format[1]; - break; - } else { - $text = ceil($secs / $format[2]).' '.$format[1]; - break; - } - } - } - - return $text; - } - - /** - * Overwrites a previous message to the output. - * - * @param OutputInterface $output An Output instance - * @param string $message The message - */ - private function overwrite(OutputInterface $output, $message) - { - $length = $this->strlen($message); - - // append whitespace to match the last line's length - if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) { - $message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); - } - - // carriage return - $output->write("\x0D"); - $output->write($message); - - $this->lastMessagesLength = $this->strlen($message); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'progress'; - } -} diff --git a/src/Symfony/Component/Console/Helper/ProgressIndicator.php b/src/Symfony/Component/Console/Helper/ProgressIndicator.php index 2e43bc3f5a3b8..d441accd46e11 100644 --- a/src/Symfony/Component/Console/Helper/ProgressIndicator.php +++ b/src/Symfony/Component/Console/Helper/ProgressIndicator.php @@ -75,42 +75,6 @@ public function setMessage($message) $this->display(); } - /** - * Gets the current indicator message. - * - * @return string|null - * - * @internal for PHP 5.3 compatibility - */ - public function getMessage() - { - return $this->message; - } - - /** - * Gets the progress bar start time. - * - * @return int The progress bar start time - * - * @internal for PHP 5.3 compatibility - */ - public function getStartTime() - { - return $this->startTime; - } - - /** - * Gets the current animated indicator character. - * - * @return string - * - * @internal for PHP 5.3 compatibility - */ - public function getCurrentValue() - { - return $this->indicatorValues[$this->indicatorCurrent % count($this->indicatorValues)]; - } - /** * Starts the indicator output. * @@ -277,13 +241,13 @@ private static function initPlaceholderFormatters() { return array( 'indicator' => function (ProgressIndicator $indicator) { - return $indicator->getCurrentValue(); + return $indicator->indicatorValues[$indicator->indicatorCurrent % count($indicator->indicatorValues)]; }, 'message' => function (ProgressIndicator $indicator) { - return $indicator->getMessage(); + return $indicator->message; }, 'elapsed' => function (ProgressIndicator $indicator) { - return Helper::formatTime(time() - $indicator->getStartTime()); + return Helper::formatTime(time() - $indicator->startTime); }, 'memory' => function () { return Helper::formatMemory(memory_get_usage(true)); diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index a1e770ab2b2a3..ee9aea9c2a706 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -11,9 +11,9 @@ namespace Symfony\Component\Console\Helper; -use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatterStyle; @@ -52,60 +52,40 @@ public function ask(InputInterface $input, OutputInterface $output, Question $qu return $question->getDefault(); } + if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { + $this->inputStream = $stream; + } + if (!$question->getValidator()) { return $this->doAsk($output, $question); } - $that = $this; - - $interviewer = function () use ($output, $question, $that) { - return $that->doAsk($output, $question); + $interviewer = function () use ($output, $question) { + return $this->doAsk($output, $question); }; return $this->validateAttempts($interviewer, $output, $question); } /** - * Sets the input stream to read from when interacting with the user. - * - * This is mainly useful for testing purpose. - * - * @param resource $stream The input stream - * - * @throws InvalidArgumentException In case the stream is not a resource - */ - public function setInputStream($stream) - { - if (!is_resource($stream)) { - throw new InvalidArgumentException('Input stream must be a valid resource.'); - } - - $this->inputStream = $stream; - } - - /** - * Returns the helper's input stream. - * - * @return resource + * {@inheritdoc} */ - public function getInputStream() + public function getName() { - return $this->inputStream; + return 'question'; } /** - * {@inheritdoc} + * Prevents usage of stty. */ - public function getName() + public static function disableStty() { - return 'question'; + self::$stty = false; } /** * Asks the question to the user. * - * This method is public for PHP 5.3 compatibility, it should be private. - * * @param OutputInterface $output * @param Question $question * @@ -113,7 +93,7 @@ public function getName() * * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ - public function doAsk(OutputInterface $output, Question $question) + private function doAsk(OutputInterface $output, Question $question) { $this->writePrompt($output, $question); @@ -387,7 +367,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream) * * @throws \Exception In case the max number of attempts has been reached and no valid response has been given */ - private function validateAttempts($interviewer, OutputInterface $output, Question $question) + private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question) { $error = null; $attempts = $question->getMaxAttempts(); diff --git a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php index 25e094a04f45c..a63225149297d 100644 --- a/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php @@ -27,28 +27,6 @@ */ class SymfonyQuestionHelper extends QuestionHelper { - /** - * {@inheritdoc} - */ - public function ask(InputInterface $input, OutputInterface $output, Question $question) - { - $validator = $question->getValidator(); - $question->setValidator(function ($value) use ($validator) { - if (null !== $validator) { - $value = $validator($value); - } else { - // make required - if (!is_array($value) && !is_bool($value) && 0 === strlen($value)) { - throw new LogicException('A value is required.'); - } - } - - return $value; - }); - - return parent::ask($input, $output, $question); - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Console/Helper/Table.php b/src/Symfony/Component/Console/Helper/Table.php index e5fc36f63dc3a..574e9b46e4a24 100644 --- a/src/Symfony/Component/Console/Helper/Table.php +++ b/src/Symfony/Component/Console/Helper/Table.php @@ -43,7 +43,7 @@ class Table * * @var array */ - private $columnWidths = array(); + private $effectiveColumnWidths = array(); /** * Number of columns cache. @@ -67,6 +67,13 @@ class Table */ private $columnStyles = array(); + /** + * User set column widths. + * + * @var array + */ + private $columnWidths = array(); + private static $styles; public function __construct(OutputInterface $output) @@ -174,6 +181,38 @@ public function getColumnStyle($columnIndex) return $this->getStyle(); } + /** + * Sets the minimum width of a column. + * + * @param int $columnIndex Column index + * @param int $width Minimum column width in characters + * + * @return $this + */ + public function setColumnWidth($columnIndex, $width) + { + $this->columnWidths[(int) $columnIndex] = (int) $width; + + return $this; + } + + /** + * Sets the minimum width of all columns. + * + * @param array $widths + * + * @return $this + */ + public function setColumnWidths(array $widths) + { + $this->columnWidths = array(); + foreach ($widths as $index => $width) { + $this->setColumnWidth($index, $width); + } + + return $this; + } + public function setHeaders(array $headers) { $headers = array_values($headers); @@ -284,7 +323,7 @@ private function renderRowSeparator() $markup = $this->style->getCrossingChar(); for ($column = 0; $column < $count; ++$column) { - $markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->columnWidths[$column]).$this->style->getCrossingChar(); + $markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->effectiveColumnWidths[$column]).$this->style->getCrossingChar(); } $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); @@ -330,11 +369,11 @@ private function renderRow(array $row, $cellFormat) private function renderCell(array $row, $column, $cellFormat) { $cell = isset($row[$column]) ? $row[$column] : ''; - $width = $this->columnWidths[$column]; + $width = $this->effectiveColumnWidths[$column]; if ($cell instanceof TableCell && $cell->getColspan() > 1) { // add the width of the following columns(numbers of colspan). foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { - $width += $this->getColumnSeparatorWidth() + $this->columnWidths[$nextColumn]; + $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; } } @@ -577,7 +616,7 @@ private function calculateColumnsWidth($rows) $lengths[] = $this->getCellWidth($row, $column); } - $this->columnWidths[$column] = max($lengths) + strlen($this->style->getCellRowContentFormat()) - 2; + $this->effectiveColumnWidths[$column] = max($lengths) + strlen($this->style->getCellRowContentFormat()) - 2; } } @@ -601,14 +640,16 @@ private function getColumnSeparatorWidth() */ private function getCellWidth(array $row, $column) { + $cellWidth = 0; + if (isset($row[$column])) { $cell = $row[$column]; $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); - - return $cellWidth; } - return 0; + $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0; + + return max($cellWidth, $columnWidth); } /** @@ -616,7 +657,7 @@ private function getCellWidth(array $row, $column) */ private function cleanup() { - $this->columnWidths = array(); + $this->effectiveColumnWidths = array(); $this->numberOfColumns = null; } diff --git a/src/Symfony/Component/Console/Helper/TableHelper.php b/src/Symfony/Component/Console/Helper/TableHelper.php deleted file mode 100644 index 1f50d2c19980e..0000000000000 --- a/src/Symfony/Component/Console/Helper/TableHelper.php +++ /dev/null @@ -1,269 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Console\Helper; - -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Exception\InvalidArgumentException; - -/** - * Provides helpers to display table output. - * - * @author Саша Стаменковић - * @author Fabien Potencier - * - * @deprecated since version 2.5, to be removed in 3.0 - * Use {@link Table} instead. - */ -class TableHelper extends Helper -{ - const LAYOUT_DEFAULT = 0; - const LAYOUT_BORDERLESS = 1; - const LAYOUT_COMPACT = 2; - - /** - * @var Table - */ - private $table; - - public function __construct($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__CLASS__.' class is deprecated since version 2.5 and will be removed in 3.0. Use the Symfony\Component\Console\Helper\Table class instead.', E_USER_DEPRECATED); - } - - $this->table = new Table(new NullOutput()); - } - - /** - * Sets table layout type. - * - * @param int $layout self::LAYOUT_* - * - * @return $this - * - * @throws InvalidArgumentException when the table layout is not known - */ - public function setLayout($layout) - { - switch ($layout) { - case self::LAYOUT_BORDERLESS: - $this->table->setStyle('borderless'); - break; - - case self::LAYOUT_COMPACT: - $this->table->setStyle('compact'); - break; - - case self::LAYOUT_DEFAULT: - $this->table->setStyle('default'); - break; - - default: - throw new InvalidArgumentException(sprintf('Invalid table layout "%s".', $layout)); - } - - return $this; - } - - public function setHeaders(array $headers) - { - $this->table->setHeaders($headers); - - return $this; - } - - public function setRows(array $rows) - { - $this->table->setRows($rows); - - return $this; - } - - public function addRows(array $rows) - { - $this->table->addRows($rows); - - return $this; - } - - public function addRow(array $row) - { - $this->table->addRow($row); - - return $this; - } - - public function setRow($column, array $row) - { - $this->table->setRow($column, $row); - - return $this; - } - - /** - * Sets padding character, used for cell padding. - * - * @param string $paddingChar - * - * @return $this - */ - public function setPaddingChar($paddingChar) - { - $this->table->getStyle()->setPaddingChar($paddingChar); - - return $this; - } - - /** - * Sets horizontal border character. - * - * @param string $horizontalBorderChar - * - * @return $this - */ - public function setHorizontalBorderChar($horizontalBorderChar) - { - $this->table->getStyle()->setHorizontalBorderChar($horizontalBorderChar); - - return $this; - } - - /** - * Sets vertical border character. - * - * @param string $verticalBorderChar - * - * @return $this - */ - public function setVerticalBorderChar($verticalBorderChar) - { - $this->table->getStyle()->setVerticalBorderChar($verticalBorderChar); - - return $this; - } - - /** - * Sets crossing character. - * - * @param string $crossingChar - * - * @return $this - */ - public function setCrossingChar($crossingChar) - { - $this->table->getStyle()->setCrossingChar($crossingChar); - - return $this; - } - - /** - * Sets header cell format. - * - * @param string $cellHeaderFormat - * - * @return $this - */ - public function setCellHeaderFormat($cellHeaderFormat) - { - $this->table->getStyle()->setCellHeaderFormat($cellHeaderFormat); - - return $this; - } - - /** - * Sets row cell format. - * - * @param string $cellRowFormat - * - * @return $this - */ - public function setCellRowFormat($cellRowFormat) - { - $this->table->getStyle()->setCellHeaderFormat($cellRowFormat); - - return $this; - } - - /** - * Sets row cell content format. - * - * @param string $cellRowContentFormat - * - * @return $this - */ - public function setCellRowContentFormat($cellRowContentFormat) - { - $this->table->getStyle()->setCellRowContentFormat($cellRowContentFormat); - - return $this; - } - - /** - * Sets table border format. - * - * @param string $borderFormat - * - * @return $this - */ - public function setBorderFormat($borderFormat) - { - $this->table->getStyle()->setBorderFormat($borderFormat); - - return $this; - } - - /** - * Sets cell padding type. - * - * @param int $padType STR_PAD_* - * - * @return $this - */ - public function setPadType($padType) - { - $this->table->getStyle()->setPadType($padType); - - return $this; - } - - /** - * Renders table to output. - * - * Example: - * +---------------+-----------------------+------------------+ - * | ISBN | Title | Author | - * +---------------+-----------------------+------------------+ - * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | - * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | - * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | - * +---------------+-----------------------+------------------+ - * - * @param OutputInterface $output - */ - public function render(OutputInterface $output) - { - $p = new \ReflectionProperty($this->table, 'output'); - $p->setAccessible(true); - $p->setValue($this->table, $output); - - $this->table->render(); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'table'; - } -} diff --git a/src/Symfony/Component/Console/Input/ArgvInput.php b/src/Symfony/Component/Console/Input/ArgvInput.php index f6e40cfc68ec0..6195c0535afa8 100644 --- a/src/Symfony/Component/Console/Input/ArgvInput.php +++ b/src/Symfony/Component/Console/Input/ArgvInput.php @@ -148,7 +148,7 @@ private function parseLongOption($token) if (false !== $pos = strpos($name, '=')) { if (0 === strlen($value = substr($name, $pos + 1))) { - array_unshift($this->parsed, null); + array_unshift($this->parsed, $value); } $this->addLongOption(substr($name, 0, $pos), $value); } else { @@ -221,23 +221,16 @@ private function addLongOption($name, $value) $option = $this->definition->getOption($name); - // Convert empty values to null - if (!isset($value[0])) { - $value = null; - } - if (null !== $value && !$option->acceptValue()) { throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); } - if (null === $value && $option->acceptValue() && count($this->parsed)) { + if (in_array($value, array('', null), true) && $option->acceptValue() && count($this->parsed)) { // if option accepts an optional or mandatory argument // let's see if there is one provided $next = array_shift($this->parsed); - if (isset($next[0]) && '-' !== $next[0]) { + if ((isset($next[0]) && '-' !== $next[0]) || in_array($next, array('', null), true)) { $value = $next; - } elseif (empty($next)) { - $value = null; } else { array_unshift($this->parsed, $next); } @@ -248,8 +241,8 @@ private function addLongOption($name, $value) throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); } - if (!$option->isArray()) { - $value = $option->isValueOptional() ? $option->getDefault() : true; + if (!$option->isArray() && !$option->isValueOptional()) { + $value = true; } } @@ -277,11 +270,14 @@ public function getFirstArgument() /** * {@inheritdoc} */ - public function hasParameterOption($values) + public function hasParameterOption($values, $onlyParams = false) { $values = (array) $values; foreach ($this->tokens as $token) { + if ($onlyParams && $token === '--') { + return false; + } foreach ($values as $value) { if ($token === $value || 0 === strpos($token, $value.'=')) { return true; @@ -295,13 +291,16 @@ public function hasParameterOption($values) /** * {@inheritdoc} */ - public function getParameterOption($values, $default = false) + public function getParameterOption($values, $default = false, $onlyParams = false) { $values = (array) $values; $tokens = $this->tokens; while (0 < count($tokens)) { $token = array_shift($tokens); + if ($onlyParams && $token === '--') { + return false; + } foreach ($values as $value) { if ($token === $value || 0 === strpos($token, $value.'=')) { @@ -324,14 +323,13 @@ public function getParameterOption($values, $default = false) */ public function __toString() { - $self = $this; - $tokens = array_map(function ($token) use ($self) { + $tokens = array_map(function ($token) { if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { - return $match[1].$self->escapeToken($match[2]); + return $match[1].$this->escapeToken($match[2]); } if ($token && $token[0] !== '-') { - return $self->escapeToken($token); + return $this->escapeToken($token); } return $token; diff --git a/src/Symfony/Component/Console/Input/ArrayInput.php b/src/Symfony/Component/Console/Input/ArrayInput.php index af4c204bba30a..434ec0240d7f9 100644 --- a/src/Symfony/Component/Console/Input/ArrayInput.php +++ b/src/Symfony/Component/Console/Input/ArrayInput.php @@ -57,7 +57,7 @@ public function getFirstArgument() /** * {@inheritdoc} */ - public function hasParameterOption($values) + public function hasParameterOption($values, $onlyParams = false) { $values = (array) $values; @@ -66,6 +66,10 @@ public function hasParameterOption($values) $v = $k; } + if ($onlyParams && $v === '--') { + return false; + } + if (in_array($v, $values)) { return true; } @@ -77,11 +81,15 @@ public function hasParameterOption($values) /** * {@inheritdoc} */ - public function getParameterOption($values, $default = false) + public function getParameterOption($values, $default = false, $onlyParams = false) { $values = (array) $values; foreach ($this->parameters as $k => $v) { + if ($onlyParams && ($k === '--' || (is_int($k) && $v === '--'))) { + return false; + } + if (is_int($k)) { if (in_array($v, $values)) { return true; @@ -119,6 +127,9 @@ public function __toString() protected function parse() { foreach ($this->parameters as $key => $value) { + if ($key === '--') { + return; + } if (0 === strpos($key, '--')) { $this->addLongOption(substr($key, 2), $value); } elseif ('-' === $key[0]) { @@ -168,7 +179,9 @@ private function addLongOption($name, $value) throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name)); } - $value = $option->isValueOptional() ? $option->getDefault() : true; + if (!$option->isValueOptional()) { + $value = true; + } } $this->options[$name] = $value; diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index 817292ed73086..244e7d4e58379 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -25,12 +25,13 @@ * * @author Fabien Potencier */ -abstract class Input implements InputInterface +abstract class Input implements InputInterface, StreamableInputInterface { /** * @var InputDefinition */ protected $definition; + protected $stream; protected $options = array(); protected $arguments = array(); protected $interactive = true; @@ -157,7 +158,7 @@ public function getOption($name) throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } - return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + return array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); } /** @@ -191,4 +192,20 @@ public function escapeToken($token) { return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); } + + /** + * {@inheritdoc} + */ + public function setStream($stream) + { + $this->stream = $stream; + } + + /** + * {@inheritdoc} + */ + public function getStream() + { + return $this->stream; + } } diff --git a/src/Symfony/Component/Console/Input/InputDefinition.php b/src/Symfony/Component/Console/Input/InputDefinition.php index 52f324d7e6557..85b778b228627 100644 --- a/src/Symfony/Component/Console/Input/InputDefinition.php +++ b/src/Symfony/Component/Console/Input/InputDefinition.php @@ -11,9 +11,6 @@ namespace Symfony\Component\Console\Input; -use Symfony\Component\Console\Descriptor\TextDescriptor; -use Symfony\Component\Console\Descriptor\XmlDescriptor; -use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; @@ -414,47 +411,4 @@ public function getSynopsis($short = false) return implode(' ', $elements); } - - /** - * Returns a textual representation of the InputDefinition. - * - * @return string A string representing the InputDefinition - * - * @deprecated since version 2.3, to be removed in 3.0. - */ - public function asText() - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); - - $descriptor = new TextDescriptor(); - $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); - $descriptor->describe($output, $this, array('raw_output' => true)); - - return $output->fetch(); - } - - /** - * Returns an XML representation of the InputDefinition. - * - * @param bool $asDom Whether to return a DOM or an XML string - * - * @return string|\DOMDocument An XML string representing the InputDefinition - * - * @deprecated since version 2.3, to be removed in 3.0. - */ - public function asXml($asDom = false) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); - - $descriptor = new XmlDescriptor(); - - if ($asDom) { - return $descriptor->getInputDefinitionDocument($this); - } - - $output = new BufferedOutput(); - $descriptor->describe($output, $this); - - return $output->fetch(); - } } diff --git a/src/Symfony/Component/Console/Input/InputInterface.php b/src/Symfony/Component/Console/Input/InputInterface.php index 4501260970a50..bc66466437fe2 100644 --- a/src/Symfony/Component/Console/Input/InputInterface.php +++ b/src/Symfony/Component/Console/Input/InputInterface.php @@ -34,11 +34,12 @@ public function getFirstArgument(); * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * - * @param string|array $values The values to look for in the raw parameters (can be an array) + * @param string|array $values The values to look for in the raw parameters (can be an array) + * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal * * @return bool true if the value is contained in the raw parameters */ - public function hasParameterOption($values); + public function hasParameterOption($values, $onlyParams = false); /** * Returns the value of a raw option (not parsed). @@ -46,12 +47,13 @@ public function hasParameterOption($values); * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * - * @param string|array $values The value(s) to look for in the raw parameters (can be an array) - * @param mixed $default The default value to return if no result is found + * @param string|array $values The value(s) to look for in the raw parameters (can be an array) + * @param mixed $default The default value to return if no result is found + * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal * * @return mixed The option value */ - public function getParameterOption($values, $default = false); + public function getParameterOption($values, $default = false, $onlyParams = false); /** * Binds the current Input instance with the given arguments and options. diff --git a/src/Symfony/Component/Console/Input/StreamableInputInterface.php b/src/Symfony/Component/Console/Input/StreamableInputInterface.php new file mode 100644 index 0000000000000..d7e462f244431 --- /dev/null +++ b/src/Symfony/Component/Console/Input/StreamableInputInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +/** + * StreamableInputInterface is the interface implemented by all input classes + * that have an input stream. + * + * @author Robin Chalas + */ +interface StreamableInputInterface extends InputInterface +{ + /** + * Sets the input stream to read from when interacting with the user. + * + * This is mainly useful for testing purpose. + * + * @param resource $stream The input stream + */ + public function setStream($stream); + + /** + * Returns the input stream. + * + * @return resource|null + */ + public function getStream(); +} diff --git a/src/Symfony/Component/Console/Input/StringInput.php b/src/Symfony/Component/Console/Input/StringInput.php index a40ddba31dec8..9ce021745f2a3 100644 --- a/src/Symfony/Component/Console/Input/StringInput.php +++ b/src/Symfony/Component/Console/Input/StringInput.php @@ -30,24 +30,13 @@ class StringInput extends ArgvInput /** * Constructor. * - * @param string $input An array of parameters from the CLI (in the argv format) - * @param InputDefinition $definition A InputDefinition instance - * - * @deprecated The second argument is deprecated as it does not work (will be removed in 3.0), use 'bind' method instead + * @param string $input An array of parameters from the CLI (in the argv format) */ - public function __construct($input, InputDefinition $definition = null) + public function __construct($input) { - if ($definition) { - @trigger_error('The $definition argument of the '.__METHOD__.' method is deprecated and will be removed in 3.0. Set this parameter with the bind() method instead.', E_USER_DEPRECATED); - } - - parent::__construct(array(), null); + parent::__construct(array()); $this->setTokens($this->tokenize($input)); - - if (null !== $definition) { - $this->bind($definition); - } } /** diff --git a/src/Symfony/Component/Console/Logger/ConsoleLogger.php b/src/Symfony/Component/Console/Logger/ConsoleLogger.php index 987e96a6587e5..208575cebf767 100644 --- a/src/Symfony/Component/Console/Logger/ConsoleLogger.php +++ b/src/Symfony/Component/Console/Logger/ConsoleLogger.php @@ -59,6 +59,7 @@ class ConsoleLogger extends AbstractLogger LogLevel::INFO => self::INFO, LogLevel::DEBUG => self::INFO, ); + private $errored = false; /** * @param OutputInterface $output @@ -81,18 +82,31 @@ public function log($level, $message, array $context = array()) throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); } + $output = $this->output; + // Write to the error output if necessary and available - if ($this->formatLevelMap[$level] === self::ERROR && $this->output instanceof ConsoleOutputInterface) { - $output = $this->output->getErrorOutput(); - } else { - $output = $this->output; + if ($this->formatLevelMap[$level] === self::ERROR) { + if ($this->output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + $this->errored = true; } + // the if condition check isn't necessary -- it's the same one that $output will do internally anyway. + // We only do it for efficiency here as the message formatting is relatively expensive. if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) { - $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context))); + $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]); } } + /** + * Returns true when any messages have been logged at error levels. + */ + public function hasErrored() + { + return $this->errored; + } + /** * Interpolates context values into the message placeholders. * diff --git a/src/Symfony/Component/Console/Output/ConsoleOutput.php b/src/Symfony/Component/Console/Output/ConsoleOutput.php index f666c793e2265..007f3f01be336 100644 --- a/src/Symfony/Component/Console/Output/ConsoleOutput.php +++ b/src/Symfony/Component/Console/Output/ConsoleOutput.php @@ -14,15 +14,16 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** - * ConsoleOutput is the default class for all CLI output. It uses STDOUT. + * ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR. * - * This class is a convenient wrapper around `StreamOutput`. + * This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR. * * $output = new ConsoleOutput(); * * This is equivalent to: * * $output = new StreamOutput(fopen('php://stdout', 'w')); + * $stdErr = new StreamOutput(fopen('php://stderr', 'w')); * * @author Fabien Potencier */ @@ -139,9 +140,11 @@ function_exists('php_uname') ? php_uname('s') : '', */ private function openOutputStream() { - $outputStream = $this->hasStdoutSupport() ? 'php://stdout' : 'php://output'; + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } - return @fopen($outputStream, 'w') ?: fopen('php://output', 'w'); + return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); } /** @@ -149,8 +152,6 @@ private function openOutputStream() */ private function openErrorStream() { - $errorStream = $this->hasStderrSupport() ? 'php://stderr' : 'php://output'; - - return fopen($errorStream, 'w'); + return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); } } diff --git a/src/Symfony/Component/Console/Output/OutputInterface.php b/src/Symfony/Component/Console/Output/OutputInterface.php index 9a8290bddd4d6..a291ca7d7e220 100644 --- a/src/Symfony/Component/Console/Output/OutputInterface.php +++ b/src/Symfony/Component/Console/Output/OutputInterface.php @@ -61,6 +61,34 @@ public function setVerbosity($level); */ public function getVerbosity(); + /** + * Returns whether verbosity is quiet (-q). + * + * @return bool true if verbosity is set to VERBOSITY_QUIET, false otherwise + */ + public function isQuiet(); + + /** + * Returns whether verbosity is verbose (-v). + * + * @return bool true if verbosity is set to VERBOSITY_VERBOSE, false otherwise + */ + public function isVerbose(); + + /** + * Returns whether verbosity is very verbose (-vv). + * + * @return bool true if verbosity is set to VERBOSITY_VERY_VERBOSE, false otherwise + */ + public function isVeryVerbose(); + + /** + * Returns whether verbosity is debug (-vvv). + * + * @return bool true if verbosity is set to VERBOSITY_DEBUG, false otherwise + */ + public function isDebug(); + /** * Sets the decorated flag. * diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index a618e7aa3ee53..6425cc5416b6b 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -164,7 +164,7 @@ public function setAutocompleterValues($values) * * @return $this */ - public function setValidator($validator) + public function setValidator(callable $validator = null) { $this->validator = $validator; @@ -224,7 +224,7 @@ public function getMaxAttempts() * * @return $this */ - public function setNormalizer($normalizer) + public function setNormalizer(callable $normalizer) { $this->normalizer = $normalizer; diff --git a/src/Symfony/Component/Console/Shell.php b/src/Symfony/Component/Console/Shell.php deleted file mode 100644 index dacdf223a1809..0000000000000 --- a/src/Symfony/Component/Console/Shell.php +++ /dev/null @@ -1,233 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Console; - -use Symfony\Component\Console\Exception\RuntimeException; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Process\ProcessBuilder; -use Symfony\Component\Process\PhpExecutableFinder; - -/** - * A Shell wraps an Application to add shell capabilities to it. - * - * Support for history and completion only works with a PHP compiled - * with readline support (either --with-readline or --with-libedit) - * - * @deprecated since version 2.8, to be removed in 3.0. - * - * @author Fabien Potencier - * @author Martin Hasoň - */ -class Shell -{ - private $application; - private $history; - private $output; - private $hasReadline; - private $processIsolation = false; - - /** - * Constructor. - * - * If there is no readline support for the current PHP executable - * a \RuntimeException exception is thrown. - * - * @param Application $application An application instance - */ - public function __construct(Application $application) - { - @trigger_error('The '.__CLASS__.' class is deprecated since Symfony 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - - $this->hasReadline = function_exists('readline'); - $this->application = $application; - $this->history = getenv('HOME').'/.history_'.$application->getName(); - $this->output = new ConsoleOutput(); - } - - /** - * Runs the shell. - */ - public function run() - { - $this->application->setAutoExit(false); - $this->application->setCatchExceptions(true); - - if ($this->hasReadline) { - readline_read_history($this->history); - readline_completion_function(array($this, 'autocompleter')); - } - - $this->output->writeln($this->getHeader()); - $php = null; - if ($this->processIsolation) { - $finder = new PhpExecutableFinder(); - $php = $finder->find(); - $this->output->writeln(<<<'EOF' -Running with process isolation, you should consider this: - * each command is executed as separate process, - * commands don't support interactivity, all params must be passed explicitly, - * commands output is not colorized. - -EOF - ); - } - - while (true) { - $command = $this->readline(); - - if (false === $command) { - $this->output->writeln("\n"); - - break; - } - - if ($this->hasReadline) { - readline_add_history($command); - readline_write_history($this->history); - } - - if ($this->processIsolation) { - $pb = new ProcessBuilder(); - - $process = $pb - ->add($php) - ->add($_SERVER['argv'][0]) - ->add($command) - ->inheritEnvironmentVariables(true) - ->getProcess() - ; - - $output = $this->output; - $process->run(function ($type, $data) use ($output) { - $output->writeln($data); - }); - - $ret = $process->getExitCode(); - } else { - $ret = $this->application->run(new StringInput($command), $this->output); - } - - if (0 !== $ret) { - $this->output->writeln(sprintf('The command terminated with an error status (%s)', $ret)); - } - } - } - - /** - * Returns the shell header. - * - * @return string The header string - */ - protected function getHeader() - { - return <<{$this->application->getName()} shell ({$this->application->getVersion()}). - -At the prompt, type help for some help, -or list to get a list of available commands. - -To exit the shell, type ^D. - -EOF; - } - - /** - * Renders a prompt. - * - * @return string The prompt - */ - protected function getPrompt() - { - // using the formatter here is required when using readline - return $this->output->getFormatter()->format($this->application->getName().' > '); - } - - protected function getOutput() - { - return $this->output; - } - - protected function getApplication() - { - return $this->application; - } - - /** - * Tries to return autocompletion for the current entered text. - * - * @param string $text The last segment of the entered text - * - * @return bool|array A list of guessed strings or true - */ - private function autocompleter($text) - { - $info = readline_info(); - $text = substr($info['line_buffer'], 0, $info['end']); - - if ($info['point'] !== $info['end']) { - return true; - } - - // task name? - if (false === strpos($text, ' ') || !$text) { - return array_keys($this->application->all()); - } - - // options and arguments? - try { - $command = $this->application->find(substr($text, 0, strpos($text, ' '))); - } catch (\Exception $e) { - return true; - } - - $list = array('--help'); - foreach ($command->getDefinition()->getOptions() as $option) { - $list[] = '--'.$option->getName(); - } - - return $list; - } - - /** - * Reads a single line from standard input. - * - * @return string The single line from standard input - */ - private function readline() - { - if ($this->hasReadline) { - $line = readline($this->getPrompt()); - } else { - $this->output->write($this->getPrompt()); - $line = fgets(STDIN, 1024); - $line = (false === $line || '' === $line) ? false : rtrim($line); - } - - return $line; - } - - public function getProcessIsolation() - { - return $this->processIsolation; - } - - public function setProcessIsolation($processIsolation) - { - $this->processIsolation = (bool) $processIsolation; - - if ($this->processIsolation && !class_exists('Symfony\\Component\\Process\\Process')) { - throw new RuntimeException('Unable to isolate processes as the Symfony Process Component is not installed.'); - } - } -} diff --git a/src/Symfony/Component/Console/Style/OutputStyle.php b/src/Symfony/Component/Console/Style/OutputStyle.php index 8371bb533551e..1274a98d13b6b 100644 --- a/src/Symfony/Component/Console/Style/OutputStyle.php +++ b/src/Symfony/Component/Console/Style/OutputStyle.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; /** * Decorates output to add console style guide helpers. @@ -113,4 +114,45 @@ public function getFormatter() { return $this->output->getFormatter(); } + + /** + * {@inheritdoc} + */ + public function isQuiet() + { + return $this->output->isQuiet(); + } + + /** + * {@inheritdoc} + */ + public function isVerbose() + { + return $this->output->isVerbose(); + } + + /** + * {@inheritdoc} + */ + public function isVeryVerbose() + { + return $this->output->isVeryVerbose(); + } + + /** + * {@inheritdoc} + */ + public function isDebug() + { + return $this->output->isDebug(); + } + + protected function getErrorOutput() + { + if (!$this->output instanceof ConsoleOutputInterface) { + return $this->output; + } + + return $this->output->getErrorOutput(); + } } diff --git a/src/Symfony/Component/Console/Style/SymfonyStyle.php b/src/Symfony/Component/Console/Style/SymfonyStyle.php index 9ae6513ceb031..a86796176717e 100644 --- a/src/Symfony/Component/Console/Style/SymfonyStyle.php +++ b/src/Symfony/Component/Console/Style/SymfonyStyle.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Console\Style; -use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; @@ -24,6 +23,7 @@ use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Terminal; /** * Output decorator helpers for the Symfony Style Guide. @@ -49,7 +49,8 @@ public function __construct(InputInterface $input, OutputInterface $output) $this->input = $input; $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. - $this->lineLength = min($this->getTerminalWidth() - (int) (DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); + $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; + $this->lineLength = min($width - (int) (DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); parent::__construct($output); } @@ -62,13 +63,14 @@ public function __construct(InputInterface $input, OutputInterface $output) * @param string|null $style The style to apply to the whole block * @param string $prefix The prefix for the block * @param bool $padding Whether to add vertical padding + * @param bool $escape Whether to escape the message */ - public function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false) + public function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = true) { $messages = is_array($messages) ? array_values($messages) : array($messages); $this->autoPrependBlock(); - $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, true)); + $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape)); $this->newLine(); } @@ -132,11 +134,7 @@ public function text($message) */ public function comment($message) { - $messages = is_array($message) ? array_values($message) : array($message); - - $this->autoPrependBlock(); - $this->writeln($this->createBlock($messages, null, null, ' // ')); - $this->newLine(); + $this->block($message, null, null, ' // ', false, false); } /** @@ -336,6 +334,16 @@ public function newLine($count = 1) $this->bufferedOutput->write(str_repeat("\n", $count)); } + /** + * Returns a new instance which makes use of stderr if available. + * + * @return self + */ + public function getErrorStyle() + { + return new self($this->input, $this->getErrorOutput()); + } + /** * @return ProgressBar */ @@ -348,14 +356,6 @@ private function getProgressBar() return $this->progressBar; } - private function getTerminalWidth() - { - $application = new Application(); - $dimensions = $application->getTerminalDimensions(); - - return $dimensions[0] ?: self::MAX_LINE_LENGTH; - } - private function autoPrependBlock() { $chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); diff --git a/src/Symfony/Component/Console/Terminal.php b/src/Symfony/Component/Console/Terminal.php new file mode 100644 index 0000000000000..927dfc4d76d71 --- /dev/null +++ b/src/Symfony/Component/Console/Terminal.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +class Terminal +{ + private static $width; + private static $height; + + /** + * Gets the terminal width. + * + * @return int + */ + public function getWidth() + { + $width = getenv('COLUMNS'); + if (false !== $width) { + return (int) trim($width); + } + + if (null === self::$width) { + self::initDimensions(); + } + + return self::$width ?: 80; + } + + /** + * Gets the terminal height. + * + * @return int + */ + public function getHeight() + { + $height = getenv('LINES'); + if (false !== $height) { + return (int) trim($height); + } + + if (null === self::$height) { + self::initDimensions(); + } + + return self::$height ?: 50; + } + + private static function initDimensions() + { + if ('\\' === DIRECTORY_SEPARATOR) { + if (preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim(getenv('ANSICON')), $matches)) { + // extract [w, H] from "wxh (WxH)" + // or [w, h] from "wxh" + self::$width = (int) $matches[1]; + self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2]; + } elseif (null !== $dimensions = self::getConsoleMode()) { + // extract [w, h] from "wxh" + self::$width = (int) $dimensions[0]; + self::$height = (int) $dimensions[1]; + } + } elseif ($sttyString = self::getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + // extract [w, h] from "rows h; columns w;" + self::$width = (int) $matches[2]; + self::$height = (int) $matches[1]; + } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + // extract [w, h] from "; h rows; w columns" + self::$width = (int) $matches[2]; + self::$height = (int) $matches[1]; + } + } + } + + /** + * Runs and parses mode CON if it's available, suppressing any error output. + * + * @return int[]|null An array composed of the width and the height or null if it could not be parsed + */ + private static function getConsoleMode() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = array( + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w'), + ); + $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return array((int) $matches[2], (int) $matches[1]); + } + } + } + + /** + * Runs and parses stty -a if it's available, suppressing any error output. + * + * @return string|null + */ + private static function getSttyColumns() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = array( + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w'), + ); + + $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } + } +} diff --git a/src/Symfony/Component/Console/Tester/ApplicationTester.php b/src/Symfony/Component/Console/Tester/ApplicationTester.php index 90efbab2182a8..c0f8c7207f2a8 100644 --- a/src/Symfony/Component/Console/Tester/ApplicationTester.php +++ b/src/Symfony/Component/Console/Tester/ApplicationTester.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; @@ -31,14 +32,13 @@ class ApplicationTester { private $application; private $input; - private $output; private $statusCode; - /** - * Constructor. - * - * @param Application $application An Application instance to test + * @var OutputInterface */ + private $output; + private $captureStreamsIndependently = false; + public function __construct(Application $application) { $this->application = $application; @@ -49,9 +49,10 @@ public function __construct(Application $application) * * Available options: * - * * interactive: Sets the input interactive flag - * * decorated: Sets the output decorated flag - * * verbosity: Sets the output verbosity flag + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * * capture_stderr_separately: Make output of stdOut and stdErr separately available * * @param array $input An array of arguments and options * @param array $options An array of options @@ -65,12 +66,35 @@ public function run(array $input, $options = array()) $this->input->setInteractive($options['interactive']); } - $this->output = new StreamOutput(fopen('php://memory', 'w', false)); - if (isset($options['decorated'])) { - $this->output->setDecorated($options['decorated']); - } - if (isset($options['verbosity'])) { - $this->output->setVerbosity($options['verbosity']); + $this->captureStreamsIndependently = array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately']; + if (!$this->captureStreamsIndependently) { + $this->output = new StreamOutput(fopen('php://memory', 'w', false)); + if (isset($options['decorated'])) { + $this->output->setDecorated($options['decorated']); + } + if (isset($options['verbosity'])) { + $this->output->setVerbosity($options['verbosity']); + } + } else { + $this->output = new ConsoleOutput( + isset($options['verbosity']) ? $options['verbosity'] : ConsoleOutput::VERBOSITY_NORMAL, + isset($options['decorated']) ? $options['decorated'] : null + ); + + $errorOutput = new StreamOutput(fopen('php://memory', 'w', false)); + $errorOutput->setFormatter($this->output->getFormatter()); + $errorOutput->setVerbosity($this->output->getVerbosity()); + $errorOutput->setDecorated($this->output->isDecorated()); + + $reflectedOutput = new \ReflectionObject($this->output); + $strErrProperty = $reflectedOutput->getProperty('stderr'); + $strErrProperty->setAccessible(true); + $strErrProperty->setValue($this->output, $errorOutput); + + $reflectedParent = $reflectedOutput->getParentClass(); + $streamProperty = $reflectedParent->getProperty('stream'); + $streamProperty->setAccessible(true); + $streamProperty->setValue($this->output, fopen('php://memory', 'w', false)); } return $this->statusCode = $this->application->run($this->input, $this->output); @@ -96,6 +120,30 @@ public function getDisplay($normalize = false) return $display; } + /** + * Gets the output written to STDERR by the application. + * + * @param bool $normalize Whether to normalize end of lines to \n or not + * + * @return string + */ + public function getErrorOutput($normalize = false) + { + if (!$this->captureStreamsIndependently) { + throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.'); + } + + rewind($this->output->getErrorOutput()->getStream()); + + $display = stream_get_contents($this->output->getErrorOutput()->getStream()); + + if ($normalize) { + $display = str_replace(PHP_EOL, "\n", $display); + } + + return $display; + } + /** * Gets the input instance used by the last execution of the application. * diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index 609f46a654da9..0bb1603c33c6f 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -21,12 +21,14 @@ * Eases the testing of console commands. * * @author Fabien Potencier + * @author Robin Chalas */ class CommandTester { private $command; private $input; private $output; + private $inputs = array(); private $statusCode; /** @@ -65,6 +67,10 @@ public function execute(array $input, array $options = array()) } $this->input = new ArrayInput($input); + if ($this->inputs) { + $this->input->setStream(self::createStream($this->inputs)); + } + if (isset($options['interactive'])) { $this->input->setInteractive($options['interactive']); } @@ -127,4 +133,29 @@ public function getStatusCode() { return $this->statusCode; } + + /** + * Sets the user inputs. + * + * @param array An array of strings representing each input + * passed to the command input stream. + * + * @return CommandTester + */ + public function setInputs(array $inputs) + { + $this->inputs = $inputs; + + return $this; + } + + private static function createStream(array $inputs) + { + $stream = fopen('php://memory', 'r+', false); + + fwrite($stream, implode(PHP_EOL, $inputs)); + rewind($stream); + + return $stream; + } } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 7c7a6be6ac1e8..840213b042509 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; +use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Input\ArgvInput; @@ -27,8 +30,10 @@ use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\Console\Event\ConsoleCommandEvent; -use Symfony\Component\Console\Event\ConsoleExceptionEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventDispatcher; class ApplicationTest extends TestCase @@ -45,6 +50,8 @@ public static function setUpBeforeClass() require_once self::$fixturesPath.'/Foo3Command.php'; require_once self::$fixturesPath.'/Foo4Command.php'; require_once self::$fixturesPath.'/Foo5Command.php'; + require_once self::$fixturesPath.'/FooSameCaseUppercaseCommand.php'; + require_once self::$fixturesPath.'/FooSameCaseLowercaseCommand.php'; require_once self::$fixturesPath.'/FoobarCommand.php'; require_once self::$fixturesPath.'/BarBucCommand.php'; require_once self::$fixturesPath.'/FooSubnamespaced1Command.php'; @@ -93,7 +100,7 @@ public function testSetGetVersion() public function testGetLongVersion() { $application = new Application('foo', 'bar'); - $this->assertEquals('foo version bar', $application->getLongVersion(), '->getLongVersion() returns the long version of the application'); + $this->assertEquals('foo bar', $application->getLongVersion(), '->getLongVersion() returns the long version of the application'); } public function testHelp() @@ -113,6 +120,25 @@ public function testAll() $this->assertCount(1, $commands, '->all() takes a namespace as its first argument'); } + public function testAllWithCommandLoader() + { + $application = new Application(); + $commands = $application->all(); + $this->assertInstanceOf('Symfony\\Component\\Console\\Command\\HelpCommand', $commands['help'], '->all() returns the registered commands'); + + $application->add(new \FooCommand()); + $commands = $application->all('foo'); + $this->assertCount(1, $commands, '->all() takes a namespace as its first argument'); + + $application->setCommandLoader(new FactoryCommandLoader(array( + 'foo:bar1' => function () { return new \Foo1Command(); }, + ))); + $commands = $application->all('foo'); + $this->assertCount(2, $commands, '->all() takes a namespace as its first argument'); + $this->assertInstanceOf(\FooCommand::class, $commands['foo:bar'], '->all() returns the registered commands'); + $this->assertInstanceOf(\Foo1Command::class, $commands['foo:bar1'], '->all() returns the registered commands'); + } + public function testRegister() { $application = new Application(); @@ -165,6 +191,30 @@ public function testHasGet() $this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $command, '->get() returns the help command if --help is provided as the input'); } + public function testHasGetWithCommandLoader() + { + $application = new Application(); + $this->assertTrue($application->has('list'), '->has() returns true if a named command is registered'); + $this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered'); + + $application->add($foo = new \FooCommand()); + $this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered'); + $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name'); + $this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias'); + + $application->setCommandLoader(new FactoryCommandLoader(array( + 'foo:bar1' => function () { return new \Foo1Command(); }, + ))); + + $this->assertTrue($application->has('afoobar'), '->has() returns true if an instance is registered for an alias even with command loader'); + $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns an instance by name even with command loader'); + $this->assertEquals($foo, $application->get('afoobar'), '->get() returns an instance by alias even with command loader'); + $this->assertTrue($application->has('foo:bar1'), '->has() returns true for commands registered in the loader'); + $this->assertInstanceOf(\Foo1Command::class, $foo1 = $application->get('foo:bar1'), '->get() returns a command by name from the command loader'); + $this->assertTrue($application->has('afoobar1'), '->has() returns true for commands registered in the loader'); + $this->assertEquals($foo1, $application->get('afoobar1'), '->get() returns a command by name from the command loader'); + } + public function testSilentHelp() { $application = new Application(); @@ -213,16 +263,22 @@ public function testFindNamespaceWithSubnamespaces() $this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns commands even if the commands are only contained in subnamespaces'); } - /** - * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException - * @expectedExceptionMessage The namespace "f" is ambiguous (foo, foo1). - */ public function testFindAmbiguousNamespace() { $application = new Application(); $application->add(new \BarBucCommand()); $application->add(new \FooCommand()); $application->add(new \Foo2Command()); + + $expectedMsg = "The namespace \"f\" is ambiguous.\nDid you mean one of these?\n foo\n foo1"; + + if (method_exists($this, 'expectException')) { + $this->expectException(CommandNotFoundException::class); + $this->expectExceptionMessage($expectedMsg); + } else { + $this->setExpectedException(CommandNotFoundException::class, $expectedMsg); + } + $application->findNamespace('f'); } @@ -262,6 +318,55 @@ public function testFind() $this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias'); } + public function testFindCaseSensitiveFirst() + { + $application = new Application(); + $application->add(new \FooSameCaseUppercaseCommand()); + $application->add(new \FooSameCaseLowercaseCommand()); + + $this->assertInstanceOf('FooSameCaseUppercaseCommand', $application->find('f:B'), '->find() returns a command if the abbreviation is the correct case'); + $this->assertInstanceOf('FooSameCaseUppercaseCommand', $application->find('f:BAR'), '->find() returns a command if the abbreviation is the correct case'); + $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:b'), '->find() returns a command if the abbreviation is the correct case'); + $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:bar'), '->find() returns a command if the abbreviation is the correct case'); + } + + public function testFindCaseInsensitiveAsFallback() + { + $application = new Application(); + $application->add(new \FooSameCaseLowercaseCommand()); + + $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:b'), '->find() returns a command if the abbreviation is the correct case'); + $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('f:B'), '->find() will fallback to case insensitivity'); + $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('FoO:BaR'), '->find() will fallback to case insensitivity'); + } + + /** + * @expectedException \Symfony\Component\Console\Exception\CommandNotFoundException + * @expectedExceptionMessage Command "FoO:BaR" is ambiguous + */ + public function testFindCaseInsensitiveSuggestions() + { + $application = new Application(); + $application->add(new \FooSameCaseLowercaseCommand()); + $application->add(new \FooSameCaseUppercaseCommand()); + + $this->assertInstanceOf('FooSameCaseLowercaseCommand', $application->find('FoO:BaR'), '->find() will find two suggestions with case insensitivity'); + } + + public function testFindWithCommandLoader() + { + $application = new Application(); + $application->setCommandLoader(new FactoryCommandLoader(array( + 'foo:bar' => $f = function () { return new \FooCommand(); }, + ))); + + $this->assertInstanceOf('FooCommand', $application->find('foo:bar'), '->find() returns a command if its name exists'); + $this->assertInstanceOf('Symfony\Component\Console\Command\HelpCommand', $application->find('h'), '->find() returns a command if its name exists'); + $this->assertInstanceOf('FooCommand', $application->find('f:bar'), '->find() returns a command if the abbreviation for the namespace exists'); + $this->assertInstanceOf('FooCommand', $application->find('f:b'), '->find() returns a command if the abbreviation for the namespace and the command name exist'); + $this->assertInstanceOf('FooCommand', $application->find('a'), '->find() returns a command if the abbreviation exists for an alias'); + } + /** * @dataProvider provideAmbiguousAbbreviations */ @@ -286,8 +391,20 @@ public function provideAmbiguousAbbreviations() { return array( array('f', 'Command "f" is not defined.'), - array('a', 'Command "a" is ambiguous (afoobar, afoobar1 and 1 more).'), - array('foo:b', 'Command "foo:b" is ambiguous (foo:bar, foo:bar1 and 1 more).'), + array( + 'a', + "Command \"a\" is ambiguous.\nDid you mean one of these?\n". + " afoobar The foo:bar command\n". + " afoobar1 The foo:bar1 command\n". + ' afoobar2 The foo1:bar command', + ), + array( + 'foo:b', + "Command \"foo:b\" is ambiguous.\nDid you mean one of these?\n". + " foo:bar The foo:bar command\n". + " foo:bar1 The foo:bar1 command\n". + ' foo1:bar The foo1:bar command', + ), ); } @@ -333,8 +450,8 @@ public function testFindAlternativeExceptionMessageSingle($name) public function provideInvalidCommandNamesSingle() { return array( - array('foo3:baR'), - array('foO3:bar'), + array('foo3:barr'), + array('fooo3:bar'), ); } @@ -459,6 +576,36 @@ public function testFindAlternativeNamespace() } } + public function testFindAlternativesOutput() + { + $application = new Application(); + + $application->add(new \FooCommand()); + $application->add(new \Foo1Command()); + $application->add(new \Foo2Command()); + $application->add(new \Foo3Command()); + + $expectedAlternatives = array( + 'afoobar', + 'afoobar1', + 'afoobar2', + 'foo1:bar', + 'foo3:bar', + 'foo:bar', + 'foo:bar1', + ); + + try { + $application->find('foo'); + $this->fail('->find() throws a CommandNotFoundException if command is not defined'); + } catch (\Exception $e) { + $this->assertInstanceOf('Symfony\Component\Console\Exception\CommandNotFoundException', $e, '->find() throws a CommandNotFoundException if command is not defined'); + $this->assertSame($expectedAlternatives, $e->getAlternatives()); + + $this->assertRegExp('/Command "foo" is not defined\..*Did you mean one of these\?.*/Ums', $e->getMessage()); + } + } + public function testFindNamespaceDoesNotFailOnDeepSimilarNamespaces() { $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getNamespaces'))->getMock(); @@ -483,17 +630,21 @@ public function testFindWithDoubleColonInNameThrowsException() public function testSetCatchExceptions() { - $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock(); + $application = new Application(); $application->setAutoExit(false); - $application->expects($this->any()) - ->method('getTerminalWidth') - ->will($this->returnValue(120)); + putenv('COLUMNS=120'); $tester = new ApplicationTester($application); $application->setCatchExceptions(true); + $this->assertTrue($application->areExceptionsCaught()); + $tester->run(array('command' => 'foo'), array('decorated' => false)); $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->setCatchExceptions() sets the catch exception flag'); + $tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->setCatchExceptions() sets the catch exception flag'); + $this->assertSame('', $tester->getDisplay(true)); + $application->setCatchExceptions(false); try { $tester->run(array('command' => 'foo'), array('decorated' => false)); @@ -504,105 +655,90 @@ public function testSetCatchExceptions() } } - /** - * @group legacy - */ - public function testLegacyAsText() + public function testAutoExitSetting() { $application = new Application(); - $application->add(new \FooCommand()); - $this->ensureStaticCommandHelp($application); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_astext1.txt', $this->normalizeLineBreaks($application->asText()), '->asText() returns a text representation of the application'); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_astext2.txt', $this->normalizeLineBreaks($application->asText('foo')), '->asText() returns a text representation of the application'); - } + $this->assertTrue($application->isAutoExitEnabled()); - /** - * @group legacy - */ - public function testLegacyAsXml() - { - $application = new Application(); - $application->add(new \FooCommand()); - $this->ensureStaticCommandHelp($application); - $this->assertXmlStringEqualsXmlFile(self::$fixturesPath.'/application_asxml1.txt', $application->asXml(), '->asXml() returns an XML representation of the application'); - $this->assertXmlStringEqualsXmlFile(self::$fixturesPath.'/application_asxml2.txt', $application->asXml('foo'), '->asXml() returns an XML representation of the application'); + $application->setAutoExit(false); + $this->assertFalse($application->isAutoExitEnabled()); } public function testRenderException() { - $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock(); + $application = new Application(); $application->setAutoExit(false); - $application->expects($this->any()) - ->method('getTerminalWidth') - ->will($this->returnValue(120)); + putenv('COLUMNS=120'); $tester = new ApplicationTester($application); - $tester->run(array('command' => 'foo'), array('decorated' => false)); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getDisplay(true), '->renderException() renders a pretty exception'); + $tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exception'); - $tester->run(array('command' => 'foo'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE)); - $this->assertContains('Exception trace', $tester->getDisplay(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose'); + $tester->run(array('command' => 'foo'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE, 'capture_stderr_separately' => true)); + $this->assertContains('Exception trace', $tester->getErrorOutput(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose'); - $tester->run(array('command' => 'list', '--foo' => true), array('decorated' => false)); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getDisplay(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command'); + $tester->run(array('command' => 'list', '--foo' => true), array('decorated' => false, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getErrorOutput(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command'); $application->add(new \Foo3Command()); $tester = new ApplicationTester($application); - $tester->run(array('command' => 'foo3:bar'), array('decorated' => false)); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions'); + $tester->run(array('command' => 'foo3:bar'), array('decorated' => false, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions'); + + $tester->run(array('command' => 'foo3:bar'), array('decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE)); + $this->assertRegExp('/\[Exception\]\s*First exception/', $tester->getDisplay(), '->renderException() renders a pretty exception without code exception when code exception is default and verbosity is verbose'); + $this->assertRegExp('/\[Exception\]\s*Second exception/', $tester->getDisplay(), '->renderException() renders a pretty exception without code exception when code exception is 0 and verbosity is verbose'); + $this->assertRegExp('/\[Exception \(404\)\]\s*Third exception/', $tester->getDisplay(), '->renderException() renders a pretty exception with code exception when code exception is 404 and verbosity is verbose'); $tester->run(array('command' => 'foo3:bar'), array('decorated' => true)); $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3decorated.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions'); - $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock(); + $tester->run(array('command' => 'foo3:bar'), array('decorated' => true, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3decorated.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions'); + + $application = new Application(); $application->setAutoExit(false); - $application->expects($this->any()) - ->method('getTerminalWidth') - ->will($this->returnValue(32)); + putenv('COLUMNS=32'); $tester = new ApplicationTester($application); - $tester->run(array('command' => 'foo'), array('decorated' => false)); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal'); + $tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception4.txt', $tester->getErrorOutput(true), '->renderException() wraps messages when they are bigger than the terminal'); + putenv('COLUMNS=120'); } public function testRenderExceptionWithDoubleWidthCharacters() { - $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock(); + $application = new Application(); $application->setAutoExit(false); - $application->expects($this->any()) - ->method('getTerminalWidth') - ->will($this->returnValue(120)); + putenv('COLUMNS=120'); $application->register('foo')->setCode(function () { throw new \Exception('エラーメッセージ'); }); $tester = new ApplicationTester($application); - $tester->run(array('command' => 'foo'), array('decorated' => false)); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions'); + $tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions'); - $tester->run(array('command' => 'foo'), array('decorated' => true)); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getDisplay(true), '->renderException() renders a pretty exceptions with previous exceptions'); + $tester->run(array('command' => 'foo'), array('decorated' => true, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth1decorated.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions'); - $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock(); + $application = new Application(); $application->setAutoExit(false); - $application->expects($this->any()) - ->method('getTerminalWidth') - ->will($this->returnValue(32)); + putenv('COLUMNS=32'); $application->register('foo')->setCode(function () { throw new \Exception('コマンドの実行中にエラーが発生しました。'); }); $tester = new ApplicationTester($application); - $tester->run(array('command' => 'foo'), array('decorated' => false)); - $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getDisplay(true), '->renderException() wraps messages when they are bigger than the terminal'); + $tester->run(array('command' => 'foo'), array('decorated' => false, 'capture_stderr_separately' => true)); + $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_doublewidth2.txt', $tester->getErrorOutput(true), '->renderException() wraps messages when they are bigger than the terminal'); + putenv('COLUMNS=120'); } public function testRenderExceptionEscapesLines() { - $application = $this->getMockBuilder('Symfony\Component\Console\Application')->setMethods(array('getTerminalWidth'))->getMock(); + $application = new Application(); $application->setAutoExit(false); - $application->expects($this->any()) - ->method('getTerminalWidth') - ->will($this->returnValue(22)); + putenv('COLUMNS=22'); $application->register('foo')->setCode(function () { throw new \Exception('dont break here !'); }); @@ -610,6 +746,7 @@ public function testRenderExceptionEscapesLines() $tester->run(array('command' => 'foo'), array('decorated' => false)); $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception_escapeslines.txt', $tester->getDisplay(true), '->renderException() escapes lines containing formatting'); + putenv('COLUMNS=120'); } public function testRun() @@ -828,8 +965,6 @@ public function testGetDefaultHelperSetReturnsDefaultValues() $helperSet = $application->getHelperSet(); $this->assertTrue($helperSet->has('formatter')); - $this->assertTrue($helperSet->has('dialog')); - $this->assertTrue($helperSet->has('progress')); } public function testAddingSingleHelperSetOverwritesDefaultValues() @@ -948,7 +1083,7 @@ public function testRunWithDispatcher() /** * @expectedException \LogicException - * @expectedExceptionMessage caught + * @expectedExceptionMessage error */ public function testRunWithExceptionAndDispatcher() { @@ -979,7 +1114,7 @@ public function testRunDispatchesAllEventsWithException() $tester = new ApplicationTester($application); $tester->run(array('command' => 'foo')); - $this->assertContains('before.foo.caught.after.', $tester->getDisplay()); + $this->assertContains('before.foo.error.after.', $tester->getDisplay()); } public function testRunDispatchesAllEventsWithExceptionInListener() @@ -999,7 +1134,7 @@ public function testRunDispatchesAllEventsWithExceptionInListener() $tester = new ApplicationTester($application); $tester->run(array('command' => 'foo')); - $this->assertContains('before.caught.after.', $tester->getDisplay()); + $this->assertContains('before.error.after.', $tester->getDisplay()); } public function testRunWithError() @@ -1024,9 +1159,76 @@ public function testRunWithError() } } + public function testRunAllowsErrorListenersToSilenceTheException() + { + $dispatcher = $this->getDispatcher(); + $dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) { + $event->getOutput()->write('silenced.'); + + $event->setExitCode(0); + }); + + $dispatcher->addListener('console.command', function () { + throw new \RuntimeException('foo'); + }); + + $application = new Application(); + $application->setDispatcher($dispatcher); + $application->setAutoExit(false); + + $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) { + $output->write('foo.'); + }); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'foo')); + $this->assertContains('before.error.silenced.after.', $tester->getDisplay()); + $this->assertEquals(ConsoleCommandEvent::RETURN_CODE_DISABLED, $tester->getStatusCode()); + } + + public function testConsoleErrorEventIsTriggeredOnCommandNotFound() + { + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) { + $this->assertNull($event->getCommand()); + $this->assertInstanceOf(CommandNotFoundException::class, $event->getError()); + $event->getOutput()->write('silenced command not found'); + }); + + $application = new Application(); + $application->setDispatcher($dispatcher); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'unknown')); + $this->assertContains('silenced command not found', $tester->getDisplay()); + $this->assertEquals(1, $tester->getStatusCode()); + } + + public function testErrorIsRethrownIfNotHandledByConsoleErrorEvent() + { + $application = new Application(); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + $application->setDispatcher(new EventDispatcher()); + + $application->register('dym')->setCode(function (InputInterface $input, OutputInterface $output) { + new \UnknownClass(); + }); + + $tester = new ApplicationTester($application); + + try { + $tester->run(array('command' => 'dym')); + $this->fail('->run() should rethrow PHP errors if not handled via ConsoleErrorEvent.'); + } catch (\Error $e) { + $this->assertSame($e->getMessage(), 'Class \'UnknownClass\' not found'); + } + } + /** * @expectedException \LogicException - * @expectedExceptionMessage caught + * @expectedExceptionMessage error */ public function testRunWithErrorAndDispatcher() { @@ -1043,7 +1245,7 @@ public function testRunWithErrorAndDispatcher() $tester = new ApplicationTester($application); $tester->run(array('command' => 'dym')); - $this->assertContains('before.dym.caught.after.', $tester->getDisplay(), 'The PHP Error did not dispached events'); + $this->assertContains('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events'); } public function testRunDispatchesAllEventsWithError() @@ -1060,7 +1262,7 @@ public function testRunDispatchesAllEventsWithError() $tester = new ApplicationTester($application); $tester->run(array('command' => 'dym')); - $this->assertContains('before.dym.caught.after.', $tester->getDisplay(), 'The PHP Error did not dispached events'); + $this->assertContains('before.dym.error.after.', $tester->getDisplay(), 'The PHP Error did not dispached events'); } public function testRunWithErrorFailingStatusCode() @@ -1153,21 +1355,6 @@ public function testRunWithDispatcherAddingInputOptions() $this->assertEquals('some test value', $extraValue); } - public function testTerminalDimensions() - { - $application = new Application(); - $originalDimensions = $application->getTerminalDimensions(); - $this->assertCount(2, $originalDimensions); - - $width = 80; - if ($originalDimensions[0] == $width) { - $width = 100; - } - - $application->setTerminalDimensions($width, 80); - $this->assertSame(array($width, 80), $application->getTerminalDimensions()); - } - public function testSetRunCustomDefaultCommand() { $command = new \FooCommand(); @@ -1205,6 +1392,24 @@ public function testSetRunCustomDefaultCommandWithOption() $this->assertEquals('called'.PHP_EOL.'opt'.PHP_EOL, $tester->getDisplay(), 'Application runs the default set command if different from \'list\' command'); } + public function testSetRunCustomSingleCommand() + { + $command = new \FooCommand(); + + $application = new Application(); + $application->setAutoExit(false); + $application->add($command); + $application->setDefaultCommand($command->getName(), true); + + $tester = new ApplicationTester($application); + + $tester->run(array()); + $this->assertContains('called', $tester->getDisplay()); + + $tester->run(array('--help' => true)); + $this->assertContains('The foo:bar command', $tester->getDisplay()); + } + /** * @requires function posix_isatty */ @@ -1218,10 +1423,40 @@ public function testCanCheckIfTerminalIsInteractive() $this->assertFalse($tester->getInput()->hasParameterOption(array('--no-interaction', '-n'))); - $inputStream = $application->getHelperSet()->get('question')->getInputStream(); + $inputStream = $tester->getInput()->getStream(); $this->assertEquals($tester->getInput()->isInteractive(), @posix_isatty($inputStream)); } + public function testRunLazyCommandService() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddConsoleCommandPass()); + $container + ->register('lazy-command', LazyCommand::class) + ->addTag('console.command', array('command' => 'lazy:command')) + ->addTag('console.command', array('command' => 'lazy:alias')) + ->addTag('console.command', array('command' => 'lazy:alias2')); + $container->compile(); + + $application = new Application(); + $application->setCommandLoader($container->get('console.command_loader')); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + + $tester->run(array('command' => 'lazy:command')); + $this->assertSame("lazy-command called\n", $tester->getDisplay(true)); + + $tester->run(array('command' => 'lazy:alias')); + $this->assertSame("lazy-command called\n", $tester->getDisplay(true)); + + $tester->run(array('command' => 'lazy:alias2')); + $this->assertSame("lazy-command called\n", $tester->getDisplay(true)); + + $command = $application->get('lazy:command'); + $this->assertSame(array('lazy:alias', 'lazy:alias2'), $command->getAliases()); + } + protected function getDispatcher($skipCommand = false) { $dispatcher = new EventDispatcher(); @@ -1239,18 +1474,15 @@ protected function getDispatcher($skipCommand = false) $event->setExitCode(ConsoleCommandEvent::RETURN_CODE_DISABLED); } }); - $dispatcher->addListener('console.exception', function (ConsoleExceptionEvent $event) { - $event->getOutput()->write('caught.'); + $dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) { + $event->getOutput()->write('error.'); - $event->setException(new \LogicException('caught.', $event->getExitCode(), $event->getException())); + $event->setError(new \LogicException('error.', $event->getExitCode(), $event->getError())); }); return $dispatcher; } - /** - * @requires PHP 7 - */ public function testErrorIsRethrownIfNotHandledByConsoleErrorEventWithCatchingEnabled() { $application = new Application(); @@ -1309,3 +1541,11 @@ public function __construct() $this->setDefaultCommand($command->getName()); } } + +class LazyCommand extends Command +{ + public function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('lazy-command called'); + } +} diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index ac4aa22fbb772..0e51632162a1f 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -46,7 +46,7 @@ public function testConstructor() */ public function testCommandNameCannotBeEmpty() { - new Command(); + (new Application())->add(new Command()); } public function testSetApplication() @@ -93,6 +93,13 @@ public function testAddOption() $this->assertTrue($command->getDefinition()->hasOption('foo'), '->addOption() adds an option to the command'); } + public function testSetHidden() + { + $command = new \TestCommand(); + $command->setHidden(true); + $this->assertTrue($command->isHidden()); + } + public function testGetNamespaceGetNameSetName() { $command = new \TestCommand(); @@ -363,7 +370,6 @@ public function getSetCodeBindToClosureTests() /** * @dataProvider getSetCodeBindToClosureTests - * @requires PHP 5.4 */ public function testSetCodeBindToClosure($previouslyBound, $expected) { @@ -386,13 +392,7 @@ public function testSetCodeWithStaticClosure() $tester = new CommandTester($command); $tester->execute(array()); - if (\PHP_VERSION_ID < 70000) { - // Cannot bind static closures in PHP 5 - $this->assertEquals('interact called'.PHP_EOL.'not bound'.PHP_EOL, $tester->getDisplay()); - } else { - // Can bind static closures in PHP 7 - $this->assertEquals('interact called'.PHP_EOL.'bound'.PHP_EOL, $tester->getDisplay()); - } + $this->assertEquals('interact called'.PHP_EOL.'bound'.PHP_EOL, $tester->getDisplay()); } private static function createClosure() @@ -412,44 +412,10 @@ public function testSetCodeWithNonClosureCallable() $this->assertEquals('interact called'.PHP_EOL.'from the code...'.PHP_EOL, $tester->getDisplay()); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid callable provided to Command::setCode. - */ - public function testSetCodeWithNonCallable() - { - $command = new \TestCommand(); - $command->setCode(array($this, 'nonExistentMethod')); - } - public function callableMethodCommand(InputInterface $input, OutputInterface $output) { $output->writeln('from the code...'); } - - /** - * @group legacy - */ - public function testLegacyAsText() - { - $command = new \TestCommand(); - $command->setApplication(new Application()); - $tester = new CommandTester($command); - $tester->execute(array('command' => $command->getName())); - $this->assertStringEqualsFile(self::$fixturesPath.'/command_astext.txt', $command->asText(), '->asText() returns a text representation of the command'); - } - - /** - * @group legacy - */ - public function testLegacyAsXml() - { - $command = new \TestCommand(); - $command->setApplication(new Application()); - $tester = new CommandTester($command); - $tester->execute(array('command' => $command->getName())); - $this->assertXmlStringEqualsXmlFile(self::$fixturesPath.'/command_asxml.txt', $command->asXml(), '->asXml() returns an XML representation of the command'); - } } // In order to get an unbound closure, we should create it outside a class diff --git a/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php b/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php index a4e032a27e762..4d618ac16078e 100644 --- a/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php @@ -65,7 +65,7 @@ public function testExecuteForApplicationCommandWithXmlOption() $application = new Application(); $commandTester = new CommandTester($application->get('help')); $commandTester->execute(array('command_name' => 'list', '--format' => 'xml')); - $this->assertContains('list [--xml] [--raw] [--format FORMAT] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command'); + $this->assertContains('list [--raw] [--format FORMAT] [--] [<namespace>]', $commandTester->getDisplay(), '->execute() returns a text help for the given command'); $this->assertContains('getDisplay(), '->execute() returns an XML help text if --format=xml is passed'); } } diff --git a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php index fb6ee3bbacad1..64478ecc0d210 100644 --- a/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/ListCommandTest.php @@ -31,7 +31,7 @@ public function testExecuteListsCommandsWithXmlOption() $application = new Application(); $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(array('command' => $command->getName(), '--format' => 'xml')); - $this->assertRegExp('//', $commandTester->getDisplay(), '->execute() returns a list of available commands in XML if --xml is passed'); + $this->assertRegExp('/
      \n"; } } catch (\Exception $e) { // something nasty happened and we cannot throw an exception anymore @@ -295,9 +255,19 @@ public function getContent(FlattenException $exception) } } + $symfonyGhostImageContents = $this->getSymfonyGhostAsSvg(); + return << -

      $title

      +
      +
      +
      +

      $title

      +
      $symfonyGhostImageContents
      +
      +
      +
      + +
      $content
      EOF; @@ -313,58 +283,51 @@ public function getContent(FlattenException $exception) public function getStylesheet(FlattenException $exception) { return <<<'EOF' - .sf-reset { font: 11px Verdana, Arial, sans-serif; color: #333 } - .sf-reset .clear { clear:both; height:0; font-size:0; line-height:0; } - .sf-reset .clear_fix:after { display:block; height:0; clear:both; visibility:hidden; } - .sf-reset .clear_fix { display:inline-block; } - .sf-reset * html .clear_fix { height:1%; } - .sf-reset .clear_fix { display:block; } - .sf-reset, .sf-reset .block { margin: auto } - .sf-reset abbr { border-bottom: 1px dotted #000; cursor: help; } - .sf-reset p { font-size:14px; line-height:20px; color:#868686; padding-bottom:20px } - .sf-reset strong { font-weight:bold; } - .sf-reset a { color:#6c6159; cursor: default; } - .sf-reset a img { border:none; } - .sf-reset a:hover { text-decoration:underline; } - .sf-reset em { font-style:italic; } - .sf-reset h1, .sf-reset h2 { font: 20px Georgia, "Times New Roman", Times, serif } - .sf-reset .exception_counter { background-color: #fff; color: #333; padding: 6px; float: left; margin-right: 10px; float: left; display: block; } - .sf-reset .exception_title { margin-left: 3em; margin-bottom: 0.7em; display: block; } - .sf-reset .exception_message { margin-left: 3em; display: block; } - .sf-reset .traces li { font-size:12px; padding: 2px 4px; list-style-type:decimal; margin-left:20px; } - .sf-reset .block { background-color:#FFFFFF; padding:10px 28px; margin-bottom:20px; - -webkit-border-bottom-right-radius: 16px; - -webkit-border-bottom-left-radius: 16px; - -moz-border-radius-bottomright: 16px; - -moz-border-radius-bottomleft: 16px; - border-bottom-right-radius: 16px; - border-bottom-left-radius: 16px; - border-bottom:1px solid #ccc; - border-right:1px solid #ccc; - border-left:1px solid #ccc; - word-wrap: break-word; - } - .sf-reset .block_exception { background-color:#ddd; color: #333; padding:20px; - -webkit-border-top-left-radius: 16px; - -webkit-border-top-right-radius: 16px; - -moz-border-radius-topleft: 16px; - -moz-border-radius-topright: 16px; - border-top-left-radius: 16px; - border-top-right-radius: 16px; - border-top:1px solid #ccc; - border-right:1px solid #ccc; - border-left:1px solid #ccc; - overflow: hidden; - word-wrap: break-word; - } - .sf-reset a { background:none; color:#868686; text-decoration:none; } - .sf-reset a:hover { background:none; color:#313131; text-decoration:underline; } - .sf-reset ol { padding: 10px 0; } - .sf-reset h1 { background-color:#FFFFFF; padding: 15px 28px; margin-bottom: 20px; - -webkit-border-radius: 10px; - -moz-border-radius: 10px; - border-radius: 10px; - border: 1px solid #ccc; + body { background-color: #F9F9F9; color: #222; font: 14px/1.4 Helvetica, Arial, sans-serif; margin: 0; padding-bottom: 45px; } + + a { cursor: pointer; text-decoration: none; } + a:hover { text-decoration: underline; } + abbr[title] { border-bottom: none; cursor: help; text-decoration: none; } + + code, pre { font: 13px/1.5 Consolas, Monaco, Menlo, "Ubuntu Mono", "Liberation Mono", monospace; } + + table, tr, th, td { background: #FFF; border-collapse: collapse; vertical-align: top; } + table { background: #FFF; border: 1px solid #E0E0E0; box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; width: 100%; } + table th, table td { border: solid #E0E0E0; border-width: 1px 0; padding: 8px 10px; } + table th { background-color: #E0E0E0; font-weight: bold; text-align: left; } + + .hidden-xs-down { display: none; } + .block { display: block; } + .break-long-words { -ms-word-break: break-all; word-break: break-all; word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; } + .text-muted { color: #999; } + + .container { max-width: 1024px; margin: 0 auto; padding: 0 15px; } + .container::after { content: ""; display: table; clear: both; } + + .exception-summary { background: #B0413E; border-bottom: 2px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, .3); flex: 0 0 auto; margin-bottom: 30px; } + + .exception-message-wrapper { display: flex; align-items: center; min-height: 70px; } + .exception-message { flex-grow: 1; padding: 30px 0; } + .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } + .exception-message.long { font-size: 18px; } + .exception-message a { text-decoration: none; } + .exception-message a:hover { text-decoration: underline; } + + .exception-illustration { flex-basis: 111px; flex-shrink: 0; height: 66px; margin-left: 15px; opacity: .7; } + + .trace + .trace { margin-top: 30px; } + .trace-head .trace-class { color: #222; font-size: 18px; font-weight: bold; line-height: 1.3; margin: 0; position: relative; } + + .trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } + + .trace-file-path, .trace-file-path a { color: #222; margin-top: 3px; font-size: 13px; } + .trace-class { color: #B0413E; } + .trace-type { padding: 0 2px; } + .trace-method { color: #B0413E; font-weight: bold; } + .trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } + + @media (min-width: 575px) { + .hidden-xs-down { display: initial; } } EOF; } @@ -377,15 +340,7 @@ private function decorate($content, $css) - + $content @@ -403,16 +358,14 @@ private function formatClass($class) private function formatPath($path, $line) { - $path = $this->escapeHtml($path); - $file = preg_match('#[^/\\\\]*$#', $path, $file) ? $file[0] : $path; - - if ($linkFormat = $this->fileLinkFormat) { - $link = strtr($this->escapeHtml($linkFormat), array('%f' => $path, '%l' => (int) $line)); + $file = $this->escapeHtml(preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path); + $fmt = $this->fileLinkFormat; - return sprintf(' in
      %s line %d', $link, $file, $line); + if ($fmt && $link = is_string($fmt) ? strtr($fmt, array('%f' => $path, '%l' => $line)) : $fmt->format($path, $line)) { + return sprintf('in %s (line %d)', $this->escapeHtml($link), $file, $line); } - return sprintf(' in %s line %d', $path, $file, $line); + return sprintf('in %s (line %d)', $this->escapeHtml($path), $file, $line); } /** @@ -430,8 +383,6 @@ private function formatArgs(array $args) $formattedValue = sprintf('object(%s)', $this->formatClass($item[1])); } elseif ('array' === $item[0]) { $formattedValue = sprintf('array(%s)', is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); - } elseif ('string' === $item[0]) { - $formattedValue = sprintf("'%s'", $this->escapeHtml($item[1])); } elseif ('null' === $item[0]) { $formattedValue = 'null'; } elseif ('boolean' === $item[0]) { @@ -439,7 +390,7 @@ private function formatArgs(array $args) } elseif ('resource' === $item[0]) { $formattedValue = 'resource'; } else { - $formattedValue = str_replace("\n", '', var_export($this->escapeHtml((string) $item[1]), true)); + $formattedValue = str_replace("\n", '', $this->escapeHtml(var_export($item[1], true))); } $result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeHtml($key), $formattedValue); @@ -448,49 +399,16 @@ private function formatArgs(array $args) return implode(', ', $result); } - /** - * Returns an UTF-8 and HTML encoded string. - * - * @deprecated since version 2.7, to be removed in 3.0. - */ - protected static function utf8Htmlize($str) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); - - return htmlspecialchars($str, ENT_QUOTES | (\PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), 'UTF-8'); - } - /** * HTML-encodes a string. */ private function escapeHtml($str) { - return htmlspecialchars($str, ENT_QUOTES | (\PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), $this->charset); - } - - /** - * @internal - */ - public function catchOutput($buffer) - { - $this->caughtBuffer = $buffer; - - return ''; + return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); } - /** - * @internal - */ - public function cleanOutput($buffer) + private function getSymfonyGhostAsSvg() { - if ($this->caughtLength) { - // use substr_replace() instead of substr() for mbstring overloading resistance - $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength); - if (isset($cleanBuffer[0])) { - $buffer = $cleanBuffer; - } - } - - return $buffer; + return ''; } } diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php index 612bfcaf7f966..32ba9a09c5777 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -16,7 +16,6 @@ use Symfony\Component\Debug\DebugClassLoader; use Composer\Autoload\ClassLoader as ComposerClassLoader; use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; -use Symfony\Component\ClassLoader\UniversalClassLoader as SymfonyUniversalClassLoader; /** * ErrorHandler for classes that do not exist. @@ -101,17 +100,12 @@ private function getClassCandidates($class) if ($function[0] instanceof DebugClassLoader) { $function = $function[0]->getClassLoader(); - // @deprecated since version 2.5. Returning an object from DebugClassLoader::getClassLoader() is deprecated. - if (is_object($function)) { - $function = array($function); - } - if (!is_array($function)) { continue; } } - if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader || $function[0] instanceof SymfonyUniversalClassLoader) { + if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) { foreach ($function[0]->getPrefixes() as $prefix => $paths) { foreach ($paths as $path) { $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); @@ -207,6 +201,6 @@ private function convertFileToClass($path, $file, $prefix) */ private function classExists($class) { - return class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false)); + return class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); } } diff --git a/src/Symfony/Component/Debug/Resources/ext/README.md b/src/Symfony/Component/Debug/Resources/ext/README.md deleted file mode 100644 index 25dccf0766470..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/README.md +++ /dev/null @@ -1,134 +0,0 @@ -Symfony Debug Extension for PHP 5 -================================= - -This extension publishes several functions to help building powerful debugging tools. -It is compatible with PHP 5.3, 5.4, 5.5 and 5.6; with ZTS and non-ZTS modes. -It is not required thus not provided for PHP 7. - -symfony_zval_info() -------------------- - -- exposes zval_hash/refcounts, allowing e.g. efficient exploration of arbitrary structures in PHP, -- does work with references, preventing memory copying. - -Its behavior is about the same as: - -```php - gettype($array[$key]), - 'zval_hash' => /* hashed memory address of $array[$key] */, - 'zval_refcount' => /* internal zval refcount of $array[$key] */, - 'zval_isref' => /* is_ref status of $array[$key] */, - ); - - switch ($info['type']) { - case 'object': - $info += array( - 'object_class' => get_class($array[$key]), - 'object_refcount' => /* internal object refcount of $array[$key] */, - 'object_hash' => spl_object_hash($array[$key]), - 'object_handle' => /* internal object handle $array[$key] */, - ); - break; - - case 'resource': - $info += array( - 'resource_handle' => (int) $array[$key], - 'resource_type' => get_resource_type($array[$key]), - 'resource_refcount' => /* internal resource refcount of $array[$key] */, - ); - break; - - case 'array': - $info += array( - 'array_count' => count($array[$key]), - ); - break; - - case 'string': - $info += array( - 'strlen' => strlen($array[$key]), - ); - break; - } - - return $info; -} -``` - -symfony_debug_backtrace() -------------------------- - -This function works like debug_backtrace(), except that it can fetch the full backtrace in case of fatal errors: - -```php -function foo() { fatal(); } -function bar() { foo(); } - -function sd() { var_dump(symfony_debug_backtrace()); } - -register_shutdown_function('sd'); - -bar(); - -/* Will output -Fatal error: Call to undefined function fatal() in foo.php on line 42 -array(3) { - [0]=> - array(2) { - ["function"]=> - string(2) "sd" - ["args"]=> - array(0) { - } - } - [1]=> - array(4) { - ["file"]=> - string(7) "foo.php" - ["line"]=> - int(1) - ["function"]=> - string(3) "foo" - ["args"]=> - array(0) { - } - } - [2]=> - array(4) { - ["file"]=> - string(102) "foo.php" - ["line"]=> - int(2) - ["function"]=> - string(3) "bar" - ["args"]=> - array(0) { - } - } -} -*/ -``` - -Usage ------ - -To enable the extension from source, run: - -``` - phpize - ./configure - make - sudo make install -``` diff --git a/src/Symfony/Component/Debug/Resources/ext/config.m4 b/src/Symfony/Component/Debug/Resources/ext/config.m4 deleted file mode 100644 index 3c56047150569..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/config.m4 +++ /dev/null @@ -1,63 +0,0 @@ -dnl $Id$ -dnl config.m4 for extension symfony_debug - -dnl Comments in this file start with the string 'dnl'. -dnl Remove where necessary. This file will not work -dnl without editing. - -dnl If your extension references something external, use with: - -dnl PHP_ARG_WITH(symfony_debug, for symfony_debug support, -dnl Make sure that the comment is aligned: -dnl [ --with-symfony_debug Include symfony_debug support]) - -dnl Otherwise use enable: - -PHP_ARG_ENABLE(symfony_debug, whether to enable symfony_debug support, -dnl Make sure that the comment is aligned: -[ --enable-symfony_debug Enable symfony_debug support]) - -if test "$PHP_SYMFONY_DEBUG" != "no"; then - dnl Write more examples of tests here... - - dnl # --with-symfony_debug -> check with-path - dnl SEARCH_PATH="/usr/local /usr" # you might want to change this - dnl SEARCH_FOR="/include/symfony_debug.h" # you most likely want to change this - dnl if test -r $PHP_SYMFONY_DEBUG/$SEARCH_FOR; then # path given as parameter - dnl SYMFONY_DEBUG_DIR=$PHP_SYMFONY_DEBUG - dnl else # search default path list - dnl AC_MSG_CHECKING([for symfony_debug files in default path]) - dnl for i in $SEARCH_PATH ; do - dnl if test -r $i/$SEARCH_FOR; then - dnl SYMFONY_DEBUG_DIR=$i - dnl AC_MSG_RESULT(found in $i) - dnl fi - dnl done - dnl fi - dnl - dnl if test -z "$SYMFONY_DEBUG_DIR"; then - dnl AC_MSG_RESULT([not found]) - dnl AC_MSG_ERROR([Please reinstall the symfony_debug distribution]) - dnl fi - - dnl # --with-symfony_debug -> add include path - dnl PHP_ADD_INCLUDE($SYMFONY_DEBUG_DIR/include) - - dnl # --with-symfony_debug -> check for lib and symbol presence - dnl LIBNAME=symfony_debug # you may want to change this - dnl LIBSYMBOL=symfony_debug # you most likely want to change this - - dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL, - dnl [ - dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $SYMFONY_DEBUG_DIR/lib, SYMFONY_DEBUG_SHARED_LIBADD) - dnl AC_DEFINE(HAVE_SYMFONY_DEBUGLIB,1,[ ]) - dnl ],[ - dnl AC_MSG_ERROR([wrong symfony_debug lib version or lib not found]) - dnl ],[ - dnl -L$SYMFONY_DEBUG_DIR/lib -lm - dnl ]) - dnl - dnl PHP_SUBST(SYMFONY_DEBUG_SHARED_LIBADD) - - PHP_NEW_EXTENSION(symfony_debug, symfony_debug.c, $ext_shared) -fi diff --git a/src/Symfony/Component/Debug/Resources/ext/config.w32 b/src/Symfony/Component/Debug/Resources/ext/config.w32 deleted file mode 100644 index 487e6913891cf..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/config.w32 +++ /dev/null @@ -1,13 +0,0 @@ -// $Id$ -// vim:ft=javascript - -// If your extension references something external, use ARG_WITH -// ARG_WITH("symfony_debug", "for symfony_debug support", "no"); - -// Otherwise, use ARG_ENABLE -// ARG_ENABLE("symfony_debug", "enable symfony_debug support", "no"); - -if (PHP_SYMFONY_DEBUG != "no") { - EXTENSION("symfony_debug", "symfony_debug.c"); -} - diff --git a/src/Symfony/Component/Debug/Resources/ext/php_symfony_debug.h b/src/Symfony/Component/Debug/Resources/ext/php_symfony_debug.h deleted file mode 100644 index 26d0e8c012030..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/php_symfony_debug.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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. - */ - -#ifndef PHP_SYMFONY_DEBUG_H -#define PHP_SYMFONY_DEBUG_H - -extern zend_module_entry symfony_debug_module_entry; -#define phpext_symfony_debug_ptr &symfony_debug_module_entry - -#define PHP_SYMFONY_DEBUG_VERSION "2.7" - -#ifdef PHP_WIN32 -# define PHP_SYMFONY_DEBUG_API __declspec(dllexport) -#elif defined(__GNUC__) && __GNUC__ >= 4 -# define PHP_SYMFONY_DEBUG_API __attribute__ ((visibility("default"))) -#else -# define PHP_SYMFONY_DEBUG_API -#endif - -#ifdef ZTS -#include "TSRM.h" -#endif - -ZEND_BEGIN_MODULE_GLOBALS(symfony_debug) - intptr_t req_rand_init; - void (*old_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args); - zval *debug_bt; -ZEND_END_MODULE_GLOBALS(symfony_debug) - -PHP_MINIT_FUNCTION(symfony_debug); -PHP_MSHUTDOWN_FUNCTION(symfony_debug); -PHP_RINIT_FUNCTION(symfony_debug); -PHP_RSHUTDOWN_FUNCTION(symfony_debug); -PHP_MINFO_FUNCTION(symfony_debug); -PHP_GINIT_FUNCTION(symfony_debug); -PHP_GSHUTDOWN_FUNCTION(symfony_debug); - -PHP_FUNCTION(symfony_zval_info); -PHP_FUNCTION(symfony_debug_backtrace); - -static char *_symfony_debug_memory_address_hash(void * TSRMLS_DC); -static const char *_symfony_debug_zval_type(zval *); -static const char* _symfony_debug_get_resource_type(long TSRMLS_DC); -static int _symfony_debug_get_resource_refcount(long TSRMLS_DC); - -void symfony_debug_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args); - -#ifdef ZTS -#define SYMFONY_DEBUG_G(v) TSRMG(symfony_debug_globals_id, zend_symfony_debug_globals *, v) -#else -#define SYMFONY_DEBUG_G(v) (symfony_debug_globals.v) -#endif - -#endif /* PHP_SYMFONY_DEBUG_H */ diff --git a/src/Symfony/Component/Debug/Resources/ext/symfony_debug.c b/src/Symfony/Component/Debug/Resources/ext/symfony_debug.c deleted file mode 100644 index 0d7cb602320f9..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/symfony_debug.c +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#include "php.h" -#ifdef ZTS -#include "TSRM.h" -#endif -#include "php_ini.h" -#include "ext/standard/info.h" -#include "php_symfony_debug.h" -#include "ext/standard/php_rand.h" -#include "ext/standard/php_lcg.h" -#include "ext/spl/php_spl.h" -#include "Zend/zend_gc.h" -#include "Zend/zend_builtin_functions.h" -#include "Zend/zend_extensions.h" /* for ZEND_EXTENSION_API_NO */ -#include "ext/standard/php_array.h" -#include "Zend/zend_interfaces.h" -#include "SAPI.h" - -#define IS_PHP_53 ZEND_EXTENSION_API_NO == 220090626 - -ZEND_DECLARE_MODULE_GLOBALS(symfony_debug) - -ZEND_BEGIN_ARG_INFO_EX(symfony_zval_arginfo, 0, 0, 2) - ZEND_ARG_INFO(0, key) - ZEND_ARG_ARRAY_INFO(0, array, 0) - ZEND_ARG_INFO(0, options) -ZEND_END_ARG_INFO() - -const zend_function_entry symfony_debug_functions[] = { - PHP_FE(symfony_zval_info, symfony_zval_arginfo) - PHP_FE(symfony_debug_backtrace, NULL) - PHP_FE_END -}; - -PHP_FUNCTION(symfony_debug_backtrace) -{ - if (zend_parse_parameters_none() == FAILURE) { - return; - } -#if IS_PHP_53 - zend_fetch_debug_backtrace(return_value, 1, 0 TSRMLS_CC); -#else - zend_fetch_debug_backtrace(return_value, 1, 0, 0 TSRMLS_CC); -#endif - - if (!SYMFONY_DEBUG_G(debug_bt)) { - return; - } - - php_array_merge(Z_ARRVAL_P(return_value), Z_ARRVAL_P(SYMFONY_DEBUG_G(debug_bt)), 0 TSRMLS_CC); -} - -PHP_FUNCTION(symfony_zval_info) -{ - zval *key = NULL, *arg = NULL; - zval **data = NULL; - HashTable *array = NULL; - long options = 0; - - if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zh|l", &key, &array, &options) == FAILURE) { - return; - } - - switch (Z_TYPE_P(key)) { - case IS_STRING: - if (zend_symtable_find(array, Z_STRVAL_P(key), Z_STRLEN_P(key) + 1, (void **)&data) == FAILURE) { - return; - } - break; - case IS_LONG: - if (zend_hash_index_find(array, Z_LVAL_P(key), (void **)&data)) { - return; - } - break; - } - - arg = *data; - - array_init(return_value); - - add_assoc_string(return_value, "type", (char *)_symfony_debug_zval_type(arg), 1); - add_assoc_stringl(return_value, "zval_hash", _symfony_debug_memory_address_hash((void *)arg TSRMLS_CC), 16, 0); - add_assoc_long(return_value, "zval_refcount", Z_REFCOUNT_P(arg)); - add_assoc_bool(return_value, "zval_isref", (zend_bool)Z_ISREF_P(arg)); - - if (Z_TYPE_P(arg) == IS_OBJECT) { - char hash[33] = {0}; - - php_spl_object_hash(arg, (char *)hash TSRMLS_CC); - add_assoc_stringl(return_value, "object_class", (char *)Z_OBJCE_P(arg)->name, Z_OBJCE_P(arg)->name_length, 1); - add_assoc_long(return_value, "object_refcount", EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(arg)].bucket.obj.refcount); - add_assoc_string(return_value, "object_hash", hash, 1); - add_assoc_long(return_value, "object_handle", Z_OBJ_HANDLE_P(arg)); - } else if (Z_TYPE_P(arg) == IS_ARRAY) { - add_assoc_long(return_value, "array_count", zend_hash_num_elements(Z_ARRVAL_P(arg))); - } else if(Z_TYPE_P(arg) == IS_RESOURCE) { - add_assoc_long(return_value, "resource_handle", Z_LVAL_P(arg)); - add_assoc_string(return_value, "resource_type", (char *)_symfony_debug_get_resource_type(Z_LVAL_P(arg) TSRMLS_CC), 1); - add_assoc_long(return_value, "resource_refcount", _symfony_debug_get_resource_refcount(Z_LVAL_P(arg) TSRMLS_CC)); - } else if (Z_TYPE_P(arg) == IS_STRING) { - add_assoc_long(return_value, "strlen", Z_STRLEN_P(arg)); - } -} - -void symfony_debug_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args) -{ - TSRMLS_FETCH(); - zval *retval; - - switch (type) { - case E_ERROR: - case E_PARSE: - case E_CORE_ERROR: - case E_CORE_WARNING: - case E_COMPILE_ERROR: - case E_COMPILE_WARNING: - ALLOC_INIT_ZVAL(retval); -#if IS_PHP_53 - zend_fetch_debug_backtrace(retval, 1, 0 TSRMLS_CC); -#else - zend_fetch_debug_backtrace(retval, 1, 0, 0 TSRMLS_CC); -#endif - SYMFONY_DEBUG_G(debug_bt) = retval; - } - - SYMFONY_DEBUG_G(old_error_cb)(type, error_filename, error_lineno, format, args); -} - -static const char* _symfony_debug_get_resource_type(long rsid TSRMLS_DC) -{ - const char *res_type; - res_type = zend_rsrc_list_get_rsrc_type(rsid TSRMLS_CC); - - if (!res_type) { - return "Unknown"; - } - - return res_type; -} - -static int _symfony_debug_get_resource_refcount(long rsid TSRMLS_DC) -{ - zend_rsrc_list_entry *le; - - if (zend_hash_index_find(&EG(regular_list), rsid, (void **) &le)==SUCCESS) { - return le->refcount; - } - - return 0; -} - -static char *_symfony_debug_memory_address_hash(void *address TSRMLS_DC) -{ - char *result = NULL; - intptr_t address_rand; - - if (!SYMFONY_DEBUG_G(req_rand_init)) { - if (!BG(mt_rand_is_seeded)) { - php_mt_srand(GENERATE_SEED() TSRMLS_CC); - } - SYMFONY_DEBUG_G(req_rand_init) = (intptr_t)php_mt_rand(TSRMLS_C); - } - - address_rand = (intptr_t)address ^ SYMFONY_DEBUG_G(req_rand_init); - - spprintf(&result, 17, "%016zx", address_rand); - - return result; -} - -static const char *_symfony_debug_zval_type(zval *zv) -{ - switch (Z_TYPE_P(zv)) { - case IS_NULL: - return "NULL"; - break; - - case IS_BOOL: - return "boolean"; - break; - - case IS_LONG: - return "integer"; - break; - - case IS_DOUBLE: - return "double"; - break; - - case IS_STRING: - return "string"; - break; - - case IS_ARRAY: - return "array"; - break; - - case IS_OBJECT: - return "object"; - - case IS_RESOURCE: - return "resource"; - - default: - return "unknown type"; - } -} - -zend_module_entry symfony_debug_module_entry = { - STANDARD_MODULE_HEADER, - "symfony_debug", - symfony_debug_functions, - PHP_MINIT(symfony_debug), - PHP_MSHUTDOWN(symfony_debug), - PHP_RINIT(symfony_debug), - PHP_RSHUTDOWN(symfony_debug), - PHP_MINFO(symfony_debug), - PHP_SYMFONY_DEBUG_VERSION, - PHP_MODULE_GLOBALS(symfony_debug), - PHP_GINIT(symfony_debug), - PHP_GSHUTDOWN(symfony_debug), - NULL, - STANDARD_MODULE_PROPERTIES_EX -}; - -#ifdef COMPILE_DL_SYMFONY_DEBUG -ZEND_GET_MODULE(symfony_debug) -#endif - -PHP_GINIT_FUNCTION(symfony_debug) -{ - memset(symfony_debug_globals, 0 , sizeof(*symfony_debug_globals)); -} - -PHP_GSHUTDOWN_FUNCTION(symfony_debug) -{ - -} - -PHP_MINIT_FUNCTION(symfony_debug) -{ - SYMFONY_DEBUG_G(old_error_cb) = zend_error_cb; - zend_error_cb = symfony_debug_error_cb; - - return SUCCESS; -} - -PHP_MSHUTDOWN_FUNCTION(symfony_debug) -{ - zend_error_cb = SYMFONY_DEBUG_G(old_error_cb); - - return SUCCESS; -} - -PHP_RINIT_FUNCTION(symfony_debug) -{ - return SUCCESS; -} - -PHP_RSHUTDOWN_FUNCTION(symfony_debug) -{ - return SUCCESS; -} - -PHP_MINFO_FUNCTION(symfony_debug) -{ - php_info_print_table_start(); - php_info_print_table_header(2, "Symfony Debug support", "enabled"); - php_info_print_table_header(2, "Symfony Debug version", PHP_SYMFONY_DEBUG_VERSION); - php_info_print_table_end(); -} diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/001.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/001.phpt deleted file mode 100644 index 15e183a70615c..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/tests/001.phpt +++ /dev/null @@ -1,153 +0,0 @@ ---TEST-- -Test symfony_zval_info API ---SKIPIF-- - ---FILE-- - $int, - 'float' => $float, - 'str' => $str, - 'object' => $object, - 'array' => $array, - 'resource' => $resource, - 'null' => $null, - 'bool' => $bool, - 'refcount' => &$refcount2, -); - -var_dump(symfony_zval_info('int', $var)); -var_dump(symfony_zval_info('float', $var)); -var_dump(symfony_zval_info('str', $var)); -var_dump(symfony_zval_info('object', $var)); -var_dump(symfony_zval_info('array', $var)); -var_dump(symfony_zval_info('resource', $var)); -var_dump(symfony_zval_info('null', $var)); -var_dump(symfony_zval_info('bool', $var)); - -var_dump(symfony_zval_info('refcount', $var)); -var_dump(symfony_zval_info('not-exist', $var)); -?> ---EXPECTF-- -array(4) { - ["type"]=> - string(7) "integer" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) -} -array(4) { - ["type"]=> - string(6) "double" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) -} -array(5) { - ["type"]=> - string(6) "string" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) - ["strlen"]=> - int(6) -} -array(8) { - ["type"]=> - string(6) "object" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) - ["object_class"]=> - string(8) "stdClass" - ["object_refcount"]=> - int(1) - ["object_hash"]=> - string(32) "%s" - ["object_handle"]=> - int(%d) -} -array(5) { - ["type"]=> - string(5) "array" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) - ["array_count"]=> - int(2) -} -array(7) { - ["type"]=> - string(8) "resource" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) - ["resource_handle"]=> - int(%d) - ["resource_type"]=> - string(6) "stream" - ["resource_refcount"]=> - int(1) -} -array(4) { - ["type"]=> - string(4) "NULL" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) -} -array(4) { - ["type"]=> - string(7) "boolean" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(2) - ["zval_isref"]=> - bool(false) -} -array(4) { - ["type"]=> - string(7) "integer" - ["zval_hash"]=> - string(16) "%s" - ["zval_refcount"]=> - int(3) - ["zval_isref"]=> - bool(true) -} -NULL diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/002.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/002.phpt deleted file mode 100644 index 2bc6d71274d82..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/tests/002.phpt +++ /dev/null @@ -1,63 +0,0 @@ ---TEST-- -Test symfony_debug_backtrace in case of fatal error ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -Fatal error: Call to undefined function notexist() in %s on line %d -Array -( - [0] => Array - ( - [function] => bt - [args] => Array - ( - ) - - ) - - [1] => Array - ( - [file] => %s - [line] => %d - [function] => foo - [args] => Array - ( - ) - - ) - - [2] => Array - ( - [file] => %s - [line] => %d - [function] => bar - [args] => Array - ( - ) - - ) - -) diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/002_1.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/002_1.phpt deleted file mode 100644 index 4e9e34f1b2a40..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/tests/002_1.phpt +++ /dev/null @@ -1,46 +0,0 @@ ---TEST-- -Test symfony_debug_backtrace in case of non fatal error ---SKIPIF-- - ---FILE-- - ---EXPECTF-- -Array -( - [0] => Array - ( - [file] => %s - [line] => %d - [function] => bt - [args] => Array - ( - ) - - ) - - [1] => Array - ( - [file] => %s - [line] => %d - [function] => bar - [args] => Array - ( - ) - - ) - -) diff --git a/src/Symfony/Component/Debug/Resources/ext/tests/003.phpt b/src/Symfony/Component/Debug/Resources/ext/tests/003.phpt deleted file mode 100644 index 2a494e27af2f1..0000000000000 --- a/src/Symfony/Component/Debug/Resources/ext/tests/003.phpt +++ /dev/null @@ -1,85 +0,0 @@ ---TEST-- -Test ErrorHandler in case of fatal error ---SKIPIF-- - ---FILE-- -setExceptionHandler('print_r'); - -if (function_exists('xdebug_disable')) { - xdebug_disable(); -} - -bar(); -?> ---EXPECTF-- -Fatal error: Call to undefined function Symfony\Component\Debug\notexist() in %s on line %d -Symfony\Component\Debug\Exception\UndefinedFunctionException Object -( - [message:protected] => Attempted to call function "notexist" from namespace "Symfony\Component\Debug". - [string:Exception:private] => - [code:protected] => 0 - [file:protected] => %s - [line:protected] => %d - [trace:Exception:private] => Array - ( - [0] => Array - ( -%A [function] => Symfony\Component\Debug\foo -%A [args] => Array - ( - ) - - ) - - [1] => Array - ( -%A [function] => Symfony\Component\Debug\bar -%A [args] => Array - ( - ) - - ) -%A - ) - - [previous:Exception:private] => - [severity:protected] => 1 -) diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 196b92c884598..1f06e5a091387 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Debug\DebugClassLoader; -use Symfony\Component\Debug\ErrorHandler; class DebugClassLoaderTest extends TestCase { @@ -26,7 +25,7 @@ class DebugClassLoaderTest extends TestCase protected function setUp() { - $this->errorReporting = error_reporting(E_ALL | E_STRICT); + $this->errorReporting = error_reporting(E_ALL); $this->loader = new ClassLoader(); spl_autoload_register(array($this->loader, 'loadClass'), true, true); DebugClassLoader::enable(); @@ -59,73 +58,6 @@ public function testIdempotence() $this->fail('DebugClassLoader did not register'); } - public function testUnsilencing() - { - if (\PHP_VERSION_ID >= 70000) { - $this->markTestSkipped('PHP7 throws exceptions, unsilencing is not required anymore.'); - } - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('HHVM is not handled in this test case.'); - } - - ob_start(); - - $this->iniSet('log_errors', 0); - $this->iniSet('display_errors', 1); - - // See below: this will fail with parse error - // but this should not be @-silenced. - @class_exists(__NAMESPACE__.'\TestingUnsilencing', true); - - $output = ob_get_clean(); - - $this->assertStringMatchesFormat('%aParse error%a', $output); - } - - public function testStacking() - { - // the ContextErrorException must not be loaded to test the workaround - // for https://bugs.php.net/65322. - if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) { - $this->markTestSkipped('The ContextErrorException class is already loaded.'); - } - if (defined('HHVM_VERSION')) { - $this->markTestSkipped('HHVM is not handled in this test case.'); - } - - ErrorHandler::register(); - - try { - // Trigger autoloading + E_STRICT at compile time - // which in turn triggers $errorHandler->handle() - // that again triggers autoloading for ContextErrorException. - // Error stacking works around the bug above and everything is fine. - - eval(' - namespace '.__NAMESPACE__.'; - class ChildTestingStacking extends TestingStacking { function foo($bar) {} } - '); - $this->fail('ContextErrorException expected'); - } catch (\ErrorException $exception) { - // if an exception is thrown, the test passed - restore_error_handler(); - restore_exception_handler(); - $this->assertStringStartsWith(__FILE__, $exception->getFile()); - if (\PHP_VERSION_ID < 70000) { - $this->assertRegExp('/^Runtime Notice: Declaration/', $exception->getMessage()); - $this->assertEquals(E_STRICT, $exception->getSeverity()); - } else { - $this->assertRegExp('/^Warning: Declaration/', $exception->getMessage()); - $this->assertEquals(E_WARNING, $exception->getSeverity()); - } - } catch (\Exception $exception) { - restore_error_handler(); - restore_exception_handler(); - - throw $exception; - } - } - /** * @expectedException \RuntimeException */ @@ -189,7 +121,7 @@ class_exists('Test\\'.__NAMESPACE__.'\\'.$class, true); $xError = array( 'type' => E_USER_DEPRECATED, - 'message' => 'The Test\Symfony\Component\Debug\Tests\\'.$class.' class '.$type.' Symfony\Component\Debug\Tests\Fixtures\\'.$super.' that is deprecated but this is a test deprecation notice.', + 'message' => 'The "Test\Symfony\Component\Debug\Tests\\'.$class.'" class '.$type.' "Symfony\Component\Debug\Tests\Fixtures\\'.$super.'" that is deprecated but this is a test deprecation notice.', ); $this->assertSame($xError, $lastError); @@ -247,17 +179,57 @@ class_exists('Symfony\Bridge\Debug\Tests\Fixtures\ExtendsDeprecatedParent', true $this->assertSame($xError, $lastError); } - public function testReservedForPhp7() + public function testExtendedFinalClass() { - if (\PHP_VERSION_ID >= 70000) { - $this->markTestSkipped('PHP7 already prevents using reserved names.'); - } + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsFinalClass', true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + $xError = array( + 'type' => E_USER_DEPRECATED, + 'message' => 'The "Symfony\Component\Debug\Tests\Fixtures\FinalClass" class is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsFinalClass".', + ); + + $this->assertSame($xError, $lastError); + } + + public function testExtendedFinalMethod() + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists(__NAMESPACE__.'\\Fixtures\\ExtendedFinalMethod', true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $xError = array( + 'type' => E_USER_DEPRECATED, + 'message' => 'The "Symfony\Component\Debug\Tests\Fixtures\FinalMethod::finalMethod()" method is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod".', + ); + + $this->assertSame($xError, $lastError); + } + + public function testExtendedDeprecatedMethod() + { set_error_handler(function () { return false; }); $e = error_reporting(0); trigger_error('', E_USER_NOTICE); - class_exists('Test\\'.__NAMESPACE__.'\\Float', true); + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsAnnotatedClass', true); error_reporting($e); restore_error_handler(); @@ -267,11 +239,30 @@ class_exists('Test\\'.__NAMESPACE__.'\\Float', true); $xError = array( 'type' => E_USER_DEPRECATED, - 'message' => 'Test\Symfony\Component\Debug\Tests\Float uses a reserved class name (Float) that will break on PHP 7 and higher', + 'message' => 'The "Symfony\Component\Debug\Tests\Fixtures\AnnotatedClass::deprecatedMethod()" method is deprecated since version 3.4. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsAnnotatedClass".', ); $this->assertSame($xError, $lastError); } + + public function testInternalsUse() + { + $deprecations = array(); + set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; }); + $e = error_reporting(E_USER_DEPRECATED); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsInternals', true); + + error_reporting($e); + restore_error_handler(); + + $this->assertSame($deprecations, array( + 'The "Symfony\Component\Debug\Tests\Fixtures\InternalClass" class is considered internal since version 3.4. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternalsParent".', + 'The "Symfony\Component\Debug\Tests\Fixtures\InternalInterface" interface is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternalsParent".', + 'The "Symfony\Component\Debug\Tests\Fixtures\InternalTrait" trait is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".', + 'The "Symfony\Component\Debug\Tests\Fixtures\InternalTrait2::internalMethod()" method is considered internal since version 3.4. It may change without further notice. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".', + )); + } } class ClassLoader @@ -295,16 +286,12 @@ public function findFile($class) eval('namespace '.__NAMESPACE__.'; class TestingStacking { function foo() {} }'); } elseif (__NAMESPACE__.'\TestingCaseMismatch' === $class) { eval('namespace '.__NAMESPACE__.'; class TestingCaseMisMatch {}'); - } elseif (__NAMESPACE__.'\Fixtures\CaseMismatch' === $class) { - return $fixtureDir.'CaseMismatch.php'; } elseif (__NAMESPACE__.'\Fixtures\Psr4CaseMismatch' === $class) { return $fixtureDir.'psr4'.DIRECTORY_SEPARATOR.'Psr4CaseMismatch.php'; } elseif (__NAMESPACE__.'\Fixtures\NotPSR0' === $class) { return $fixtureDir.'reallyNotPsr0.php'; } elseif (__NAMESPACE__.'\Fixtures\NotPSR0bis' === $class) { return $fixtureDir.'notPsr0Bis.php'; - } elseif (__NAMESPACE__.'\Fixtures\DeprecatedInterface' === $class) { - return $fixtureDir.'DeprecatedInterface.php'; } elseif ('Symfony\Bridge\Debug\Tests\Fixtures\ExtendsDeprecatedParent' === $class) { eval('namespace Symfony\Bridge\Debug\Tests\Fixtures; class ExtendsDeprecatedParent extends \\'.__NAMESPACE__.'\Fixtures\DeprecatedClass {}'); } elseif ('Test\\'.__NAMESPACE__.'\DeprecatedParentClass' === $class) { @@ -315,6 +302,20 @@ public function findFile($class) eval('namespace Test\\'.__NAMESPACE__.'; class NonDeprecatedInterfaceClass implements \\'.__NAMESPACE__.'\Fixtures\NonDeprecatedInterface {}'); } elseif ('Test\\'.__NAMESPACE__.'\Float' === $class) { eval('namespace Test\\'.__NAMESPACE__.'; class Float {}'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsFinalClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsFinalClass extends \\'.__NAMESPACE__.'\Fixtures\FinalClass {}'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsAnnotatedClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsAnnotatedClass extends \\'.__NAMESPACE__.'\Fixtures\AnnotatedClass { + public function deprecatedMethod() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternals' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternals extends ExtendsInternalsParent { + use \\'.__NAMESPACE__.'\Fixtures\InternalTrait; + + public function internalMethod() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternalsParent' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternalsParent extends \\'.__NAMESPACE__.'\Fixtures\InternalClass implements \\'.__NAMESPACE__.'\Fixtures\InternalInterface { }'); } } } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 001a64d9bd14c..4b7bcc3cddb2f 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -13,9 +13,9 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LogLevel; -use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\Debug\BufferingLogger; -use Symfony\Component\Debug\Exception\ContextErrorException; +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\Debug\Exception\SilencedErrorContext; /** * ErrorHandlerTest. @@ -71,45 +71,33 @@ public function testNotice() try { self::triggerNotice($this); - $this->fail('ContextErrorException expected'); - } catch (ContextErrorException $exception) { + $this->fail('ErrorException expected'); + } catch (\ErrorException $exception) { // if an exception is thrown, the test passed - restore_error_handler(); - restore_exception_handler(); - $this->assertEquals(E_NOTICE, $exception->getSeverity()); $this->assertEquals(__FILE__, $exception->getFile()); $this->assertRegExp('/^Notice: Undefined variable: (foo|bar)/', $exception->getMessage()); - $this->assertArrayHasKey('foobar', $exception->getContext()); $trace = $exception->getTrace(); + $this->assertEquals(__FILE__, $trace[0]['file']); - $this->assertEquals('Symfony\Component\Debug\ErrorHandler', $trace[0]['class']); - $this->assertEquals('handleError', $trace[0]['function']); - $this->assertEquals('->', $trace[0]['type']); + $this->assertEquals(__CLASS__, $trace[0]['class']); + $this->assertEquals('triggerNotice', $trace[0]['function']); + $this->assertEquals('::', $trace[0]['type']); - $this->assertEquals(__FILE__, $trace[1]['file']); + $this->assertEquals(__FILE__, $trace[0]['file']); $this->assertEquals(__CLASS__, $trace[1]['class']); - $this->assertEquals('triggerNotice', $trace[1]['function']); - $this->assertEquals('::', $trace[1]['type']); - - $this->assertEquals(__FILE__, $trace[1]['file']); - $this->assertEquals(__CLASS__, $trace[2]['class']); - $this->assertEquals(__FUNCTION__, $trace[2]['function']); - $this->assertEquals('->', $trace[2]['type']); - } catch (\Exception $e) { + $this->assertEquals(__FUNCTION__, $trace[1]['function']); + $this->assertEquals('->', $trace[1]['type']); + } finally { restore_error_handler(); restore_exception_handler(); - - throw $e; } } // dummy function to test trace in error handler. private static function triggerNotice($that) { - // dummy variable to check for in error handler. - $foobar = 123; $that->assertSame('', $foo.$foo.$bar); } @@ -119,14 +107,9 @@ public function testConstruct() $handler = ErrorHandler::register(); $handler->throwAt(3, true); $this->assertEquals(3 | E_RECOVERABLE_ERROR | E_USER_ERROR, $handler->throwAt(0)); - - restore_error_handler(); - restore_exception_handler(); - } catch (\Exception $e) { + } finally { restore_error_handler(); restore_exception_handler(); - - throw $e; } } @@ -158,14 +141,9 @@ public function testDefaultLogger() E_CORE_ERROR => array(null, LogLevel::CRITICAL), ); $this->assertSame($loggers, $handler->setLoggers(array())); - - restore_error_handler(); - restore_exception_handler(); - } catch (\Exception $e) { + } finally { restore_error_handler(); restore_exception_handler(); - - throw $e; } } @@ -216,14 +194,14 @@ public function testHandleError() $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - $that = $this; - $warnArgCheck = function ($logLevel, $message, $context) use ($that) { - $that->assertEquals('info', $logLevel); - $that->assertEquals('foo', $message); - $that->assertArrayHasKey('type', $context); - $that->assertEquals($context['type'], E_USER_DEPRECATED); - $that->assertArrayHasKey('stack', $context); - $that->assertInternalType('array', $context['stack']); + $warnArgCheck = function ($logLevel, $message, $context) { + $this->assertEquals('info', $logLevel); + $this->assertEquals('User Deprecated: foo', $message); + $this->assertArrayHasKey('exception', $context); + $exception = $context['exception']; + $this->assertInstanceOf(\ErrorException::class, $exception); + $this->assertSame('User Deprecated: foo', $exception->getMessage()); + $this->assertSame(E_USER_DEPRECATED, $exception->getSeverity()); }; $logger @@ -241,11 +219,17 @@ public function testHandleError() $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - $that = $this; - $logArgCheck = function ($level, $message, $context) use ($that) { - $that->assertEquals('Undefined variable: undefVar', $message); - $that->assertArrayHasKey('type', $context); - $that->assertEquals($context['type'], E_NOTICE); + $line = null; + $logArgCheck = function ($level, $message, $context) use (&$line) { + $this->assertEquals('Notice: Undefined variable: undefVar', $message); + $this->assertArrayHasKey('exception', $context); + $exception = $context['exception']; + $this->assertInstanceOf(SilencedErrorContext::class, $exception); + $this->assertSame(E_NOTICE, $exception->getSeverity()); + $this->assertSame(__FILE__, $exception->getFile()); + $this->assertSame($line, $exception->getLine()); + $this->assertNotEmpty($exception->getTrace()); + $this->assertSame(1, $exception->count); }; $logger @@ -258,6 +242,7 @@ public function testHandleError() $handler->setDefaultLogger($logger, E_NOTICE); $handler->screamAt(E_NOTICE); unset($undefVar); + $line = __LINE__ + 1; @$undefVar++; restore_error_handler(); @@ -286,25 +271,20 @@ public function testHandleUserError() } $this->assertSame($x, $e); - - restore_error_handler(); - restore_exception_handler(); - } catch (\Exception $e) { + } finally { restore_error_handler(); restore_exception_handler(); - - throw $e; } } public function testHandleDeprecation() { - $that = $this; - $logArgCheck = function ($level, $message, $context) use ($that) { - $that->assertEquals(LogLevel::INFO, $level); - $that->assertArrayHasKey('level', $context); - $that->assertEquals(E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED, $context['level']); - $that->assertArrayHasKey('stack', $context); + $logArgCheck = function ($level, $message, $context) { + $this->assertEquals(LogLevel::INFO, $level); + $this->assertArrayHasKey('exception', $context); + $exception = $context['exception']; + $this->assertInstanceOf(\ErrorException::class, $exception); + $this->assertSame('User Deprecated: Foo deprecation', $exception->getMessage()); }; $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); @@ -328,11 +308,10 @@ public function testHandleException() $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - $that = $this; - $logArgCheck = function ($level, $message, $context) use ($that) { - $that->assertEquals('Uncaught Exception: foo', $message); - $that->assertArrayHasKey('type', $context); - $that->assertEquals($context['type'], E_ERROR); + $logArgCheck = function ($level, $message, $context) { + $this->assertSame('Uncaught Exception: foo', $message); + $this->assertArrayHasKey('exception', $context); + $this->assertInstanceOf(\Exception::class, $context['exception']); }; $logger @@ -350,54 +329,14 @@ public function testHandleException() $this->assertSame($exception, $e); } - $that = $this; - $handler->setExceptionHandler(function ($e) use ($exception, $that) { - $that->assertSame($exception, $e); + $handler->setExceptionHandler(function ($e) use ($exception) { + $this->assertSame($exception, $e); }); $handler->handleException($exception); - - restore_error_handler(); - restore_exception_handler(); - } catch (\Exception $e) { + } finally { restore_error_handler(); restore_exception_handler(); - - throw $e; - } - } - - public function testErrorStacking() - { - try { - $handler = ErrorHandler::register(); - $handler->screamAt(E_USER_WARNING); - - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - - $logger - ->expects($this->exactly(2)) - ->method('log') - ->withConsecutive( - array($this->equalTo(LogLevel::WARNING), $this->equalTo('Dummy log')), - array($this->equalTo(LogLevel::DEBUG), $this->equalTo('Silenced warning')) - ) - ; - - $handler->setDefaultLogger($logger, array(E_USER_WARNING => LogLevel::WARNING)); - - ErrorHandler::stackErrors(); - @trigger_error('Silenced warning', E_USER_WARNING); - $logger->log(LogLevel::WARNING, 'Dummy log'); - ErrorHandler::unstackErrors(); - - restore_error_handler(); - restore_exception_handler(); - } catch (\Exception $e) { - restore_error_handler(); - restore_exception_handler(); - - throw $e; } } @@ -427,23 +366,50 @@ public function testBootstrappingLogger() $this->assertSame($loggers, $handler->setLoggers(array())); $handler->handleError(E_DEPRECATED, 'Foo message', __FILE__, 123, array()); - $expectedLog = array(LogLevel::INFO, 'Foo message', array('type' => E_DEPRECATED, 'file' => __FILE__, 'line' => 123, 'level' => error_reporting())); $logs = $bootLogger->cleanLogs(); - unset($logs[0][2]['stack']); - $this->assertSame(array($expectedLog), $logs); + $this->assertCount(1, $logs); + $log = $logs[0]; + $this->assertSame('info', $log[0]); + $this->assertSame('Deprecated: Foo message', $log[1]); + $this->assertArrayHasKey('exception', $log[2]); + $exception = $log[2]['exception']; + $this->assertInstanceOf(\ErrorException::class, $exception); + $this->assertSame('Deprecated: Foo message', $exception->getMessage()); + $this->assertSame(__FILE__, $exception->getFile()); + $this->assertSame(123, $exception->getLine()); + $this->assertSame(E_DEPRECATED, $exception->getSeverity()); - $bootLogger->log($expectedLog[0], $expectedLog[1], $expectedLog[2]); + $bootLogger->log(LogLevel::WARNING, 'Foo message', array('exception' => $exception)); $mockLogger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); $mockLogger->expects($this->once()) ->method('log') - ->with(LogLevel::WARNING, 'Foo message', $expectedLog[2]); + ->with(LogLevel::WARNING, 'Foo message', array('exception' => $exception)); $handler->setLoggers(array(E_DEPRECATED => array($mockLogger, LogLevel::WARNING))); } + public function testSettingLoggerWhenExceptionIsBuffered() + { + $bootLogger = new BufferingLogger(); + $handler = new ErrorHandler($bootLogger); + + $exception = new \Exception('Foo message'); + + $mockLogger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $mockLogger->expects($this->once()) + ->method('log') + ->with(LogLevel::CRITICAL, 'Uncaught Exception: Foo message', array('exception' => $exception)); + + $handler->setExceptionHandler(function () use ($handler, $mockLogger) { + $handler->setDefaultLogger($mockLogger); + }); + + $handler->handleException($exception); + } + public function testHandleFatalError() { try { @@ -458,11 +424,10 @@ public function testHandleFatalError() $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - $that = $this; - $logArgCheck = function ($level, $message, $context) use ($that) { - $that->assertEquals('Fatal Parse Error: foo', $message); - $that->assertArrayHasKey('type', $context); - $that->assertEquals($context['type'], E_PARSE); + $logArgCheck = function ($level, $message, $context) { + $this->assertEquals('Fatal Parse Error: foo', $message); + $this->assertArrayHasKey('exception', $context); + $this->assertInstanceOf(\Exception::class, $context['exception']); }; $logger @@ -485,9 +450,6 @@ public function testHandleFatalError() } } - /** - * @requires PHP 7 - */ public function testHandleErrorException() { $exception = new \Error("Class 'Foo' not found"); @@ -502,92 +464,4 @@ public function testHandleErrorException() $this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $args[0]); $this->assertStringStartsWith("Attempted to load class \"Foo\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage()); } - - public function testHandleFatalErrorOnHHVM() - { - try { - $handler = ErrorHandler::register(); - - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - $logger - ->expects($this->once()) - ->method('log') - ->with( - $this->equalTo(LogLevel::CRITICAL), - $this->equalTo('Fatal Error: foo'), - $this->equalTo(array( - 'type' => 1, - 'file' => 'bar', - 'line' => 123, - 'level' => -1, - 'stack' => array(456), - )) - ) - ; - - $handler->setDefaultLogger($logger, E_ERROR); - - $error = array( - 'type' => E_ERROR + 0x1000000, // This error level is used by HHVM for fatal errors - 'message' => 'foo', - 'file' => 'bar', - 'line' => 123, - 'context' => array(123), - 'backtrace' => array(456), - ); - - call_user_func_array(array($handler, 'handleError'), $error); - $handler->handleFatalError($error); - - restore_error_handler(); - restore_exception_handler(); - } catch (\Exception $e) { - restore_error_handler(); - restore_exception_handler(); - - throw $e; - } - } - - /** - * @group legacy - */ - public function testLegacyInterface() - { - try { - $handler = ErrorHandler::register(0); - $this->assertFalse($handler->handle(0, 'foo', 'foo.php', 12, array())); - - restore_error_handler(); - restore_exception_handler(); - - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); - - $that = $this; - $logArgCheck = function ($level, $message, $context) use ($that) { - $that->assertEquals('Undefined variable: undefVar', $message); - $that->assertArrayHasKey('type', $context); - $that->assertEquals($context['type'], E_NOTICE); - }; - - $logger - ->expects($this->once()) - ->method('log') - ->will($this->returnCallback($logArgCheck)) - ; - - $handler = ErrorHandler::register(E_NOTICE); - @$handler->setLogger($logger, 'scream'); - unset($undefVar); - @$undefVar++; - - restore_error_handler(); - restore_exception_handler(); - } catch (\Exception $e) { - restore_error_handler(); - restore_exception_handler(); - - throw $e; - } - } } diff --git a/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php index ae01e9cb0d839..d68f9a035e8f0 100644 --- a/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; @@ -79,6 +80,11 @@ public function testStatusCode() $flattened = FlattenException::create(new UnsupportedMediaTypeHttpException()); $this->assertEquals('415', $flattened->getStatusCode()); + + if (class_exists(SuspiciousOperationException::class)) { + $flattened = FlattenException::create(new SuspiciousOperationException()); + $this->assertEquals('400', $flattened->getStatusCode()); + } } public function testHeadersForHttpException() @@ -132,9 +138,6 @@ public function testPrevious(\Exception $exception, $statusCode) $this->assertSame(array($flattened2), $flattened->getAllPrevious()); } - /** - * @requires PHP 7.0 - */ public function testPreviousError() { $exception = new \Exception('test', 123, new \ParseError('Oh noes!', 42)); @@ -191,6 +194,68 @@ public function flattenDataProvider() ); } + public function testArguments() + { + $dh = opendir(__DIR__); + $fh = tmpfile(); + + $incomplete = unserialize('O:14:"BogusTestClass":0:{}'); + + $exception = $this->createException(array( + (object) array('foo' => 1), + new NotFoundHttpException(), + $incomplete, + $dh, + $fh, + function () {}, + array(1, 2), + array('foo' => 123), + null, + true, + false, + 0, + 0.0, + '0', + '', + INF, + NAN, + )); + + $flattened = FlattenException::create($exception); + $trace = $flattened->getTrace(); + $args = $trace[1]['args']; + $array = $args[0][1]; + + closedir($dh); + fclose($fh); + + $i = 0; + $this->assertSame(array('object', 'stdClass'), $array[$i++]); + $this->assertSame(array('object', 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'), $array[$i++]); + $this->assertSame(array('incomplete-object', 'BogusTestClass'), $array[$i++]); + $this->assertSame(array('resource', 'stream'), $array[$i++]); + $this->assertSame(array('resource', 'stream'), $array[$i++]); + + $args = $array[$i++]; + $this->assertSame($args[0], 'object'); + $this->assertTrue('Closure' === $args[1] || is_subclass_of($args[1], '\Closure'), 'Expect object class name to be Closure or a subclass of Closure.'); + + $this->assertSame(array('array', array(array('integer', 1), array('integer', 2))), $array[$i++]); + $this->assertSame(array('array', array('foo' => array('integer', 123))), $array[$i++]); + $this->assertSame(array('null', null), $array[$i++]); + $this->assertSame(array('boolean', true), $array[$i++]); + $this->assertSame(array('boolean', false), $array[$i++]); + $this->assertSame(array('integer', 0), $array[$i++]); + $this->assertSame(array('float', 0.0), $array[$i++]); + $this->assertSame(array('string', '0'), $array[$i++]); + $this->assertSame(array('string', ''), $array[$i++]); + $this->assertSame(array('float', INF), $array[$i++]); + + // assertEquals() does not like NAN values. + $this->assertEquals($array[$i][0], 'float'); + $this->assertTrue(is_nan($array[$i++][1])); + } + public function testRecursionInArguments() { $a = array('foo', array(2, &$a)); @@ -217,6 +282,9 @@ public function testTooBigArray() $flattened = FlattenException::create($exception); $trace = $flattened->getTrace(); + + $this->assertSame($trace[1]['args'][0], array('array', array('array', '*SKIPPED over 10000 entries*'))); + $serializeTrace = serialize($trace); $this->assertContains('*SKIPPED over 10000 entries*', $serializeTrace); @@ -227,45 +295,4 @@ private function createException($foo) { return new \Exception(); } - - public function testSetTraceIncompleteClass() - { - $flattened = FlattenException::create(new \Exception('test', 123)); - $flattened->setTrace( - array( - array( - 'file' => __FILE__, - 'line' => 123, - 'function' => 'test', - 'args' => array( - unserialize('O:14:"BogusTestClass":0:{}'), - ), - ), - ), - 'foo.php', 123 - ); - - $this->assertEquals(array( - array( - 'message' => 'test', - 'class' => 'Exception', - 'trace' => array( - array( - 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', - 'file' => 'foo.php', 'line' => 123, - 'args' => array(), - ), - array( - 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => 'test', - 'file' => __FILE__, 'line' => 123, - 'args' => array( - array( - 'incomplete-object', 'BogusTestClass', - ), - ), - ), - ), - ), - ), $flattened->toArray()); - } } diff --git a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php index 77cc0b5cbdd46..0285eff1346cb 100644 --- a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php @@ -39,8 +39,8 @@ public function testDebug() $handler->sendPhpResponse(new \RuntimeException('Foo')); $response = ob_get_clean(); - $this->assertContains('

      Whoops, looks like something went wrong.

      ', $response); - $this->assertNotContains('

      ', $response); + $this->assertContains('Whoops, looks like something went wrong.', $response); + $this->assertNotContains('
      ', $response); $handler = new ExceptionHandler(true); @@ -48,8 +48,8 @@ public function testDebug() $handler->sendPhpResponse(new \RuntimeException('Foo')); $response = ob_get_clean(); - $this->assertContains('

      Whoops, looks like something went wrong.

      ', $response); - $this->assertContains('

      ', $response); + $this->assertContains('Whoops, looks like something went wrong.', $response); + $this->assertContains('
      ', $response); } public function testStatusCode() @@ -94,7 +94,7 @@ public function testNestedExceptions() $handler->sendPhpResponse(new \RuntimeException('Foo', 0, new \RuntimeException('Bar'))); $response = ob_get_clean(); - $this->assertStringMatchesFormat('%AFoo%ABar%A', $response); + $this->assertStringMatchesFormat('%A

      Foo

      %A

      Bar

      %A', $response); } public function testHandle() @@ -108,9 +108,8 @@ public function testHandle() $handler->handle($exception); - $that = $this; - $handler->setHandler(function ($e) use ($exception, $that) { - $that->assertSame($exception, $e); + $handler->setHandler(function ($e) use ($exception) { + $this->assertSame($exception, $e); }); $handler->handle($exception); @@ -125,9 +124,8 @@ public function testHandleOutOfMemoryException() ->expects($this->once()) ->method('sendPhpResponse'); - $that = $this; - $handler->setHandler(function ($e) use ($that) { - $that->fail('OutOfMemoryException should bypass the handler'); + $handler->setHandler(function ($e) { + $this->fail('OutOfMemoryException should bypass the handler'); }); $handler->handle($exception); diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php index 0611ed91e31d9..65c80fc1cf34a 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Debug\Tests\FatalErrorHandler; use PHPUnit\Framework\TestCase; -use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; -use Symfony\Component\ClassLoader\UniversalClassLoader as SymfonyUniversalClassLoader; use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; use Symfony\Component\Debug\DebugClassLoader; @@ -69,35 +67,12 @@ public function testHandleClassNotFound($error, $translatedMessage, $autoloader $this->assertSame($error['line'], $exception->getLine()); } - /** - * @group legacy - */ - public function testLegacyHandleClassNotFound() - { - $prefixes = array('Symfony\Component\Debug\Exception\\' => realpath(__DIR__.'/../../Exception')); - $symfonyUniversalClassLoader = new SymfonyUniversalClassLoader(); - $symfonyUniversalClassLoader->registerPrefixes($prefixes); - - $this->testHandleClassNotFound( - array( - 'type' => 1, - 'line' => 12, - 'file' => 'foo.php', - 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', - ), - "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", - array($symfonyUniversalClassLoader, 'loadClass') - ); - } - public function provideClassNotFoundData() { - $prefixes = array('Symfony\Component\Debug\Exception\\' => realpath(__DIR__.'/../../Exception')); - - $symfonyAutoloader = new SymfonyClassLoader(); - $symfonyAutoloader->addPrefixes($prefixes); + $autoloader = new ComposerClassLoader(); + $autoloader->add('Symfony\Component\Debug\Exception\\', realpath(__DIR__.'/../../Exception')); - $debugClassLoader = new DebugClassLoader(array($symfonyAutoloader, 'loadClass')); + $debugClassLoader = new DebugClassLoader(array($autoloader, 'loadClass')); return array( array( @@ -153,7 +128,7 @@ public function provideClassNotFoundData() 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', ), "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", - array($symfonyAutoloader, 'loadClass'), + array($autoloader, 'loadClass'), ), array( array( diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php index 1dc2120045c2c..60153fc5ec2f2 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php @@ -26,7 +26,7 @@ public function testUndefinedFunction($error, $translatedMessage) $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); $this->assertInstanceOf('Symfony\Component\Debug\Exception\UndefinedFunctionException', $exception); - // class names are case insensitive and PHP/HHVM do not return the same + // class names are case insensitive and PHP do not return the same $this->assertSame(strtolower($translatedMessage), strtolower($exception->getMessage())); $this->assertSame($error['type'], $exception->getSeverity()); $this->assertSame($error['file'], $exception->getFile()); diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/AnnotatedClass.php b/src/Symfony/Component/Debug/Tests/Fixtures/AnnotatedClass.php new file mode 100644 index 0000000000000..dff9517d0a046 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/AnnotatedClass.php @@ -0,0 +1,13 @@ +=5.3.9", + "php": "^7.1.3", "psr/log": "~1.0" }, "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + "symfony/http-kernel": "<3.4" }, "require-dev": { - "symfony/class-loader": "~2.2|~3.0.0", - "symfony/http-kernel": "~2.3.24|~2.5.9|^2.6.2|~3.0.0" + "symfony/http-kernel": "~3.4|~4.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Debug\\": "" }, @@ -35,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Symfony/Component/DependencyInjection/Alias.php b/src/Symfony/Component/DependencyInjection/Alias.php index eaf7f00ccd446..a113f8f7f2c38 100644 --- a/src/Symfony/Component/DependencyInjection/Alias.php +++ b/src/Symfony/Component/DependencyInjection/Alias.php @@ -22,7 +22,7 @@ class Alias */ public function __construct($id, $public = true) { - $this->id = strtolower($id); + $this->id = (string) $id; $this->public = $public; } diff --git a/src/Symfony/Component/DependencyInjection/Argument/ArgumentInterface.php b/src/Symfony/Component/DependencyInjection/Argument/ArgumentInterface.php new file mode 100644 index 0000000000000..b46eb77be749e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Argument/ArgumentInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Argument; + +/** + * Represents a complex argument containing nested values. + * + * @author Titouan Galopin + */ +interface ArgumentInterface +{ + /** + * @return array + */ + public function getValues(); + + public function setValues(array $values); +} diff --git a/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php b/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php new file mode 100644 index 0000000000000..f72f2110744c1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Argument; + +/** + * @author Guilhem Niot + */ +final class BoundArgument implements ArgumentInterface +{ + private static $sequence = 0; + + private $value; + private $identifier; + private $used; + + public function __construct($value) + { + $this->value = $value; + $this->identifier = ++self::$sequence; + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + return array($this->value, $this->identifier, $this->used); + } + + /** + * {@inheritdoc} + */ + public function setValues(array $values) + { + list($this->value, $this->identifier, $this->used) = $values; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php new file mode 100644 index 0000000000000..ab3a87900ad80 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Argument; + +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Represents a collection of values to lazily iterate over. + * + * @author Titouan Galopin + */ +class IteratorArgument implements ArgumentInterface +{ + private $values; + + /** + * @param Reference[] $values + */ + public function __construct(array $values) + { + $this->setValues($values); + } + + /** + * @return array The values to lazily iterate over + */ + public function getValues() + { + return $this->values; + } + + /** + * @param Reference[] $values The service references to lazily iterate over + */ + public function setValues(array $values) + { + foreach ($values as $k => $v) { + if (null !== $v && !$v instanceof Reference) { + throw new InvalidArgumentException(sprintf('An IteratorArgument must hold only Reference instances, "%s" given.', is_object($v) ? get_class($v) : gettype($v))); + } + } + + $this->values = $values; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Argument/RewindableGenerator.php b/src/Symfony/Component/DependencyInjection/Argument/RewindableGenerator.php new file mode 100644 index 0000000000000..e162a7c34deca --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Argument/RewindableGenerator.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Argument; + +/** + * @internal + */ +class RewindableGenerator implements \IteratorAggregate, \Countable +{ + private $generator; + private $count; + + /** + * @param callable $generator + * @param int|callable $count + */ + public function __construct(callable $generator, $count) + { + $this->generator = $generator; + $this->count = $count; + } + + public function getIterator() + { + $g = $this->generator; + + return $g(); + } + + public function count() + { + if (is_callable($count = $this->count)) { + $this->count = $count(); + } + + return $this->count; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php b/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php new file mode 100644 index 0000000000000..2fec5d26d0fc5 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Argument; + +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Represents a service wrapped in a memoizing closure. + * + * @author Nicolas Grekas + */ +class ServiceClosureArgument implements ArgumentInterface +{ + private $values; + + public function __construct(Reference $reference) + { + $this->values = array($reference); + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + return $this->values; + } + + /** + * {@inheritdoc} + */ + public function setValues(array $values) + { + if (array(0) !== array_keys($values) || !($values[0] instanceof Reference || null === $values[0])) { + throw new InvalidArgumentException('A ServiceClosureArgument must hold one and only one Reference.'); + } + + $this->values = $values; + } +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 27cb2d58a4af1..3d2c56a5c82c5 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -1,6 +1,77 @@ CHANGELOG ========= +4.0.0 +----- + + * removed autowiring services based on the types they implement + * added a third `$methodName` argument to the `getProxyFactoryCode()` method + of the `DumperInterface` + * removed support for autowiring types + * removed `Container::isFrozen` + * removed support for dumping an ucompiled container in `PhpDumper` + * removed support for generating a dumped `Container` without populating the method map + * removed support for case insensitive service identifiers + * removed the `DefinitionDecorator` class, replaced by `ChildDefinition` + * removed the `AutowireServiceResource` class and related `AutowirePass::createResourceForClass()` method + * removed `LoggingFormatter`, `Compiler::getLoggingFormatter()` and `addLogMessage()` class and methods, use the `ContainerBuilder::log()` method instead + * removed `FactoryReturnTypePass` + * removed `ContainerBuilder::addClassResource()`, use the `addObjectResource()` or the `getReflectionClass()` method instead. + * removed support for top-level anonymous services + * removed silent behavior for unused attributes and elements + * removed support for setting and accessing private services in `Container` + * removed support for setting pre-defined services in `Container` + +3.4.0 +----- + + * deprecated service auto-registration while autowiring + * deprecated the ability to check for the initialization of a private service with the `Container::initialized()` method + * deprecated support for top-level anonymous services in XML + +3.3.0 +----- + + * deprecated autowiring services based on the types they implement; + rename (or alias) your services to their FQCN id to make them autowirable + * added "ServiceSubscriberInterface" - to allow for per-class explicit service-locator definitions + * added "container.service_locator" tag for defining service-locator services + * added anonymous services support in YAML configuration files using the `!service` tag. + * added "TypedReference" and "ServiceClosureArgument" for creating service-locator services + * added `ServiceLocator` - a PSR-11 container holding a set of services to be lazily loaded + * added "instanceof" section for local interface-defined configs + * added prototype services for PSR4-based discovery and registration + * added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info + * deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead + * added `ContainerBuilder::fileExists()` for checking and tracking file or directory existence + * deprecated autowiring-types, use aliases instead + * added support for omitting the factory class name in a service definition if the definition class is set + * deprecated case insensitivity of service identifiers + * added "iterator" argument type for lazy iteration over a set of values and services + * added file-wide configurable defaults for service attributes "public", "tags", + "autowire" and "autoconfigure" + * made the "class" attribute optional, using the "id" as fallback + * using the `PhpDumper` with an uncompiled `ContainerBuilder` is deprecated and + will not be supported anymore in 4.0 + * deprecated the `DefinitionDecorator` class in favor of `ChildDefinition` + * allow config files to be loaded using a glob pattern + * [BC BREAK] the `NullDumper` class is now final + +3.2.0 +----- + + * allowed to prioritize compiler passes by introducing a third argument to `PassConfig::addPass()`, to `Compiler::addPass` and to `ContainerBuilder::addCompilerPass()` + * added support for PHP constants in YAML configuration files + * deprecated the ability to set or unset a private service with the `Container::set()` method + * deprecated the ability to check for the existence of a private service with the `Container::has()` method + * deprecated the ability to request a private service with the `Container::get()` method + * deprecated support for generating a dumped `Container` without populating the method map + +3.0.0 +----- + + * removed all deprecated codes from 2.x versions + 2.8.0 ----- diff --git a/src/Symfony/Component/DependencyInjection/ChildDefinition.php b/src/Symfony/Component/DependencyInjection/ChildDefinition.php new file mode 100644 index 0000000000000..fe7c836c47537 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ChildDefinition.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection; + +use Symfony\Component\DependencyInjection\Exception\BadMethodCallException; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; + +/** + * This definition extends another definition. + * + * @author Johannes M. Schmitt + */ +class ChildDefinition extends Definition +{ + private $parent; + + /** + * @param string $parent The id of Definition instance to decorate + */ + public function __construct($parent) + { + $this->parent = $parent; + } + + /** + * Returns the Definition to inherit from. + * + * @return string + */ + public function getParent() + { + return $this->parent; + } + + /** + * Sets the Definition to inherit from. + * + * @param string $parent + * + * @return $this + */ + public function setParent($parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Gets an argument to pass to the service constructor/factory method. + * + * If replaceArgument() has been used to replace an argument, this method + * will return the replacement value. + * + * @param int|string $index + * + * @return mixed The argument value + * + * @throws OutOfBoundsException When the argument does not exist + */ + public function getArgument($index) + { + if (array_key_exists('index_'.$index, $this->arguments)) { + return $this->arguments['index_'.$index]; + } + + return parent::getArgument($index); + } + + /** + * You should always use this method when overwriting existing arguments + * of the parent definition. + * + * If you directly call setArguments() keep in mind that you must follow + * certain conventions when you want to overwrite the arguments of the + * parent definition, otherwise your arguments will only be appended. + * + * @param int|string $index + * @param mixed $value + * + * @return self the current instance + * + * @throws InvalidArgumentException when $index isn't an integer + */ + public function replaceArgument($index, $value) + { + if (is_int($index)) { + $this->arguments['index_'.$index] = $value; + } elseif (0 === strpos($index, '$')) { + $this->arguments[$index] = $value; + } else { + throw new InvalidArgumentException('The argument must be an existing index or the name of a constructor\'s parameter.'); + } + + return $this; + } + + /** + * @internal + */ + public function setAutoconfigured($autoconfigured) + { + throw new BadMethodCallException('A ChildDefinition cannot be autoconfigured.'); + } + + /** + * @internal + */ + public function setInstanceofConditionals(array $instanceof) + { + throw new BadMethodCallException('A ChildDefinition cannot have instanceof conditionals set on it.'); + } + + /** + * @internal + */ + public function setBindings(array $bindings) + { + throw new BadMethodCallException('A ChildDefinition cannot have bindings set on it.'); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php new file mode 100644 index 0000000000000..bbe869b935e5c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Nicolas Grekas + */ +abstract class AbstractRecursivePass implements CompilerPassInterface +{ + protected $container; + protected $currentId; + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $this->container = $container; + + try { + $this->processValue($container->getDefinitions(), true); + } finally { + $this->container = null; + } + } + + /** + * Processes a value found in a definition tree. + * + * @param mixed $value + * @param bool $isRoot + * + * @return mixed The processed value + */ + protected function processValue($value, $isRoot = false) + { + if (is_array($value)) { + foreach ($value as $k => $v) { + if ($isRoot) { + $this->currentId = $k; + } + if ($v !== $processedValue = $this->processValue($v, $isRoot)) { + $value[$k] = $processedValue; + } + } + } elseif ($value instanceof ArgumentInterface) { + $value->setValues($this->processValue($value->getValues())); + } elseif ($value instanceof Definition) { + $value->setArguments($this->processValue($value->getArguments())); + $value->setProperties($this->processValue($value->getProperties())); + $value->setMethodCalls($this->processValue($value->getMethodCalls())); + $value->setBindings($this->processValue($value->getBindings())); + + $changes = $value->getChanges(); + if (isset($changes['factory'])) { + $value->setFactory($this->processValue($value->getFactory())); + } + if (isset($changes['configurator'])) { + $value->setConfigurator($this->processValue($value->getConfigurator())); + } + } + + return $value; + } + + /** + * @param Definition $definition + * @param bool $required + * + * @return \ReflectionFunctionAbstract|null + * + * @throws RuntimeException + */ + protected function getConstructor(Definition $definition, $required) + { + if (is_string($factory = $definition->getFactory())) { + if (!function_exists($factory)) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": function "%s" does not exist.', $this->currentId, $factory)); + } + $r = new \ReflectionFunction($factory); + if (false !== $r->getFileName() && file_exists($r->getFileName())) { + $this->container->fileExists($r->getFileName()); + } + + return $r; + } + + if ($factory) { + list($class, $method) = $factory; + if ($class instanceof Reference) { + $class = $this->container->findDefinition((string) $class)->getClass(); + } elseif (null === $class) { + $class = $definition->getClass(); + } + if ('__construct' === $method) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": "__construct()" cannot be used as a factory method.', $this->currentId)); + } + + return $this->getReflectionMethod(new Definition($class), $method); + } + + $class = $definition->getClass(); + + if (!$r = $this->container->getReflectionClass($class)) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class)); + } + if (!$r = $r->getConstructor()) { + if ($required) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": class%s has no constructor.', $this->currentId, sprintf($class !== $this->currentId ? ' "%s"' : '', $class))); + } + } elseif (!$r->isPublic()) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": %s must be public.', $this->currentId, sprintf($class !== $this->currentId ? 'constructor of class "%s"' : 'its constructor', $class))); + } + + return $r; + } + + /** + * @param Definition $definition + * @param string $method + * + * @throws RuntimeException + * + * @return \ReflectionFunctionAbstract + */ + protected function getReflectionMethod(Definition $definition, $method) + { + if ('__construct' === $method) { + return $this->getConstructor($definition, true); + } + + if (!$class = $definition->getClass()) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId)); + } + + if (!$r = $this->container->getReflectionClass($class)) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class)); + } + + if (!$r->hasMethod($method)) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": method "%s()" does not exist.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); + } + + $r = $r->getMethod($method); + if (!$r->isPublic()) { + throw new RuntimeException(sprintf('Unable to resolve service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); + } + + return $r; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php index 681f8afdde744..3a375f351652d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php @@ -11,9 +11,12 @@ namespace Symfony\Component\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ExpressionLanguage; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\ExpressionLanguage\Expression; /** * Run this pass before passes that need to know more about the relation of @@ -24,14 +27,14 @@ * * @author Johannes M. Schmitt */ -class AnalyzeServiceReferencesPass implements RepeatablePassInterface +class AnalyzeServiceReferencesPass extends AbstractRecursivePass implements RepeatablePassInterface { private $graph; - private $container; - private $currentId; private $currentDefinition; private $repeatedPass; private $onlyConstructorArguments; + private $lazy; + private $expressionLanguage; /** * @param bool $onlyConstructorArguments Sets this Service Reference pass to ignore method calls @@ -59,68 +62,67 @@ public function process(ContainerBuilder $container) $this->container = $container; $this->graph = $container->getCompiler()->getServiceReferenceGraph(); $this->graph->clear(); - - foreach ($container->getDefinitions() as $id => $definition) { - if ($definition->isSynthetic() || $definition->isAbstract()) { - continue; - } - - $this->currentId = $id; - $this->currentDefinition = $definition; - - $this->processArguments($definition->getArguments()); - if ($definition->getFactoryService(false)) { - $this->processArguments(array(new Reference($definition->getFactoryService(false)))); - } - if (is_array($definition->getFactory())) { - $this->processArguments($definition->getFactory()); - } - - if (!$this->onlyConstructorArguments) { - $this->processArguments($definition->getMethodCalls()); - $this->processArguments($definition->getProperties()); - if ($definition->getConfigurator()) { - $this->processArguments(array($definition->getConfigurator())); - } - } - } + $this->lazy = false; foreach ($container->getAliases() as $id => $alias) { $this->graph->connect($id, $alias, (string) $alias, $this->getDefinition((string) $alias), null); } + + parent::process($container); } - /** - * Processes service definitions for arguments to find relationships for the service graph. - * - * @param array $arguments An array of Reference or Definition objects relating to service definitions - */ - private function processArguments(array $arguments) + protected function processValue($value, $isRoot = false) { - foreach ($arguments as $argument) { - if (is_array($argument)) { - $this->processArguments($argument); - } elseif ($argument instanceof Reference) { - $this->graph->connect( - $this->currentId, - $this->currentDefinition, - $this->getDefinitionId((string) $argument), - $this->getDefinition((string) $argument), - $argument - ); - } elseif ($argument instanceof Definition) { - $this->processArguments($argument->getArguments()); - $this->processArguments($argument->getMethodCalls()); - $this->processArguments($argument->getProperties()); - - if (is_array($argument->getFactory())) { - $this->processArguments($argument->getFactory()); - } - if ($argument->getFactoryService(false)) { - $this->processArguments(array(new Reference($argument->getFactoryService(false)))); - } + $lazy = $this->lazy; + + if ($value instanceof ArgumentInterface) { + $this->lazy = true; + parent::processValue($value->getValues()); + $this->lazy = $lazy; + + return $value; + } + if ($value instanceof Expression) { + $this->getExpressionLanguage()->compile((string) $value, array('this' => 'container')); + + return $value; + } + if ($value instanceof Reference) { + $targetDefinition = $this->getDefinition((string) $value); + + $this->graph->connect( + $this->currentId, + $this->currentDefinition, + $this->getDefinitionId((string) $value), + $targetDefinition, + $value, + $this->lazy || ($targetDefinition && $targetDefinition->isLazy()) + ); + + return $value; + } + if (!$value instanceof Definition) { + return parent::processValue($value, $isRoot); + } + if ($isRoot) { + if ($value->isSynthetic() || $value->isAbstract()) { + return $value; } + $this->currentDefinition = $value; + } + $this->lazy = false; + + $this->processValue($value->getFactory()); + $this->processValue($value->getArguments()); + + if (!$this->onlyConstructorArguments) { + $this->processValue($value->getProperties()); + $this->processValue($value->getMethodCalls()); + $this->processValue($value->getConfigurator()); } + $this->lazy = $lazy; + + return $value; } /** @@ -149,4 +151,27 @@ private function getDefinitionId($id) return $id; } + + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + $providers = $this->container->getExpressionLanguageProviders(); + $this->expressionLanguage = new ExpressionLanguage(null, $providers, function ($arg) { + if ('""' === substr_replace($arg, '', 1, -1)) { + $id = stripcslashes(substr($arg, 1, -1)); + + $this->graph->connect( + $this->currentId, + $this->currentDefinition, + $this->getDefinitionId($id), + $this->getDefinition($id) + ); + } + + return sprintf('$this->get(%s)', $arg); + }); + } + + return $this->expressionLanguage; + } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowireExceptionPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowireExceptionPass.php new file mode 100644 index 0000000000000..12be3d915fd10 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowireExceptionPass.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Throws autowire exceptions from AutowirePass for definitions that still exist. + * + * @author Ryan Weaver + */ +class AutowireExceptionPass implements CompilerPassInterface +{ + private $autowirePass; + private $inlineServicePass; + + public function __construct(AutowirePass $autowirePass, InlineServiceDefinitionsPass $inlineServicePass) + { + $this->autowirePass = $autowirePass; + $this->inlineServicePass = $inlineServicePass; + } + + public function process(ContainerBuilder $container) + { + // the pass should only be run once + if (null === $this->autowirePass || null === $this->inlineServicePass) { + return; + } + + $inlinedIds = $this->inlineServicePass->getInlinedServiceIds(); + $exceptions = $this->autowirePass->getAutowiringExceptions(); + + // free up references + $this->autowirePass = null; + $this->inlineServicePass = null; + + foreach ($exceptions as $exception) { + if ($this->doesServiceExistInTheContainer($exception->getServiceId(), $container, $inlinedIds)) { + throw $exception; + } + } + } + + private function doesServiceExistInTheContainer($serviceId, ContainerBuilder $container, array $inlinedIds) + { + if ($container->hasDefinition($serviceId)) { + return true; + } + + // was the service inlined? Of so, does its parent service exist? + if (isset($inlinedIds[$serviceId])) { + foreach ($inlinedIds[$serviceId] as $parentId) { + if ($this->doesServiceExistInTheContainer($parentId, $container, $inlinedIds)) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index b279548553161..a87fc940af244 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -13,141 +13,263 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; -use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; +use Symfony\Component\DependencyInjection\TypedReference; /** - * Guesses constructor arguments of services definitions and try to instantiate services if necessary. + * Inspects existing service definitions and wires the autowired ones using the type hints of their classes. * * @author Kévin Dunglas + * @author Nicolas Grekas */ -class AutowirePass implements CompilerPassInterface +class AutowirePass extends AbstractRecursivePass { - private $container; - private $reflectionClasses = array(); - private $definedTypes = array(); private $types; - private $notGuessableTypes = array(); + private $ambiguousServiceTypes = array(); private $autowired = array(); + private $lastFailure; + private $throwOnAutowiringException; + private $autowiringExceptions = array(); + + /** + * @param bool $throwOnAutowireException If false, retrieved errors via getAutowiringExceptions + */ + public function __construct($throwOnAutowireException = true) + { + $this->throwOnAutowiringException = $throwOnAutowireException; + } + + /** + * @return AutowiringFailedException[] + */ + public function getAutowiringExceptions() + { + return $this->autowiringExceptions; + } /** * {@inheritdoc} */ public function process(ContainerBuilder $container) { - $throwingAutoloader = function ($class) { throw new \ReflectionException(sprintf('Class %s does not exist', $class)); }; - spl_autoload_register($throwingAutoloader); + // clear out any possibly stored exceptions from before + $this->autowiringExceptions = array(); try { - $this->container = $container; - foreach ($container->getDefinitions() as $id => $definition) { - if ($definition->isAutowired()) { - $this->completeDefinition($id, $definition); - } + parent::process($container); + } finally { + $this->types = null; + $this->ambiguousServiceTypes = array(); + $this->autowired = array(); + } + } + + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + try { + return $this->doProcessValue($value, $isRoot); + } catch (AutowiringFailedException $e) { + if ($this->throwOnAutowiringException) { + throw $e; + } + + $this->autowiringExceptions[] = $e; + + return parent::processValue($value, $isRoot); + } + } + + private function doProcessValue($value, $isRoot = false) + { + if ($value instanceof TypedReference) { + if ($ref = $this->getAutowiredReference($value, $value->getRequiringClass() ? sprintf('for "%s" in "%s"', $value->getType(), $value->getRequiringClass()) : '')) { + return $ref; } - } catch (\Exception $e) { - } catch (\Throwable $e) { + $this->container->log($this, $this->createTypeNotFoundMessage($value, 'it')); + } + $value = parent::processValue($value, $isRoot); + + if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) { + return $value; + } + if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) { + $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass())); + + return $value; + } + + $autowiredMethods = $this->getMethodsToAutowire($reflectionClass); + $methodCalls = $value->getMethodCalls(); + + try { + $constructor = $this->getConstructor($value, false); + } catch (RuntimeException $e) { + throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e); + } + + if ($constructor) { + array_unshift($methodCalls, array($constructor, $value->getArguments())); } - spl_autoload_unregister($throwingAutoloader); + $methodCalls = $this->autowireCalls($reflectionClass, $methodCalls, $autowiredMethods); - // Free memory and remove circular reference to container - $this->container = null; - $this->reflectionClasses = array(); - $this->definedTypes = array(); - $this->types = null; - $this->notGuessableTypes = array(); - $this->autowired = array(); + if ($constructor) { + list(, $arguments) = array_shift($methodCalls); - if (isset($e)) { - throw $e; + if ($arguments !== $value->getArguments()) { + $value->setArguments($arguments); + } + } + + if ($methodCalls !== $value->getMethodCalls()) { + $value->setMethodCalls($methodCalls); } + + return $value; } /** - * Wires the given definition. + * Gets the list of methods to autowire. * - * @param string $id - * @param Definition $definition + * @param \ReflectionClass $reflectionClass * - * @throws RuntimeException + * @return \ReflectionMethod[] */ - private function completeDefinition($id, Definition $definition) + private function getMethodsToAutowire(\ReflectionClass $reflectionClass) { - if ($definition->getFactory() || $definition->getFactoryClass(false) || $definition->getFactoryService(false)) { - throw new RuntimeException(sprintf('Service "%s" can use either autowiring or a factory, not both.', $id)); + $methodsToAutowire = array(); + + foreach ($reflectionClass->getMethods() as $reflectionMethod) { + $r = $reflectionMethod; + + if ($r->isConstructor()) { + continue; + } + + while (true) { + if (false !== $doc = $r->getDocComment()) { + if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) { + $methodsToAutowire[strtolower($reflectionMethod->name)] = $reflectionMethod; + break; + } + if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) { + break; + } + } + try { + $r = $r->getPrototype(); + } catch (\ReflectionException $e) { + break; // method has no prototype + } + } } - if (!$reflectionClass = $this->getReflectionClass($id, $definition)) { - return; + return $methodsToAutowire; + } + + /** + * @param \ReflectionClass $reflectionClass + * @param array $methodCalls + * @param \ReflectionMethod[] $autowiredMethods + * + * @return array + */ + private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls, array $autowiredMethods) + { + foreach ($methodCalls as $i => $call) { + list($method, $arguments) = $call; + + if ($method instanceof \ReflectionFunctionAbstract) { + $reflectionMethod = $method; + } elseif (isset($autowiredMethods[$lcMethod = strtolower($method)]) && $autowiredMethods[$lcMethod]->isPublic()) { + $reflectionMethod = $autowiredMethods[$lcMethod]; + unset($autowiredMethods[$lcMethod]); + } else { + $reflectionMethod = $this->getReflectionMethod(new Definition($reflectionClass->name), $method); + } + + $arguments = $this->autowireMethod($reflectionMethod, $arguments); + + if ($arguments !== $call[1]) { + $methodCalls[$i][1] = $arguments; + } } - $this->container->addClassResource($reflectionClass); + foreach ($autowiredMethods as $lcMethod => $reflectionMethod) { + $method = $reflectionMethod->name; - if (!$constructor = $reflectionClass->getConstructor()) { - return; + if (!$reflectionMethod->isPublic()) { + $class = $reflectionClass->name; + throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method)); + } + $methodCalls[] = array($method, $this->autowireMethod($reflectionMethod, array())); } - $parameters = $constructor->getParameters(); - if (method_exists('ReflectionMethod', 'isVariadic') && $constructor->isVariadic()) { + + return $methodCalls; + } + + /** + * Autowires the constructor or a method. + * + * @param \ReflectionFunctionAbstract $reflectionMethod + * @param array $arguments + * + * @return array The autowired arguments + * + * @throws AutowiringFailedException + */ + private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments) + { + $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId; + $method = $reflectionMethod->name; + $parameters = $reflectionMethod->getParameters(); + if ($reflectionMethod->isVariadic()) { array_pop($parameters); } - $arguments = $definition->getArguments(); foreach ($parameters as $index => $parameter) { if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) { continue; } - try { - if (!$typeHint = $parameter->getClass()) { - if (isset($arguments[$index])) { - continue; - } - - // no default value? Then fail - if (!$parameter->isOptional()) { - throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id)); - } - - // specifically pass the default value - $arguments[$index] = $parameter->getDefaultValue(); + $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true); + if (!$type) { + if (isset($arguments[$index])) { continue; } - if (isset($this->autowired[$typeHint->name])) { - $arguments[$index] = $this->autowired[$typeHint->name] ? new Reference($this->autowired[$typeHint->name]) : null; - continue; + // no default value? Then fail + if (!$parameter->isDefaultValueAvailable()) { + // For core classes, isDefaultValueAvailable() can + // be false when isOptional() returns true. If the + // argument *is* optional, allow it to be missing + if ($parameter->isOptional()) { + continue; + } + throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" must have a type-hint or be given a value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method)); } - if (null === $this->types) { - $this->populateAvailableTypes(); - } + // specifically pass the default value + $arguments[$index] = $parameter->getDefaultValue(); - if (isset($this->types[$typeHint->name]) && !isset($this->notGuessableTypes[$typeHint->name])) { - $value = new Reference($this->types[$typeHint->name]); - } else { - try { - $value = $this->createAutowiredDefinition($typeHint, $id); - } catch (RuntimeException $e) { - if ($parameter->isDefaultValueAvailable()) { - $value = $parameter->getDefaultValue(); - } elseif ($parameter->allowsNull()) { - $value = null; - } else { - throw $e; - } - $this->autowired[$typeHint->name] = false; - } - } - } catch (\ReflectionException $e) { - // Typehint against a non-existing class + continue; + } - if (!$parameter->isDefaultValueAvailable()) { - throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e); - } + if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$method))) { + $failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method)); - $value = $parameter->getDefaultValue(); + if ($parameter->isDefaultValueAvailable()) { + $value = $parameter->getDefaultValue(); + } elseif (!$parameter->allowsNull()) { + throw new AutowiringFailedException($this->currentId, $failureMessage); + } + $this->container->log($this, $failureMessage); } $arguments[$index] = $value; @@ -166,7 +288,39 @@ private function completeDefinition($id, Definition $definition) // it's possible index 1 was set, then index 0, then 2, etc // make sure that we re-order so they're injected as expected ksort($arguments); - $definition->setArguments($arguments); + + return $arguments; + } + + /** + * @return TypedReference|null A reference to the service matching the given type, if any + */ + private function getAutowiredReference(TypedReference $reference, $deprecationMessage) + { + $this->lastFailure = null; + $type = $reference->getType(); + + if ($type !== (string) $reference || ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract())) { + return $reference; + } + + if (!$reference->canBeAutoregistered()) { + return; + } + + if (null === $this->types) { + $this->populateAvailableTypes(); + } + + if (isset($this->types[$type]) || isset($this->ambiguousServiceTypes[$type])) { + return; + } + + if (isset($this->autowired[$type])) { + return $this->autowired[$type] ? new TypedReference($this->autowired[$type], $type) : null; + } + + return $this->createAutowiredDefinition($type); } /** @@ -194,13 +348,7 @@ private function populateAvailableType($id, Definition $definition) return; } - foreach ($definition->getAutowiringTypes() as $type) { - $this->definedTypes[$type] = true; - $this->types[$type] = $id; - unset($this->notGuessableTypes[$type]); - } - - if (!$reflectionClass = $this->getReflectionClass($id, $definition)) { + if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) { return; } @@ -221,114 +369,132 @@ private function populateAvailableType($id, Definition $definition) */ private function set($type, $id) { - if (isset($this->definedTypes[$type])) { + // is this already a type/class that is known to match multiple services? + if (isset($this->ambiguousServiceTypes[$type])) { + $this->ambiguousServiceTypes[$type][] = $id; + return; } - if (!isset($this->types[$type])) { + // check to make sure the type doesn't match multiple services + if (!isset($this->types[$type]) || $this->types[$type] === $id) { $this->types[$type] = $id; return; } - if ($this->types[$type] === $id) { - return; - } - - if (!isset($this->notGuessableTypes[$type])) { - $this->notGuessableTypes[$type] = true; - $this->types[$type] = (array) $this->types[$type]; + // keep an array of all services matching this type + if (!isset($this->ambiguousServiceTypes[$type])) { + $this->ambiguousServiceTypes[$type] = array($this->types[$type]); + unset($this->types[$type]); } - - $this->types[$type][] = $id; + $this->ambiguousServiceTypes[$type][] = $id; } /** * Registers a definition for the type if possible or throws an exception. * - * @param \ReflectionClass $typeHint - * @param string $id - * - * @return Reference A reference to the registered definition + * @param string $type * - * @throws RuntimeException + * @return TypedReference|null A reference to the registered definition */ - private function createAutowiredDefinition(\ReflectionClass $typeHint, $id) + private function createAutowiredDefinition($type) { - if (isset($this->notGuessableTypes[$typeHint->name])) { - $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; - $matchingServices = implode(', ', $this->types[$typeHint->name]); - - throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices)); - } - - if (!$typeHint->isInstantiable()) { - $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; - throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface)); + if (!($typeHint = $this->container->getReflectionClass($type, false)) || !$typeHint->isInstantiable()) { + return; } - $this->autowired[$typeHint->name] = $argumentId = sprintf('autowired.%s', $typeHint->name); - - $argumentDefinition = $this->container->register($argumentId, $typeHint->name); + $currentId = $this->currentId; + $this->currentId = $type; + $this->autowired[$type] = $argumentId = sprintf('autowired.%s', $type); + $argumentDefinition = new Definition($type); $argumentDefinition->setPublic(false); + $argumentDefinition->setAutowired(true); try { - $this->completeDefinition($argumentId, $argumentDefinition); - } catch (RuntimeException $e) { - $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; - $message = sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface); - throw new RuntimeException($message, 0, $e); + $originalThrowSetting = $this->throwOnAutowiringException; + $this->throwOnAutowiringException = true; + $this->processValue($argumentDefinition, true); + $this->container->setDefinition($argumentId, $argumentDefinition); + } catch (AutowiringFailedException $e) { + $this->autowired[$type] = false; + $this->lastFailure = $e->getMessage(); + $this->container->log($this, $this->lastFailure); + + return; + } finally { + $this->throwOnAutowiringException = $originalThrowSetting; + $this->currentId = $currentId; } - return new Reference($argumentId); + @trigger_error(sprintf('Relying on service auto-registration for type "%s" is deprecated since version 3.4 and won\'t be supported in 4.0. Create a service named "%s" instead.', $type, $type), E_USER_DEPRECATED); + + $this->container->log($this, sprintf('Type "%s" has been auto-registered for service "%s".', $type, $this->currentId)); + + return new TypedReference($argumentId, $type); } - /** - * Retrieves the reflection class associated with the given service. - * - * @param string $id - * @param Definition $definition - * - * @return \ReflectionClass|false - */ - private function getReflectionClass($id, Definition $definition) + private function createTypeNotFoundMessage(TypedReference $reference, $label) { - if (isset($this->reflectionClasses[$id])) { - return $this->reflectionClasses[$id]; + if (!$r = $this->container->getReflectionClass($type = $reference->getType(), false)) { + $message = sprintf('has type "%s" but this class cannot be loaded.', $type); + } else { + $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists'; + $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $this->createTypeAlternatives($reference)); } - // Cannot use reflection if the class isn't set - if (!$class = $definition->getClass()) { - return false; + $message = sprintf('Cannot autowire service "%s": %s %s', $this->currentId, $label, $message); + + if (null !== $this->lastFailure) { + $message = $this->lastFailure."\n".$message; + $this->lastFailure = null; } - $class = $this->container->getParameterBag()->resolveValue($class); + return $message; + } - if ($deprecated = $definition->isDeprecated()) { - $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) { - return (E_USER_DEPRECATED === $level || !$prevErrorHandler) ? false : $prevErrorHandler($level, $message, $file, $line); - }); + private function createTypeAlternatives(TypedReference $reference) + { + // try suggesting available aliases first + if ($message = $this->getAliasesSuggestionForType($type = $reference->getType())) { + return ' '.$message; } - $e = null; - - try { - $reflector = new \ReflectionClass($class); - } catch (\Exception $e) { - } catch (\Throwable $e) { + if (isset($this->ambiguousServiceTypes[$type])) { + $message = sprintf('one of these existing services: "%s"', implode('", "', $this->ambiguousServiceTypes[$type])); + } elseif (isset($this->types[$type])) { + $message = sprintf('the existing "%s" service', $this->types[$type]); + } elseif ($reference->getRequiringClass() && !$reference->canBeAutoregistered()) { + return ' It cannot be auto-registered because it is from a different root namespace.'; + } else { + return; } - if ($deprecated) { - restore_error_handler(); + return sprintf(' You should maybe alias this %s to %s.', class_exists($type, false) ? 'class' : 'interface', $message); + } + + private function getAliasesSuggestionForType($type, $extraContext = null) + { + $aliases = array(); + foreach (class_parents($type) + class_implements($type) as $parent) { + if ($this->container->has($parent) && !$this->container->findDefinition($parent)->isAbstract()) { + $aliases[] = $parent; + } } - if (null !== $e) { - if (!$e instanceof \ReflectionException) { - throw $e; + $extraContext = $extraContext ? ' '.$extraContext : ''; + if (1 < $len = count($aliases)) { + $message = sprintf('Try changing the type-hint%s to one of its parents: ', $extraContext); + for ($i = 0, --$len; $i < $len; ++$i) { + $message .= sprintf('%s "%s", ', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]); } - $reflector = false; + $message .= sprintf('or %s "%s".', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]); + + return $message; } - return $this->reflectionClasses[$id] = $reflector; + if ($aliases) { + return sprintf('Try changing the type-hint%s to "%s" instead.', $extraContext, $aliases[0]); + } } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php new file mode 100644 index 0000000000000..6b48a156919da --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckArgumentsValidityPass.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; + +/** + * Checks if arguments of methods are properly configured. + * + * @author Kévin Dunglas + * @author Nicolas Grekas + */ +class CheckArgumentsValidityPass extends AbstractRecursivePass +{ + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition) { + return parent::processValue($value, $isRoot); + } + + $i = 0; + foreach ($value->getArguments() as $k => $v) { + if ($k !== $i++) { + if (!is_int($k)) { + throw new RuntimeException(sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k)); + } + + throw new RuntimeException(sprintf('Invalid constructor argument %d for service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $this->currentId, $i)); + } + } + + foreach ($value->getMethodCalls() as $methodCall) { + $i = 0; + foreach ($methodCall[1] as $k => $v) { + if ($k !== $i++) { + if (!is_int($k)) { + throw new RuntimeException(sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k)); + } + + throw new RuntimeException(sprintf('Invalid argument %d for method call "%s" of service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $methodCall[0], $this->currentId, $i)); + } + } + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php index f39a89af2be71..1bcf3d141924c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php @@ -60,8 +60,8 @@ private function checkOutEdges(array $edges) $id = $node->getId(); if (empty($this->checkedNodes[$id])) { - // don't check circular dependencies for lazy services - if (!$node->getValue() || !$node->getValue()->isLazy()) { + // Don't check circular references for lazy edges + if (!$node->getValue() || !$edge->isLazy()) { $searchKey = array_search($id, $this->currentPath); $this->currentPath[] = $id; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php index e54ee60abbcaf..a43e0cd78f404 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php @@ -11,7 +11,6 @@ namespace Symfony\Component\DependencyInjection\Compiler; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -24,8 +23,6 @@ * * - non synthetic, non abstract services always have a class set * - synthetic services are always public - * - synthetic services are always of non-prototype scope - * - shared services are always of non-prototype scope * * @author Johannes M. Schmitt */ @@ -46,25 +43,20 @@ public function process(ContainerBuilder $container) throw new RuntimeException(sprintf('A synthetic service ("%s") must be public.', $id)); } - // synthetic service has non-prototype scope - if ($definition->isSynthetic() && ContainerInterface::SCOPE_PROTOTYPE === $definition->getScope(false)) { - throw new RuntimeException(sprintf('A synthetic service ("%s") cannot be of scope "prototype".', $id)); - } - - // shared service has non-prototype scope - if ($definition->isShared() && ContainerInterface::SCOPE_PROTOTYPE === $definition->getScope(false)) { - throw new RuntimeException(sprintf('A shared service ("%s") cannot be of scope "prototype".', $id)); - } - - if ($definition->getFactory() && ($definition->getFactoryClass(false) || $definition->getFactoryService(false) || $definition->getFactoryMethod(false))) { - throw new RuntimeException(sprintf('A service ("%s") can use either the old or the new factory syntax, not both.', $id)); - } - // non-synthetic, non-abstract service has class if (!$definition->isAbstract() && !$definition->isSynthetic() && !$definition->getClass()) { - if ($definition->getFactory() || $definition->getFactoryClass(false) || $definition->getFactoryService(false)) { + if ($definition->getFactory()) { throw new RuntimeException(sprintf('Please add the class to service "%s" even if it is constructed by a factory since we might need to add method calls based on compile-time checks.', $id)); } + if (class_exists($id) || interface_exists($id, false)) { + throw new RuntimeException(sprintf( + 'The definition for "%s" has no class attribute, and appears to reference a ' + .'class or interface in the global namespace. Leaving out the "class" attribute ' + .'is only allowed for namespaced classes. Please specify the class attribute ' + .'explicitly to get rid of this error.', + $id + )); + } throw new RuntimeException(sprintf( 'The definition for "%s" has no class. If you intend to inject ' diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php index 304f7849516c8..35fb325e74964 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -11,53 +11,27 @@ namespace Symfony\Component\DependencyInjection\Compiler; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Checks that all references are pointing to a valid service. * * @author Johannes M. Schmitt */ -class CheckExceptionOnInvalidReferenceBehaviorPass implements CompilerPassInterface +class CheckExceptionOnInvalidReferenceBehaviorPass extends AbstractRecursivePass { - private $container; - private $sourceId; - - public function process(ContainerBuilder $container) - { - $this->container = $container; - - foreach ($container->getDefinitions() as $id => $definition) { - $this->sourceId = $id; - $this->processDefinition($definition); - } - } - - private function processDefinition(Definition $definition) + protected function processValue($value, $isRoot = false) { - $this->processReferences($definition->getArguments()); - $this->processReferences($definition->getMethodCalls()); - $this->processReferences($definition->getProperties()); - } + if ($value instanceof Reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) { + $destId = (string) $value; - private function processReferences(array $arguments) - { - foreach ($arguments as $argument) { - if (is_array($argument)) { - $this->processReferences($argument); - } elseif ($argument instanceof Definition) { - $this->processDefinition($argument); - } elseif ($argument instanceof Reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $argument->getInvalidBehavior()) { - $destId = (string) $argument; - - if (!$this->container->has($destId)) { - throw new ServiceNotFoundException($destId, $this->sourceId); - } + if (!$this->container->has($destId)) { + throw new ServiceNotFoundException($destId, $this->currentId); } } + + return parent::processValue($value, $isRoot); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckReferenceValidityPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckReferenceValidityPass.php index ac4072a849023..72c7dd165d4af 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckReferenceValidityPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckReferenceValidityPass.php @@ -12,154 +12,37 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\RuntimeException; -use Symfony\Component\DependencyInjection\Exception\ScopeCrossingInjectionException; -use Symfony\Component\DependencyInjection\Exception\ScopeWideningInjectionException; /** * Checks the validity of references. * * The following checks are performed by this pass: * - target definitions are not abstract - * - target definitions are of equal or wider scope - * - target definitions are in the same scope hierarchy * * @author Johannes M. Schmitt */ -class CheckReferenceValidityPass implements CompilerPassInterface +class CheckReferenceValidityPass extends AbstractRecursivePass { - private $container; - private $currentId; - private $currentScope; - private $currentScopeAncestors; - private $currentScopeChildren; - - /** - * Processes the ContainerBuilder to validate References. - * - * @param ContainerBuilder $container - */ - public function process(ContainerBuilder $container) + protected function processValue($value, $isRoot = false) { - $this->container = $container; - - $children = $this->container->getScopeChildren(false); - $ancestors = array(); - - $scopes = $this->container->getScopes(false); - foreach ($scopes as $name => $parent) { - $ancestors[$name] = array($parent); - - while (isset($scopes[$parent])) { - $ancestors[$name][] = $parent = $scopes[$parent]; - } - } - - foreach ($container->getDefinitions() as $id => $definition) { - if ($definition->isSynthetic() || $definition->isAbstract()) { - continue; - } - - $this->currentId = $id; - $this->currentScope = $scope = $definition->getScope(false); - - if (ContainerInterface::SCOPE_CONTAINER === $scope) { - $this->currentScopeChildren = array_keys($scopes); - $this->currentScopeAncestors = array(); - } elseif (ContainerInterface::SCOPE_PROTOTYPE !== $scope) { - $this->currentScopeChildren = isset($children[$scope]) ? $children[$scope] : array(); - $this->currentScopeAncestors = isset($ancestors[$scope]) ? $ancestors[$scope] : array(); + if ($isRoot && $value instanceof Definition && ($value->isSynthetic() || $value->isAbstract())) { + return $value; + } + if ($value instanceof Reference && $this->container->hasDefinition((string) $value)) { + $targetDefinition = $this->container->getDefinition((string) $value); + + if ($targetDefinition->isAbstract()) { + throw new RuntimeException(sprintf( + 'The definition "%s" has a reference to an abstract definition "%s". ' + .'Abstract definitions cannot be the target of references.', + $this->currentId, + $value + )); } - - $this->validateReferences($definition->getArguments()); - $this->validateReferences($definition->getMethodCalls()); - $this->validateReferences($definition->getProperties()); - } - } - - /** - * Validates an array of References. - * - * @param array $arguments An array of Reference objects - * - * @throws RuntimeException when there is a reference to an abstract definition. - */ - private function validateReferences(array $arguments) - { - foreach ($arguments as $argument) { - if (is_array($argument)) { - $this->validateReferences($argument); - } elseif ($argument instanceof Reference) { - $targetDefinition = $this->getDefinition((string) $argument); - - if (null !== $targetDefinition && $targetDefinition->isAbstract()) { - throw new RuntimeException(sprintf( - 'The definition "%s" has a reference to an abstract definition "%s". ' - .'Abstract definitions cannot be the target of references.', - $this->currentId, - $argument - )); - } - - $this->validateScope($argument, $targetDefinition); - } - } - } - - /** - * Validates the scope of a single Reference. - * - * @param Reference $reference - * @param Definition $definition - * - * @throws ScopeWideningInjectionException when the definition references a service of a narrower scope - * @throws ScopeCrossingInjectionException when the definition references a service of another scope hierarchy - */ - private function validateScope(Reference $reference, Definition $definition = null) - { - if (ContainerInterface::SCOPE_PROTOTYPE === $this->currentScope) { - return; - } - - if (!$reference->isStrict(false)) { - return; - } - - if (null === $definition) { - return; - } - - if ($this->currentScope === $scope = $definition->getScope(false)) { - return; - } - - $id = (string) $reference; - - if (in_array($scope, $this->currentScopeChildren, true)) { - throw new ScopeWideningInjectionException($this->currentId, $this->currentScope, $id, $scope); - } - - if (!in_array($scope, $this->currentScopeAncestors, true)) { - throw new ScopeCrossingInjectionException($this->currentId, $this->currentScope, $id, $scope); - } - } - - /** - * Returns the Definition given an id. - * - * @param string $id Definition identifier - * - * @return Definition - */ - private function getDefinition($id) - { - if (!$this->container->hasDefinition($id)) { - return; } - return $this->container->getDefinition($id); + return parent::processValue($value, $isRoot); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php index 1f6304ee82e13..e58b3dbe7fce5 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\EnvParameterException; /** * This class is used to remove circular dependencies between individual passes. @@ -22,14 +23,12 @@ class Compiler { private $passConfig; private $log = array(); - private $loggingFormatter; private $serviceReferenceGraph; public function __construct() { $this->passConfig = new PassConfig(); $this->serviceReferenceGraph = new ServiceReferenceGraph(); - $this->loggingFormatter = new LoggingFormatter(); } /** @@ -52,35 +51,28 @@ public function getServiceReferenceGraph() return $this->serviceReferenceGraph; } - /** - * Returns the logging formatter which can be used by compilation passes. - * - * @return LoggingFormatter - */ - public function getLoggingFormatter() - { - return $this->loggingFormatter; - } - /** * Adds a pass to the PassConfig. * - * @param CompilerPassInterface $pass A compiler pass - * @param string $type The type of the pass + * @param CompilerPassInterface $pass A compiler pass + * @param string $type The type of the pass + * @param int $priority Used to sort the passes */ - public function addPass(CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION) + public function addPass(CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0) { - $this->passConfig->addPass($pass, $type); + $this->passConfig->addPass($pass, $type, $priority); } /** - * Adds a log message. - * - * @param string $string The log message + * @final */ - public function addLogMessage($string) + public function log(CompilerPassInterface $pass, $message) { - $this->log[] = $string; + if (false !== strpos($message, "\n")) { + $message = str_replace("\n", "\n".get_class($pass).': ', trim($message)); + } + + $this->log[] = get_class($pass).': '.$message; } /** @@ -100,8 +92,29 @@ public function getLog() */ public function compile(ContainerBuilder $container) { - foreach ($this->passConfig->getPasses() as $pass) { - $pass->process($container); + try { + foreach ($this->passConfig->getPasses() as $pass) { + $pass->process($container); + } + } catch (\Exception $e) { + $usedEnvs = array(); + $prev = $e; + + do { + $msg = $prev->getMessage(); + + if ($msg !== $resolvedMsg = $container->resolveEnvPlaceholders($msg, null, $usedEnvs)) { + $r = new \ReflectionProperty($prev, 'message'); + $r->setAccessible(true); + $r->setValue($prev, $resolvedMsg); + } + } while ($prev = $prev->getPrevious()); + + if ($usedEnvs) { + $e = new EnvParameterException($usedEnvs, $e); + } + + throw $e; } } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php index 2c4c79d60e9d5..7d00ce221e3e7 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php @@ -35,8 +35,7 @@ public function process(ContainerBuilder $container) $definitions->insert(array($id, $definition), array($decorated[2], --$order)); } - foreach ($definitions as $arr) { - list($id, $definition) = $arr; + foreach ($definitions as list($id, $definition)) { list($inner, $renamedId) = $definition->getDecoratedService(); $definition->setDecoratedService(null); @@ -54,11 +53,9 @@ public function process(ContainerBuilder $container) } else { $decoratedDefinition = $container->getDefinition($inner); $definition->setTags(array_merge($decoratedDefinition->getTags(), $definition->getTags())); - $definition->setAutowiringTypes(array_merge($decoratedDefinition->getAutowiringTypes(), $definition->getAutowiringTypes())); $public = $decoratedDefinition->isPublic(); $decoratedDefinition->setPublic(false); $decoratedDefinition->setTags(array()); - $decoratedDefinition->setAutowiringTypes(array()); $container->setDefinition($renamedId, $decoratedDefinition); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index bf95ea5b0a269..f2ef363c837cc 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -11,23 +11,19 @@ namespace Symfony\Component\DependencyInjection\Compiler; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Inline service definitions where this is possible. * * @author Johannes M. Schmitt */ -class InlineServiceDefinitionsPass implements RepeatablePassInterface +class InlineServiceDefinitionsPass extends AbstractRecursivePass implements RepeatablePassInterface { private $repeatedPass; - private $graph; - private $compiler; - private $formatter; - private $currentId; + private $inlinedServiceIds = array(); /** * {@inheritdoc} @@ -38,78 +34,51 @@ public function setRepeatedPass(RepeatedPass $repeatedPass) } /** - * Processes the ContainerBuilder for inline service definitions. + * Returns an array of all services inlined by this pass. * - * @param ContainerBuilder $container + * The key is the inlined service id and its value is the list of services it was inlined into. + * + * @return array */ - public function process(ContainerBuilder $container) + public function getInlinedServiceIds() { - $this->compiler = $container->getCompiler(); - $this->formatter = $this->compiler->getLoggingFormatter(); - $this->graph = $this->compiler->getServiceReferenceGraph(); - - $container->setDefinitions($this->inlineArguments($container, $container->getDefinitions(), true)); + return $this->inlinedServiceIds; } /** - * Processes inline arguments. - * - * @param ContainerBuilder $container The ContainerBuilder - * @param array $arguments An array of arguments - * @param bool $isRoot If we are processing the root definitions or not - * - * @return array + * {@inheritdoc} */ - private function inlineArguments(ContainerBuilder $container, array $arguments, $isRoot = false) + protected function processValue($value, $isRoot = false) { - foreach ($arguments as $k => $argument) { - if ($isRoot) { - $this->currentId = $k; - } - if (is_array($argument)) { - $arguments[$k] = $this->inlineArguments($container, $argument); - } elseif ($argument instanceof Reference) { - if (!$container->hasDefinition($id = (string) $argument)) { - continue; - } + if ($value instanceof ArgumentInterface) { + // Reference found in ArgumentInterface::getValues() are not inlineable + return $value; + } + if ($value instanceof Reference && $this->container->hasDefinition($id = (string) $value)) { + $definition = $this->container->getDefinition($id); - if ($this->isInlineableDefinition($container, $id, $definition = $container->getDefinition($id))) { - $this->compiler->addLogMessage($this->formatter->formatInlineService($this, $id, $this->currentId)); + if ($this->isInlineableDefinition($id, $definition, $this->container->getCompiler()->getServiceReferenceGraph())) { + $this->container->log($this, sprintf('Inlined service "%s" to "%s".', $id, $this->currentId)); + $this->inlinedServiceIds[$id][] = $this->currentId; - if ($definition->isShared() && ContainerInterface::SCOPE_PROTOTYPE !== $definition->getScope(false)) { - $arguments[$k] = $definition; - } else { - $arguments[$k] = clone $definition; - } + if ($definition->isShared()) { + return $definition; } - } elseif ($argument instanceof Definition) { - $argument->setArguments($this->inlineArguments($container, $argument->getArguments())); - $argument->setMethodCalls($this->inlineArguments($container, $argument->getMethodCalls())); - $argument->setProperties($this->inlineArguments($container, $argument->getProperties())); - - $configurator = $this->inlineArguments($container, array($argument->getConfigurator())); - $argument->setConfigurator($configurator[0]); - - $factory = $this->inlineArguments($container, array($argument->getFactory())); - $argument->setFactory($factory[0]); + $value = clone $definition; } } - return $arguments; + return parent::processValue($value, $isRoot); } /** * Checks if the definition is inlineable. * - * @param ContainerBuilder $container - * @param string $id - * @param Definition $definition - * * @return bool If the definition is inlineable */ - private function isInlineableDefinition(ContainerBuilder $container, $id, Definition $definition) + private function isInlineableDefinition($id, Definition $definition, ServiceReferenceGraph $graph) { - if (!$definition->isShared() || ContainerInterface::SCOPE_PROTOTYPE === $definition->getScope(false)) { + if (!$definition->isShared()) { return true; } @@ -117,7 +86,7 @@ private function isInlineableDefinition(ContainerBuilder $container, $id, Defini return false; } - if (!$this->graph->hasNode($id)) { + if (!$graph->hasNode($id)) { return true; } @@ -126,7 +95,7 @@ private function isInlineableDefinition(ContainerBuilder $container, $id, Defini } $ids = array(); - foreach ($this->graph->getNode($id)->getInEdges() as $edge) { + foreach ($graph->getNode($id)->getInEdges() as $edge) { $ids[] = $edge->getSourceNode()->getId(); } @@ -138,10 +107,6 @@ private function isInlineableDefinition(ContainerBuilder $container, $id, Defini return false; } - if (count($ids) > 1 && $definition->getFactoryService(false)) { - return false; - } - - return $container->getDefinition(reset($ids))->getScope(false) === $definition->getScope(false); + return true; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php b/src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php deleted file mode 100644 index db208fa0d63c1..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Compiler; - -/** - * Used to format logging messages during the compilation. - * - * @author Johannes M. Schmitt - */ -class LoggingFormatter -{ - public function formatRemoveService(CompilerPassInterface $pass, $id, $reason) - { - return $this->format($pass, sprintf('Removed service "%s"; reason: %s.', $id, $reason)); - } - - public function formatInlineService(CompilerPassInterface $pass, $id, $target) - { - return $this->format($pass, sprintf('Inlined service "%s" to "%s".', $id, $target)); - } - - public function formatUpdateReference(CompilerPassInterface $pass, $serviceId, $oldDestId, $newDestId) - { - return $this->format($pass, sprintf('Changed reference of service "%s" previously pointing to "%s" to "%s".', $serviceId, $oldDestId, $newDestId)); - } - - public function formatResolveInheritance(CompilerPassInterface $pass, $childId, $parentId) - { - return $this->format($pass, sprintf('Resolving inheritance for "%s" (parent: %s).', $childId, $parentId)); - } - - public function format(CompilerPassInterface $pass, $message) - { - return sprintf('%s: %s', get_class($pass), $message); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index cc6e0e71b3652..b2d4595ff4166 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -39,123 +39,144 @@ public function __construct() { $this->mergePass = new MergeExtensionConfigurationPass(); - $this->optimizationPasses = array( + $this->beforeOptimizationPasses = array( + 100 => array( + new ResolveClassPass(), + new ResolveInstanceofConditionalsPass(), + ), + ); + + $this->optimizationPasses = array(array( new ExtensionCompilerPass(), new ResolveDefinitionTemplatesPass(), + new ServiceLocatorTagPass(), new DecoratorServicePass(), - new ResolveParameterPlaceHoldersPass(), + new ResolveParameterPlaceHoldersPass(false), + new ResolveFactoryClassPass(), new CheckDefinitionValidityPass(), + new RegisterServiceSubscribersPass(), + new ResolveNamedArgumentsPass(), + new ResolveBindingsPass(), + $autowirePass = new AutowirePass(false), + new ResolveServiceSubscribersPass(), new ResolveReferencesToAliasesPass(), new ResolveInvalidReferencesPass(), - new AutowirePass(), new AnalyzeServiceReferencesPass(true), new CheckCircularReferencesPass(), new CheckReferenceValidityPass(), - ); + new CheckArgumentsValidityPass(), + )); - $this->removingPasses = array( + $this->removingPasses = array(array( new RemovePrivateAliasesPass(), new ReplaceAliasByActualDefinitionPass(), new RemoveAbstractDefinitionsPass(), new RepeatedPass(array( new AnalyzeServiceReferencesPass(), - new InlineServiceDefinitionsPass(), + $inlinedServicePass = new InlineServiceDefinitionsPass(), new AnalyzeServiceReferencesPass(), new RemoveUnusedDefinitionsPass(), )), + new AutowireExceptionPass($autowirePass, $inlinedServicePass), new CheckExceptionOnInvalidReferenceBehaviorPass(), - ); + )); } /** * Returns all passes in order to be processed. * - * @return array An array of all passes to process + * @return CompilerPassInterface[] */ public function getPasses() { return array_merge( array($this->mergePass), - $this->beforeOptimizationPasses, - $this->optimizationPasses, - $this->beforeRemovingPasses, - $this->removingPasses, - $this->afterRemovingPasses + $this->getBeforeOptimizationPasses(), + $this->getOptimizationPasses(), + $this->getBeforeRemovingPasses(), + $this->getRemovingPasses(), + $this->getAfterRemovingPasses() ); } /** * Adds a pass. * - * @param CompilerPassInterface $pass A Compiler pass - * @param string $type The pass type + * @param CompilerPassInterface $pass A Compiler pass + * @param string $type The pass type + * @param int $priority Used to sort the passes * * @throws InvalidArgumentException when a pass type doesn't exist */ - public function addPass(CompilerPassInterface $pass, $type = self::TYPE_BEFORE_OPTIMIZATION) + public function addPass(CompilerPassInterface $pass, $type = self::TYPE_BEFORE_OPTIMIZATION, int $priority = 0) { $property = $type.'Passes'; if (!isset($this->$property)) { throw new InvalidArgumentException(sprintf('Invalid type "%s".', $type)); } - $this->{$property}[] = $pass; + $passes = &$this->$property; + + if (!isset($passes[$priority])) { + $passes[$priority] = array(); + } + $passes[$priority][] = $pass; } /** * Gets all passes for the AfterRemoving pass. * - * @return array An array of passes + * @return CompilerPassInterface[] */ public function getAfterRemovingPasses() { - return $this->afterRemovingPasses; + return $this->sortPasses($this->afterRemovingPasses); } /** * Gets all passes for the BeforeOptimization pass. * - * @return array An array of passes + * @return CompilerPassInterface[] */ public function getBeforeOptimizationPasses() { - return $this->beforeOptimizationPasses; + return $this->sortPasses($this->beforeOptimizationPasses); } /** * Gets all passes for the BeforeRemoving pass. * - * @return array An array of passes + * @return CompilerPassInterface[] */ public function getBeforeRemovingPasses() { - return $this->beforeRemovingPasses; + return $this->sortPasses($this->beforeRemovingPasses); } /** * Gets all passes for the Optimization pass. * - * @return array An array of passes + * @return CompilerPassInterface[] */ public function getOptimizationPasses() { - return $this->optimizationPasses; + return $this->sortPasses($this->optimizationPasses); } /** * Gets all passes for the Removing pass. * - * @return array An array of passes + * @return CompilerPassInterface[] */ public function getRemovingPasses() { - return $this->removingPasses; + return $this->sortPasses($this->removingPasses); } /** * Gets the Merge pass. * - * @return CompilerPassInterface The merge pass + * @return CompilerPassInterface */ public function getMergePass() { @@ -175,50 +196,69 @@ public function setMergePass(CompilerPassInterface $pass) /** * Sets the AfterRemoving passes. * - * @param array $passes An array of passes + * @param CompilerPassInterface[] $passes */ public function setAfterRemovingPasses(array $passes) { - $this->afterRemovingPasses = $passes; + $this->afterRemovingPasses = array($passes); } /** * Sets the BeforeOptimization passes. * - * @param array $passes An array of passes + * @param CompilerPassInterface[] $passes */ public function setBeforeOptimizationPasses(array $passes) { - $this->beforeOptimizationPasses = $passes; + $this->beforeOptimizationPasses = array($passes); } /** * Sets the BeforeRemoving passes. * - * @param array $passes An array of passes + * @param CompilerPassInterface[] $passes */ public function setBeforeRemovingPasses(array $passes) { - $this->beforeRemovingPasses = $passes; + $this->beforeRemovingPasses = array($passes); } /** * Sets the Optimization passes. * - * @param array $passes An array of passes + * @param CompilerPassInterface[] $passes */ public function setOptimizationPasses(array $passes) { - $this->optimizationPasses = $passes; + $this->optimizationPasses = array($passes); } /** * Sets the Removing passes. * - * @param array $passes An array of passes + * @param CompilerPassInterface[] $passes */ public function setRemovingPasses(array $passes) { - $this->removingPasses = $passes; + $this->removingPasses = array($passes); + } + + /** + * Sort passes by priority. + * + * @param array $passes CompilerPassInterface instances with their priority as key + * + * @return CompilerPassInterface[] + */ + private function sortPasses(array $passes) + { + if (0 === count($passes)) { + return array(); + } + + krsort($passes); + + // Flatten the array + return call_user_func_array('array_merge', $passes); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php new file mode 100644 index 0000000000000..97b083fa13c39 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Trait that allows a generic method to find and sort service by priority option in the tag. + * + * @author Iltar van der Berg + */ +trait PriorityTaggedServiceTrait +{ + /** + * Finds all services with the given tag name and order them by their priority. + * + * The order of additions must be respected for services having the same priority, + * and knowing that the \SplPriorityQueue class does not respect the FIFO method, + * we should not use that class. + * + * @see https://bugs.php.net/bug.php?id=53710 + * @see https://bugs.php.net/bug.php?id=60926 + * + * @param string $tagName + * @param ContainerBuilder $container + * + * @return Reference[] + */ + private function findAndSortTaggedServices($tagName, ContainerBuilder $container) + { + $services = array(); + + foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $services[$priority][] = new Reference($serviceId); + } + + if ($services) { + krsort($services); + $services = call_user_func_array('array_merge', $services); + } + + return $services; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php new file mode 100644 index 0000000000000..f8dba86a0b547 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; +use Symfony\Component\DependencyInjection\TypedReference; + +/** + * Compiler pass to register tagged services that require a service locator. + * + * @author Nicolas Grekas + */ +class RegisterServiceSubscribersPass extends AbstractRecursivePass +{ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition || $value->isAbstract() || $value->isSynthetic() || !$value->hasTag('container.service_subscriber')) { + return parent::processValue($value, $isRoot); + } + + $serviceMap = array(); + $autowire = $value->isAutowired(); + + foreach ($value->getTag('container.service_subscriber') as $attributes) { + if (!$attributes) { + $autowire = true; + continue; + } + ksort($attributes); + if (array() !== array_diff(array_keys($attributes), array('id', 'key'))) { + throw new InvalidArgumentException(sprintf('The "container.service_subscriber" tag accepts only the "key" and "id" attributes, "%s" given for service "%s".', implode('", "', array_keys($attributes)), $this->currentId)); + } + if (!array_key_exists('id', $attributes)) { + throw new InvalidArgumentException(sprintf('Missing "id" attribute on "container.service_subscriber" tag with key="%s" for service "%s".', $attributes['key'], $this->currentId)); + } + if (!array_key_exists('key', $attributes)) { + $attributes['key'] = $attributes['id']; + } + if (isset($serviceMap[$attributes['key']])) { + continue; + } + $serviceMap[$attributes['key']] = new Reference($attributes['id']); + } + $class = $value->getClass(); + + if (!is_subclass_of($class, ServiceSubscriberInterface::class)) { + if (!class_exists($class, false)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $this->currentId)); + } + + throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $this->currentId, ServiceSubscriberInterface::class)); + } + $this->container->addObjectResource($class); + $subscriberMap = array(); + $declaringClass = (new \ReflectionMethod($class, 'getSubscribedServices'))->class; + + foreach ($class::getSubscribedServices() as $key => $type) { + if (!is_string($type) || !preg_match('/^\??[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $type)) { + throw new InvalidArgumentException(sprintf('"%s::getSubscribedServices()" must return valid PHP types for service "%s" key "%s", "%s" returned.', $class, $this->currentId, $key, is_string($type) ? $type : gettype($type))); + } + if ($optionalBehavior = '?' === $type[0]) { + $type = substr($type, 1); + $optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + } + if (is_int($key)) { + $key = $type; + } + if (!isset($serviceMap[$key])) { + if (!$autowire) { + throw new InvalidArgumentException(sprintf('Service "%s" misses a "container.service_subscriber" tag with "key"/"id" attributes corresponding to entry "%s" as returned by "%s::getSubscribedServices()".', $this->currentId, $key, $class)); + } + $serviceMap[$key] = new Reference($type); + } + + $subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $declaringClass, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); + unset($serviceMap[$key]); + } + + if ($serviceMap = array_keys($serviceMap)) { + $message = sprintf(1 < count($serviceMap) ? 'keys "%s" do' : 'key "%s" does', str_replace('%', '%%', implode('", "', $serviceMap))); + throw new InvalidArgumentException(sprintf('Service %s not exist in the map returned by "%s::getSubscribedServices()" for service "%s".', $message, $class, $this->currentId)); + } + + $value->addTag('container.service_subscriber.locator', array('id' => (string) ServiceLocatorTagPass::register($this->container, $subscriberMap))); + + return parent::processValue($value); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RemoveAbstractDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RemoveAbstractDefinitionsPass.php index 0ef0af05b578b..0ce79daa79977 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RemoveAbstractDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RemoveAbstractDefinitionsPass.php @@ -25,13 +25,10 @@ class RemoveAbstractDefinitionsPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { - $compiler = $container->getCompiler(); - $formatter = $compiler->getLoggingFormatter(); - foreach ($container->getDefinitions() as $id => $definition) { if ($definition->isAbstract()) { $container->removeDefinition($id); - $compiler->addLogMessage($formatter->formatRemoveService($this, $id, 'abstract')); + $container->log($this, sprintf('Removed service "%s"; reason: abstract.', $id)); } } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RemovePrivateAliasesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RemovePrivateAliasesPass.php index 5c53a33949a53..5d6d51d6916f3 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RemovePrivateAliasesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RemovePrivateAliasesPass.php @@ -29,16 +29,13 @@ class RemovePrivateAliasesPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { - $compiler = $container->getCompiler(); - $formatter = $compiler->getLoggingFormatter(); - foreach ($container->getAliases() as $id => $alias) { if ($alias->isPublic()) { continue; } $container->removeAlias($id); - $compiler->addLogMessage($formatter->formatRemoveService($this, $id, 'private alias')); + $container->log($this, sprintf('Removed service "%s"; reason: private alias.', $id)); } } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php index 9e18a9ebde062..79a2600d8f785 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php @@ -37,9 +37,7 @@ public function setRepeatedPass(RepeatedPass $repeatedPass) */ public function process(ContainerBuilder $container) { - $compiler = $container->getCompiler(); - $formatter = $compiler->getLoggingFormatter(); - $graph = $compiler->getServiceReferenceGraph(); + $graph = $container->getCompiler()->getServiceReferenceGraph(); $hasChanged = false; foreach ($container->getDefinitions() as $id => $definition) { @@ -69,10 +67,11 @@ public function process(ContainerBuilder $container) $container->setDefinition((string) reset($referencingAliases), $definition); $definition->setPublic(true); $container->removeDefinition($id); - $compiler->addLogMessage($formatter->formatRemoveService($this, $id, 'replaces alias '.reset($referencingAliases))); + $container->log($this, sprintf('Removed service "%s"; reason: replaces alias %s.', $id, reset($referencingAliases))); } elseif (0 === count($referencingAliases) && false === $isReferenced) { $container->removeDefinition($id); - $compiler->addLogMessage($formatter->formatRemoveService($this, $id, 'unused')); + $container->resolveEnvPlaceholders(serialize($definition)); + $container->log($this, sprintf('Removed service "%s"; reason: unused.', $id)); $hasChanged = true; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ReplaceAliasByActualDefinitionPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ReplaceAliasByActualDefinitionPass.php index b7210ee6ee586..30a8f5d048f9a 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ReplaceAliasByActualDefinitionPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ReplaceAliasByActualDefinitionPass.php @@ -21,10 +21,9 @@ * * @author Johannes M. Schmitt */ -class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface +class ReplaceAliasByActualDefinitionPass extends AbstractRecursivePass { - private $compiler; - private $formatter; + private $replacements; /** * Process the Container to replace aliases with service definitions. @@ -35,9 +34,6 @@ class ReplaceAliasByActualDefinitionPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { - // Setup - $this->compiler = $container->getCompiler(); - $this->formatter = $this->compiler->getLoggingFormatter(); // First collect all alias targets that need to be replaced $seenAliasTargets = array(); $replacements = array(); @@ -71,74 +67,24 @@ public function process(ContainerBuilder $container) $container->removeDefinition($targetId); $replacements[$targetId] = $definitionId; } + $this->replacements = $replacements; - // Now replace target instances in all definitions - foreach ($container->getDefinitions() as $definitionId => $definition) { - $definition->setArguments($this->updateArgumentReferences($replacements, $definitionId, $definition->getArguments())); - $definition->setMethodCalls($this->updateArgumentReferences($replacements, $definitionId, $definition->getMethodCalls())); - $definition->setProperties($this->updateArgumentReferences($replacements, $definitionId, $definition->getProperties())); - $definition->setFactoryService($this->updateFactoryReferenceId($replacements, $definition->getFactoryService(false)), false); - $definition->setFactory($this->updateFactoryReference($replacements, $definition->getFactory())); - } + parent::process($container); + $this->replacements = array(); } /** - * Recursively updates references in an array. - * - * @param array $replacements Table of aliases to replace - * @param string $definitionId Identifier of this definition - * @param array $arguments Where to replace the aliases - * - * @return array + * {@inheritdoc} */ - private function updateArgumentReferences(array $replacements, $definitionId, array $arguments) + protected function processValue($value, $isRoot = false) { - foreach ($arguments as $k => $argument) { - // Handle recursion step - if (is_array($argument)) { - $arguments[$k] = $this->updateArgumentReferences($replacements, $definitionId, $argument); - continue; - } - // Skip arguments that don't need replacement - if (!$argument instanceof Reference) { - continue; - } - $referenceId = (string) $argument; - if (!isset($replacements[$referenceId])) { - continue; - } + if ($value instanceof Reference && isset($this->replacements[$referenceId = (string) $value])) { // Perform the replacement - $newId = $replacements[$referenceId]; - $arguments[$k] = new Reference($newId, $argument->getInvalidBehavior()); - $this->compiler->addLogMessage($this->formatter->formatUpdateReference($this, $definitionId, $referenceId, $newId)); - } - - return $arguments; - } - - /** - * Returns the updated reference for the factory service. - * - * @param array $replacements Table of aliases to replace - * @param string|null $referenceId Factory service reference identifier - * - * @return string|null - */ - private function updateFactoryReferenceId(array $replacements, $referenceId) - { - if (null === $referenceId) { - return; - } - - return isset($replacements[$referenceId]) ? $replacements[$referenceId] : $referenceId; - } - - private function updateFactoryReference(array $replacements, $factory) - { - if (is_array($factory) && $factory[0] instanceof Reference && isset($replacements[$referenceId = (string) $factory[0]])) { - $factory[0] = new Reference($replacements[$referenceId], $factory[0]->getInvalidBehavior()); + $newId = $this->replacements[$referenceId]; + $value = new Reference($newId, $value->getInvalidBehavior()); + $this->container->log($this, sprintf('Changed reference of service "%s" previously pointing to "%s" to "%s".', $this->currentId, $referenceId, $newId)); } - return $factory; + return parent::processValue($value, $isRoot); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php new file mode 100644 index 0000000000000..73ca29d35f424 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; +use Symfony\Component\DependencyInjection\TypedReference; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Guilhem Niot + */ +class ResolveBindingsPass extends AbstractRecursivePass +{ + private $usedBindings = array(); + private $unusedBindings = array(); + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + try { + parent::process($container); + + foreach ($this->unusedBindings as list($key, $serviceId)) { + throw new InvalidArgumentException(sprintf('Unused binding "%s" in service "%s".', $key, $serviceId)); + } + } finally { + $this->usedBindings = array(); + $this->unusedBindings = array(); + } + } + + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if ($value instanceof TypedReference && $value->getType() === (string) $value) { + // Already checked + $bindings = $this->container->getDefinition($this->currentId)->getBindings(); + + if (isset($bindings[$value->getType()])) { + return $this->getBindingValue($bindings[$value->getType()]); + } + + return parent::processValue($value, $isRoot); + } + + if (!$value instanceof Definition || !$bindings = $value->getBindings()) { + return parent::processValue($value, $isRoot); + } + + foreach ($bindings as $key => $binding) { + list($bindingValue, $bindingId, $used) = $binding->getValues(); + if ($used) { + $this->usedBindings[$bindingId] = true; + unset($this->unusedBindings[$bindingId]); + } elseif (!isset($this->usedBindings[$bindingId])) { + $this->unusedBindings[$bindingId] = array($key, $this->currentId); + } + + if (isset($key[0]) && '$' === $key[0]) { + continue; + } + + if (null !== $bindingValue && !$bindingValue instanceof Reference && !$bindingValue instanceof Definition) { + throw new InvalidArgumentException(sprintf('Invalid value for binding key "%s" for service "%s": expected null, an instance of %s or an instance of %s, %s given.', $key, $this->currentId, Reference::class, Definition::class, gettype($bindingValue))); + } + } + + if ($value->isAbstract()) { + return parent::processValue($value, $isRoot); + } + + $calls = $value->getMethodCalls(); + + if ($constructor = $this->getConstructor($value, false)) { + $calls[] = array($constructor, $value->getArguments()); + } + + foreach ($calls as $i => $call) { + list($method, $arguments) = $call; + + if ($method instanceof \ReflectionFunctionAbstract) { + $reflectionMethod = $method; + } else { + $reflectionMethod = $this->getReflectionMethod($value, $method); + } + + foreach ($reflectionMethod->getParameters() as $key => $parameter) { + if (array_key_exists($key, $arguments) && '' !== $arguments[$key]) { + continue; + } + + if (array_key_exists('$'.$parameter->name, $bindings)) { + $arguments[$key] = $this->getBindingValue($bindings['$'.$parameter->name]); + + continue; + } + + $typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true); + + if (!isset($bindings[$typeHint])) { + continue; + } + + $arguments[$key] = $this->getBindingValue($bindings[$typeHint]); + } + + if ($arguments !== $call[1]) { + ksort($arguments); + $calls[$i][1] = $arguments; + } + } + + if ($constructor) { + list(, $arguments) = array_pop($calls); + + if ($arguments !== $value->getArguments()) { + $value->setArguments($arguments); + } + } + + if ($calls !== $value->getMethodCalls()) { + $value->setMethodCalls($calls); + } + + return parent::processValue($value, $isRoot); + } + + private function getBindingValue(BoundArgument $binding) + { + list($bindingValue, $bindingId) = $binding->getValues(); + + $this->usedBindings[$bindingId] = true; + unset($this->unusedBindings[$bindingId]); + + return $bindingValue; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveClassPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveClassPass.php new file mode 100644 index 0000000000000..0235e5abb8e3b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveClassPass.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + */ +class ResolveClassPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + foreach ($container->getDefinitions() as $id => $definition) { + if ($definition->isSynthetic() || null !== $definition->getClass()) { + continue; + } + if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)++$/', $id)) { + if ($definition instanceof ChildDefinition && !class_exists($id)) { + throw new InvalidArgumentException(sprintf('Service definition "%s" has a parent but no class, and its name looks like a FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class.', $id)); + } + $definition->setClass($id); + } + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php index 4f8cd2b70e84c..dcede86a189b2 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php @@ -11,123 +11,84 @@ namespace Symfony\Component\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\DefinitionDecorator; -use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\ExceptionInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; /** - * This replaces all DefinitionDecorator instances with their equivalent fully + * This replaces all ChildDefinition instances with their equivalent fully * merged Definition instance. * * @author Johannes M. Schmitt * @author Nicolas Grekas */ -class ResolveDefinitionTemplatesPass implements CompilerPassInterface +class ResolveDefinitionTemplatesPass extends AbstractRecursivePass { - private $compiler; - private $formatter; - private $currentId; - - /** - * Process the ContainerBuilder to replace DefinitionDecorator instances with their real Definition instances. - * - * @param ContainerBuilder $container - */ - public function process(ContainerBuilder $container) - { - $this->compiler = $container->getCompiler(); - $this->formatter = $this->compiler->getLoggingFormatter(); - - $container->setDefinitions($this->resolveArguments($container, $container->getDefinitions(), true)); - } - - /** - * Resolves definition decorator arguments. - * - * @param ContainerBuilder $container The ContainerBuilder - * @param array $arguments An array of arguments - * @param bool $isRoot If we are processing the root definitions or not - * - * @return array - */ - private function resolveArguments(ContainerBuilder $container, array $arguments, $isRoot = false) + protected function processValue($value, $isRoot = false) { - foreach ($arguments as $k => $argument) { + if (!$value instanceof Definition) { + return parent::processValue($value, $isRoot); + } + if ($isRoot) { + // yes, we are specifically fetching the definition from the + // container to ensure we are not operating on stale data + $value = $this->container->getDefinition($this->currentId); + } + if ($value instanceof ChildDefinition) { + $value = $this->resolveDefinition($value); if ($isRoot) { - // yes, we are specifically fetching the definition from the - // container to ensure we are not operating on stale data - $arguments[$k] = $argument = $container->getDefinition($k); - $this->currentId = $k; - } - if (is_array($argument)) { - $arguments[$k] = $this->resolveArguments($container, $argument); - } elseif ($argument instanceof Definition) { - if ($argument instanceof DefinitionDecorator) { - $arguments[$k] = $argument = $this->resolveDefinition($container, $argument); - if ($isRoot) { - $container->setDefinition($k, $argument); - } - } - $argument->setArguments($this->resolveArguments($container, $argument->getArguments())); - $argument->setMethodCalls($this->resolveArguments($container, $argument->getMethodCalls())); - $argument->setProperties($this->resolveArguments($container, $argument->getProperties())); - - $configurator = $this->resolveArguments($container, array($argument->getConfigurator())); - $argument->setConfigurator($configurator[0]); - - $factory = $this->resolveArguments($container, array($argument->getFactory())); - $argument->setFactory($factory[0]); + $this->container->setDefinition($this->currentId, $value); } } - return $arguments; + return parent::processValue($value, $isRoot); } /** * Resolves the definition. * - * @param ContainerBuilder $container The ContainerBuilder - * @param DefinitionDecorator $definition - * * @return Definition * - * @throws \RuntimeException When the definition is invalid + * @throws RuntimeException When the definition is invalid */ - private function resolveDefinition(ContainerBuilder $container, DefinitionDecorator $definition) + private function resolveDefinition(ChildDefinition $definition) { - if (!$container->hasDefinition($parent = $definition->getParent())) { - throw new RuntimeException(sprintf('The parent definition "%s" defined for definition "%s" does not exist.', $parent, $this->currentId)); + try { + return $this->doResolveDefinition($definition); + } catch (ExceptionInterface $e) { + $r = new \ReflectionProperty($e, 'message'); + $r->setAccessible(true); + $r->setValue($e, sprintf('Service "%s": %s', $this->currentId, $e->getMessage())); + + throw $e; } + } - $parentDef = $container->getDefinition($parent); - if ($parentDef instanceof DefinitionDecorator) { + private function doResolveDefinition(ChildDefinition $definition) + { + if (!$this->container->has($parent = $definition->getParent())) { + throw new RuntimeException(sprintf('Parent definition "%s" does not exist.', $parent)); + } + + $parentDef = $this->container->findDefinition($parent); + if ($parentDef instanceof ChildDefinition) { $id = $this->currentId; $this->currentId = $parent; - $parentDef = $this->resolveDefinition($container, $parentDef); - $container->setDefinition($parent, $parentDef); + $parentDef = $this->resolveDefinition($parentDef); + $this->container->setDefinition($parent, $parentDef); $this->currentId = $id; } - $this->compiler->addLogMessage($this->formatter->formatResolveInheritance($this, $this->currentId, $parent)); + $this->container->log($this, sprintf('Resolving inheritance for "%s" (parent: %s).', $this->currentId, $parent)); $def = new Definition(); // merge in parent definition - // purposely ignored attributes: scope, abstract, tags + // purposely ignored attributes: abstract, shared, tags, autoconfigured $def->setClass($parentDef->getClass()); $def->setArguments($parentDef->getArguments()); $def->setMethodCalls($parentDef->getMethodCalls()); $def->setProperties($parentDef->getProperties()); - $def->setAutowiringTypes($parentDef->getAutowiringTypes()); - if ($parentDef->getFactoryClass(false)) { - $def->setFactoryClass($parentDef->getFactoryClass(false)); - } - if ($parentDef->getFactoryMethod(false)) { - $def->setFactoryMethod($parentDef->getFactoryMethod(false)); - } - if ($parentDef->getFactoryService(false)) { - $def->setFactoryService($parentDef->getFactoryService(false)); - } if ($parentDef->isDeprecated()) { $def->setDeprecated(true, $parentDef->getDeprecationMessage('%service_id%')); } @@ -137,21 +98,15 @@ private function resolveDefinition(ContainerBuilder $container, DefinitionDecora $def->setPublic($parentDef->isPublic()); $def->setLazy($parentDef->isLazy()); $def->setAutowired($parentDef->isAutowired()); + $def->setChanges($parentDef->getChanges()); + + $def->setBindings($parentDef->getBindings()); // overwrite with values specified in the decorator $changes = $definition->getChanges(); if (isset($changes['class'])) { $def->setClass($definition->getClass()); } - if (isset($changes['factory_class'])) { - $def->setFactoryClass($definition->getFactoryClass(false)); - } - if (isset($changes['factory_method'])) { - $def->setFactoryMethod($definition->getFactoryMethod(false)); - } - if (isset($changes['factory_service'])) { - $def->setFactoryService($definition->getFactoryService(false)); - } if (isset($changes['factory'])) { $def->setFactory($definition->getFactory()); } @@ -170,9 +125,12 @@ private function resolveDefinition(ContainerBuilder $container, DefinitionDecora if (isset($changes['deprecated'])) { $def->setDeprecated($definition->isDeprecated(), $definition->getDeprecationMessage('%service_id%')); } - if (isset($changes['autowire'])) { + if (isset($changes['autowired'])) { $def->setAutowired($definition->isAutowired()); } + if (isset($changes['shared'])) { + $def->setShared($definition->isShared()); + } if (isset($changes['decorated_service'])) { $decoratedService = $definition->getDecoratedService(); if (null === $decoratedService) { @@ -186,15 +144,11 @@ private function resolveDefinition(ContainerBuilder $container, DefinitionDecora foreach ($definition->getArguments() as $k => $v) { if (is_numeric($k)) { $def->addArgument($v); - continue; - } - - if (0 !== strpos($k, 'index_')) { - throw new RuntimeException(sprintf('Invalid argument key "%s" found.', $k)); + } elseif (0 === strpos($k, 'index_')) { + $def->replaceArgument((int) substr($k, strlen('index_')), $v); + } else { + $def->setArgument($k, $v); } - - $index = (int) substr($k, strlen('index_')); - $def->replaceArgument($index, $v); } // merge properties @@ -203,20 +157,16 @@ private function resolveDefinition(ContainerBuilder $container, DefinitionDecora } // append method calls - if (count($calls = $definition->getMethodCalls()) > 0) { + if ($calls = $definition->getMethodCalls()) { $def->setMethodCalls(array_merge($def->getMethodCalls(), $calls)); } - // merge autowiring types - foreach ($definition->getAutowiringTypes() as $autowiringType) { - $def->addAutowiringType($autowiringType); - } - // these attributes are always taken from the child $def->setAbstract($definition->isAbstract()); - $def->setScope($definition->getScope(false), false); - $def->setShared($definition->isShared()); $def->setTags($definition->getTags()); + // autoconfigure is never taken from parent (on purpose) + // and it's not legal on an instanceof + $def->setAutoconfigured($definition->isAutoconfigured()); return $def; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveFactoryClassPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveFactoryClassPass.php new file mode 100644 index 0000000000000..c41cf973fe620 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveFactoryClassPass.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; + +/** + * @author Maxime Steinhausser + */ +class ResolveFactoryClassPass extends AbstractRecursivePass +{ + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if ($value instanceof Definition && is_array($factory = $value->getFactory()) && null === $factory[0]) { + if (null === $class = $value->getClass()) { + throw new RuntimeException(sprintf('The "%s" service is defined to be created by a factory, but is missing the factory class. Did you forget to define the factory or service class?', $this->currentId)); + } + + $factory[0] = $class; + $value->setFactory($factory); + } + + return parent::processValue($value, $isRoot); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.php new file mode 100644 index 0000000000000..32d7ad2911408 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInstanceofConditionalsPass.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\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + +/** + * Applies instanceof conditionals to definitions. + * + * @author Nicolas Grekas + */ +class ResolveInstanceofConditionalsPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + foreach ($container->getAutoconfiguredInstanceof() as $interface => $definition) { + if ($definition->getArguments()) { + throw new InvalidArgumentException(sprintf('Autoconfigured instanceof for type "%s" defines arguments but these are not supported and should be removed.', $interface)); + } + if ($definition->getMethodCalls()) { + throw new InvalidArgumentException(sprintf('Autoconfigured instanceof for type "%s" defines method calls but these are not supported and should be removed.', $interface)); + } + } + + foreach ($container->getDefinitions() as $id => $definition) { + if ($definition instanceof ChildDefinition) { + // don't apply "instanceof" to children: it will be applied to their parent + continue; + } + $container->setDefinition($id, $this->processDefinition($container, $id, $definition)); + } + } + + private function processDefinition(ContainerBuilder $container, $id, Definition $definition) + { + $instanceofConditionals = $definition->getInstanceofConditionals(); + $autoconfiguredInstanceof = $definition->isAutoconfigured() ? $container->getAutoconfiguredInstanceof() : array(); + if (!$instanceofConditionals && !$autoconfiguredInstanceof) { + return $definition; + } + + if (!$class = $container->getParameterBag()->resolveValue($definition->getClass())) { + return $definition; + } + + $conditionals = $this->mergeConditionals($autoconfiguredInstanceof, $instanceofConditionals, $container); + + $definition->setInstanceofConditionals(array()); + $parent = $shared = null; + $instanceofTags = array(); + + foreach ($conditionals as $interface => $instanceofDefs) { + if ($interface !== $class && (!$container->getReflectionClass($class, false))) { + continue; + } + + if ($interface !== $class && !is_subclass_of($class, $interface)) { + continue; + } + + foreach ($instanceofDefs as $key => $instanceofDef) { + /** @var ChildDefinition $instanceofDef */ + $instanceofDef = clone $instanceofDef; + $instanceofDef->setAbstract(true)->setParent($parent ?: 'abstract.instanceof.'.$id); + $parent = 'instanceof.'.$interface.'.'.$key.'.'.$id; + $container->setDefinition($parent, $instanceofDef); + $instanceofTags[] = $instanceofDef->getTags(); + $instanceofDef->setTags(array()); + + if (isset($instanceofDef->getChanges()['shared'])) { + $shared = $instanceofDef->isShared(); + } + } + } + + if ($parent) { + $abstract = $container->setDefinition('abstract.instanceof.'.$id, $definition); + + // cast Definition to ChildDefinition + $definition = serialize($definition); + $definition = substr_replace($definition, '53', 2, 2); + $definition = substr_replace($definition, 'Child', 44, 0); + $definition = unserialize($definition); + $definition->setParent($parent); + + if (null !== $shared && !isset($definition->getChanges()['shared'])) { + $definition->setShared($shared); + } + + $i = count($instanceofTags); + while (0 <= --$i) { + foreach ($instanceofTags[$i] as $k => $v) { + foreach ($v as $v) { + if ($definition->hasTag($k) && in_array($v, $definition->getTag($k))) { + continue; + } + $definition->addTag($k, $v); + } + } + } + + // reset fields with "merge" behavior + $abstract + ->setArguments(array()) + ->setMethodCalls(array()) + ->setTags(array()) + ->setAbstract(true); + } + + return $definition; + } + + private function mergeConditionals(array $autoconfiguredInstanceof, array $instanceofConditionals, ContainerBuilder $container) + { + // make each value an array of ChildDefinition + $conditionals = array_map(function ($childDef) { return array($childDef); }, $autoconfiguredInstanceof); + + foreach ($instanceofConditionals as $interface => $instanceofDef) { + // make sure the interface/class exists (but don't validate automaticInstanceofConditionals) + if (!$container->getReflectionClass($interface)) { + throw new RuntimeException(sprintf('"%s" is set as an "instanceof" conditional, but it does not exist.', $interface)); + } + + if (!isset($autoconfiguredInstanceof[$interface])) { + $conditionals[$interface] = array(); + } + + $conditionals[$interface][] = $instanceofDef; + } + + return $conditionals; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php index 85dbceb9a61ec..690dce7efe8ae 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php @@ -11,7 +11,10 @@ namespace Symfony\Component\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -25,6 +28,7 @@ class ResolveInvalidReferencesPass implements CompilerPassInterface { private $container; + private $signalingException; /** * Process the ContainerBuilder to resolve invalid references. @@ -34,72 +38,78 @@ class ResolveInvalidReferencesPass implements CompilerPassInterface public function process(ContainerBuilder $container) { $this->container = $container; - foreach ($container->getDefinitions() as $definition) { - if ($definition->isSynthetic() || $definition->isAbstract()) { - continue; - } - - $definition->setArguments( - $this->processArguments($definition->getArguments()) - ); - - $calls = array(); - foreach ($definition->getMethodCalls() as $call) { - try { - $calls[] = array($call[0], $this->processArguments($call[1], true)); - } catch (RuntimeException $e) { - // this call is simply removed - } - } - $definition->setMethodCalls($calls); + $this->signalingException = new RuntimeException('Invalid reference.'); - $properties = array(); - foreach ($definition->getProperties() as $name => $value) { - try { - $value = $this->processArguments(array($value), true); - $properties[$name] = reset($value); - } catch (RuntimeException $e) { - // ignore property - } - } - $definition->setProperties($properties); + try { + $this->processValue($container->getDefinitions(), 1); + } finally { + $this->container = $this->signalingException = null; } } /** * Processes arguments to determine invalid references. * - * @param array $arguments An array of Reference objects - * @param bool $inMethodCall - * - * @return array - * - * @throws RuntimeException When the config is invalid + * @throws RuntimeException When an invalid reference is found */ - private function processArguments(array $arguments, $inMethodCall = false) + private function processValue($value, $rootLevel = 0, $level = 0) { - foreach ($arguments as $k => $argument) { - if (is_array($argument)) { - $arguments[$k] = $this->processArguments($argument, $inMethodCall); - } elseif ($argument instanceof Reference) { - $id = (string) $argument; - - $invalidBehavior = $argument->getInvalidBehavior(); - $exists = $this->container->has($id); + if ($value instanceof ServiceClosureArgument) { + $value->setValues($this->processValue($value->getValues(), 1, 1)); + } elseif ($value instanceof ArgumentInterface) { + $value->setValues($this->processValue($value->getValues(), $rootLevel, 1 + $level)); + } elseif ($value instanceof Definition) { + if ($value->isSynthetic() || $value->isAbstract()) { + return $value; + } + $value->setArguments($this->processValue($value->getArguments(), 0)); + $value->setProperties($this->processValue($value->getProperties(), 1)); + $value->setMethodCalls($this->processValue($value->getMethodCalls(), 2)); + } elseif (is_array($value)) { + $i = 0; - // resolve invalid behavior - if (!$exists && ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) { - $arguments[$k] = null; - } elseif (!$exists && ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) { - if ($inMethodCall) { - throw new RuntimeException('Method shouldn\'t be called.'); + foreach ($value as $k => $v) { + try { + if (false !== $i && $k !== $i++) { + $i = false; + } + if ($v !== $processedValue = $this->processValue($v, $rootLevel, 1 + $level)) { + $value[$k] = $processedValue; + } + } catch (RuntimeException $e) { + if ($rootLevel < $level || ($rootLevel && !$level)) { + unset($value[$k]); + } elseif ($rootLevel) { + throw $e; + } else { + $value[$k] = null; } + } + } + + // Ensure numerically indexed arguments have sequential numeric keys. + if (false !== $i) { + $value = array_values($value); + } + } elseif ($value instanceof Reference) { + $id = (string) $value; + + if ($this->container->has($id)) { + return $value; + } + $invalidBehavior = $value->getInvalidBehavior(); - $arguments[$k] = null; + // resolve invalid behavior + if (ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) { + $value = null; + } elseif (ContainerInterface::IGNORE_ON_INVALID_REFERENCE === $invalidBehavior) { + if (0 < $level || $rootLevel) { + throw $this->signalingException; } + $value = null; } } - return $arguments; + return $value; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php new file mode 100644 index 0000000000000..e4d592d0c6473 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Resolves named arguments to their corresponding numeric index. + * + * @author Kévin Dunglas + */ +class ResolveNamedArgumentsPass extends AbstractRecursivePass +{ + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition) { + return parent::processValue($value, $isRoot); + } + + $calls = $value->getMethodCalls(); + $calls[] = array('__construct', $value->getArguments()); + + foreach ($calls as $i => $call) { + list($method, $arguments) = $call; + $parameters = null; + $resolvedArguments = array(); + + foreach ($arguments as $key => $argument) { + if (is_int($key)) { + $resolvedArguments[$key] = $argument; + continue; + } + + if (null === $parameters) { + $r = $this->getReflectionMethod($value, $method); + $class = $r instanceof \ReflectionMethod ? $r->class : $this->currentId; + $parameters = $r->getParameters(); + } + + if (isset($key[0]) && '$' === $key[0]) { + foreach ($parameters as $j => $p) { + if ($key === '$'.$p->name) { + $resolvedArguments[$j] = $argument; + + continue 2; + } + } + + throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument named "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); + } + + if (null !== $argument && !$argument instanceof Reference && !$argument instanceof Definition) { + throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the value of argument "%s" of method "%s()" must be null, an instance of %s or an instance of %s, %s given.', $this->currentId, $key, $class !== $this->currentId ? $class.'::'.$method : $method, Reference::class, Definition::class, gettype($argument))); + } + + foreach ($parameters as $j => $p) { + if (ProxyHelper::getTypeHint($r, $p, true) === $key) { + $resolvedArguments[$j] = $argument; + + continue 2; + } + } + + throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument type-hinted as "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key)); + } + + if ($resolvedArguments !== $call[1]) { + ksort($resolvedArguments); + $calls[$i][1] = $resolvedArguments; + } + } + + list(, $arguments) = array_pop($calls); + + if ($arguments !== $value->getArguments()) { + $value->setArguments($arguments); + } + if ($calls !== $value->getMethodCalls()) { + $value->setMethodCalls($calls); + } + + return parent::processValue($value, $isRoot); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php index a35f84cbe4a80..75e629ab6f083 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; /** @@ -19,55 +20,67 @@ * * @author Johannes M. Schmitt */ -class ResolveParameterPlaceHoldersPass implements CompilerPassInterface +class ResolveParameterPlaceHoldersPass extends AbstractRecursivePass { + private $bag; + private $resolveArrays; + + public function __construct($resolveArrays = true) + { + $this->resolveArrays = $resolveArrays; + } + /** - * Processes the ContainerBuilder to resolve parameter placeholders. - * - * @param ContainerBuilder $container + * {@inheritdoc} * * @throws ParameterNotFoundException */ public function process(ContainerBuilder $container) { - $parameterBag = $container->getParameterBag(); + $this->bag = $container->getParameterBag(); - foreach ($container->getDefinitions() as $id => $definition) { - try { - $definition->setClass($parameterBag->resolveValue($definition->getClass())); - $definition->setFile($parameterBag->resolveValue($definition->getFile())); - $definition->setArguments($parameterBag->resolveValue($definition->getArguments())); - if ($definition->getFactoryClass(false)) { - $definition->setFactoryClass($parameterBag->resolveValue($definition->getFactoryClass(false))); - } + try { + parent::process($container); - $factory = $definition->getFactory(); + $aliases = array(); + foreach ($container->getAliases() as $name => $target) { + $this->currentId = $name; + $aliases[$this->bag->resolveValue($name)] = $this->bag->resolveValue($target); + } + $container->setAliases($aliases); + } catch (ParameterNotFoundException $e) { + $e->setSourceId($this->currentId); - if (is_array($factory) && isset($factory[0])) { - $factory[0] = $parameterBag->resolveValue($factory[0]); - $definition->setFactory($factory); - } + throw $e; + } - $calls = array(); - foreach ($definition->getMethodCalls() as $name => $arguments) { - $calls[$parameterBag->resolveValue($name)] = $parameterBag->resolveValue($arguments); - } - $definition->setMethodCalls($calls); + $this->bag->resolve(); + $this->bag = null; + } - $definition->setProperties($parameterBag->resolveValue($definition->getProperties())); - } catch (ParameterNotFoundException $e) { - $e->setSourceId($id); + protected function processValue($value, $isRoot = false) + { + if (is_string($value)) { + $v = $this->bag->resolveValue($value); - throw $e; + return $this->resolveArrays || !$v || !is_array($v) ? $v : $value; + } + if ($value instanceof Definition) { + $changes = $value->getChanges(); + if (isset($changes['class'])) { + $value->setClass($this->bag->resolveValue($value->getClass())); + } + if (isset($changes['file'])) { + $value->setFile($this->bag->resolveValue($value->getFile())); } } - $aliases = array(); - foreach ($container->getAliases() as $name => $target) { - $aliases[$parameterBag->resolveValue($name)] = $parameterBag->resolveValue($target); + $value = parent::processValue($value, $isRoot); + + if ($value && is_array($value)) { + $value = array_combine($this->bag->resolveValue(array_keys($value)), $value); } - $container->setAliases($aliases); - $parameterBag->resolve(); + return $value; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php index c200cb4d9138a..6e79faba43f04 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -42,8 +43,9 @@ public function process(ContainerBuilder $container) $definition->setArguments($this->processArguments($definition->getArguments())); $definition->setMethodCalls($this->processArguments($definition->getMethodCalls())); $definition->setProperties($this->processArguments($definition->getProperties())); - $definition->setFactory($this->processFactory($definition->getFactory())); - $definition->setFactoryService($this->processFactoryService($definition->getFactoryService(false)), false); + if (isset($definition->getChanges()['factory'])) { + $definition->setFactory($this->processFactory($definition->getFactory())); + } } foreach ($container->getAliases() as $id => $alias) { @@ -66,11 +68,13 @@ private function processArguments(array $arguments) foreach ($arguments as $k => $argument) { if (is_array($argument)) { $arguments[$k] = $this->processArguments($argument); + } elseif ($argument instanceof ArgumentInterface) { + $argument->setValues($this->processArguments($argument->getValues())); } elseif ($argument instanceof Reference) { $defId = $this->getDefinitionId($id = (string) $argument); if ($defId !== $id) { - $arguments[$k] = new Reference($defId, $argument->getInvalidBehavior(), $argument->isStrict(false)); + $arguments[$k] = new Reference($defId, $argument->getInvalidBehavior()); } } } @@ -78,15 +82,6 @@ private function processArguments(array $arguments) return $arguments; } - private function processFactoryService($factoryService) - { - if (null === $factoryService) { - return; - } - - return $this->getDefinitionId($factoryService); - } - private function processFactory($factory) { if (null === $factory || !is_array($factory) || !$factory[0] instanceof Reference) { @@ -96,7 +91,7 @@ private function processFactory($factory) $defId = $this->getDefinitionId($id = (string) $factory[0]); if ($defId !== $id) { - $factory[0] = new Reference($defId, $factory[0]->getInvalidBehavior(), $factory[0]->isStrict(false)); + $factory[0] = new Reference($defId, $factory[0]->getInvalidBehavior()); } return $factory; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveServiceSubscribersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveServiceSubscribersPass.php new file mode 100644 index 0000000000000..9245f21f74cb8 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveServiceSubscribersPass.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Compiler pass to inject their service locator to service subscribers. + * + * @author Nicolas Grekas + */ +class ResolveServiceSubscribersPass extends AbstractRecursivePass +{ + private $serviceLocator; + + protected function processValue($value, $isRoot = false) + { + if ($value instanceof Reference && $this->serviceLocator && ContainerInterface::class === (string) $value) { + return new Reference($this->serviceLocator); + } + + if (!$value instanceof Definition) { + return parent::processValue($value, $isRoot); + } + + $serviceLocator = $this->serviceLocator; + $this->serviceLocator = $value->hasTag('container.service_subscriber.locator') ? $value->getTag('container.service_subscriber.locator')[0]['id'] : null; + + try { + return parent::processValue($value); + } finally { + $this->serviceLocator = $serviceLocator; + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php new file mode 100644 index 0000000000000..d9cd241d76e53 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * Applies the "container.service_locator" tag by wrapping references into ServiceClosureArgument instances. + * + * @author Nicolas Grekas + */ +final class ServiceLocatorTagPass extends AbstractRecursivePass +{ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition || !$value->hasTag('container.service_locator')) { + return parent::processValue($value, $isRoot); + } + + if (!$value->getClass()) { + $value->setClass(ServiceLocator::class); + } + + $arguments = $value->getArguments(); + if (!isset($arguments[0]) || !is_array($arguments[0])) { + throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId)); + } + + foreach ($arguments[0] as $k => $v) { + if ($v instanceof ServiceClosureArgument) { + continue; + } + if (!$v instanceof Reference) { + throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set, "%s" found for key "%s".', $this->currentId, is_object($v) ? get_class($v) : gettype($v), $k)); + } + $arguments[0][$k] = new ServiceClosureArgument($v); + } + ksort($arguments[0]); + + $value->setArguments($arguments); + + $id = 'service_locator.'.ContainerBuilder::hash($value); + + if ($isRoot) { + if ($id !== $this->currentId) { + $this->container->setAlias($id, new Alias($this->currentId, false)); + } + + return $value; + } + + $this->container->setDefinition($id, $value->setPublic(false)); + + return new Reference($id); + } + + /** + * @param ContainerBuilder $container + * @param Reference[] $refMap + * + * @return Reference + */ + public static function register(ContainerBuilder $container, array $refMap) + { + foreach ($refMap as $id => $ref) { + $refMap[$id] = new ServiceClosureArgument($ref); + } + ksort($refMap); + + $locator = (new Definition(ServiceLocator::class)) + ->addArgument($refMap) + ->setPublic(false) + ->addTag('container.service_locator'); + + if (!$container->has($id = 'service_locator.'.ContainerBuilder::hash($locator))) { + $container->setDefinition($id, $locator); + } + + return new Reference($id); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php index e7306ab560e22..f754067a0580c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php @@ -84,12 +84,13 @@ public function clear() * @param string $destId * @param string $destValue * @param string $reference + * @param bool $lazy */ - public function connect($sourceId, $sourceValue, $destId, $destValue = null, $reference = null) + public function connect($sourceId, $sourceValue, $destId, $destValue = null, $reference = null, bool $lazy = false) { $sourceNode = $this->createNode($sourceId, $sourceValue); $destNode = $this->createNode($destId, $destValue); - $edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference); + $edge = new ServiceReferenceGraphEdge($sourceNode, $destNode, $reference, $lazy); $sourceNode->addOutEdge($edge); $destNode->addInEdge($edge); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php index e3c793c4f4eaf..17dd5d9559f9d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php @@ -23,17 +23,20 @@ class ServiceReferenceGraphEdge private $sourceNode; private $destNode; private $value; + private $lazy; /** * @param ServiceReferenceGraphNode $sourceNode * @param ServiceReferenceGraphNode $destNode * @param string $value + * @param bool $lazy */ - public function __construct(ServiceReferenceGraphNode $sourceNode, ServiceReferenceGraphNode $destNode, $value = null) + public function __construct(ServiceReferenceGraphNode $sourceNode, ServiceReferenceGraphNode $destNode, $value = null, $lazy = false) { $this->sourceNode = $sourceNode; $this->destNode = $destNode; $this->value = $value; + $this->lazy = $lazy; } /** @@ -65,4 +68,14 @@ public function getDestNode() { return $this->destNode; } + + /** + * Returns true if the edge is lazy, meaning it's a dependency not requiring direct instantiation. + * + * @return bool + */ + public function isLazy() + { + return $this->lazy; + } } diff --git a/src/Symfony/Component/DependencyInjection/Config/ContainerParametersResource.php b/src/Symfony/Component/DependencyInjection/Config/ContainerParametersResource.php new file mode 100644 index 0000000000000..072f0580aada7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Config/ContainerParametersResource.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Config; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * Tracks container parameters. + * + * @author Maxime Steinhausser + */ +class ContainerParametersResource implements ResourceInterface, \Serializable +{ + private $parameters; + + /** + * @param array $parameters The container parameters to track + */ + public function __construct(array $parameters) + { + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return 'container_parameters_'.md5(serialize($this->parameters)); + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize($this->parameters); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $this->parameters = unserialize($serialized); + } + + /** + * @return array Tracked parameters + */ + public function getParameters() + { + return $this->parameters; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Config/ContainerParametersResourceChecker.php b/src/Symfony/Component/DependencyInjection/Config/ContainerParametersResourceChecker.php new file mode 100644 index 0000000000000..6ed77e3fd2c48 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Config/ContainerParametersResourceChecker.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Config; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\ResourceCheckerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @author Maxime Steinhausser + */ +class ContainerParametersResourceChecker implements ResourceCheckerInterface +{ + /** @var ContainerInterface */ + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function supports(ResourceInterface $metadata) + { + return $metadata instanceof ContainerParametersResource; + } + + /** + * {@inheritdoc} + */ + public function isFresh(ResourceInterface $resource, $timestamp) + { + foreach ($resource->getParameters() as $key => $value) { + if (!$this->container->hasParameter($key) || $this->container->getParameter($key) !== $value) { + return false; + } + } + + return true; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index 19459f77b95ad..c99c479b8fc04 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -11,14 +11,12 @@ namespace Symfony\Component\DependencyInjection; -use Symfony\Component\DependencyInjection\Exception\InactiveScopeException; +use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\Exception\LogicException; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; /** @@ -28,16 +26,7 @@ * * Services and parameters are simple key/pair stores. * - * Parameter and service keys are case insensitive. - * - * A service can also be defined by creating a method named - * getXXXService(), where XXX is the camelized version of the id: - * - *
        - *
      • request -> getRequestService()
      • - *
      • mysql_session_storage -> getMysqlSessionStorageService()
      • - *
      • symfony.mysql_session_storage -> getSymfony_MysqlSessionStorageService()
      • - *
      + * Parameter keys are case insensitive. * * The container can have three possible behaviors when a service does not exist: * @@ -49,7 +38,7 @@ * @author Fabien Potencier * @author Johannes M. Schmitt */ -class Container implements IntrospectableContainerInterface, ResettableContainerInterface +class Container implements ResettableContainerInterface { /** * @var ParameterBagInterface @@ -57,22 +46,20 @@ class Container implements IntrospectableContainerInterface, ResettableContainer protected $parameterBag; protected $services = array(); + protected $fileMap = array(); protected $methodMap = array(); protected $aliases = array(); - protected $scopes = array(); - protected $scopeChildren = array(); - protected $scopedServices = array(); - protected $scopeStacks = array(); protected $loading = array(); - private $underscoreMap = array('_' => '', '.' => '_', '\\' => '_'); + private $envCache = array(); + private $compiled = false; /** * @param ParameterBagInterface $parameterBag A ParameterBagInterface instance */ public function __construct(ParameterBagInterface $parameterBag = null) { - $this->parameterBag = $parameterBag ?: new ParameterBag(); + $this->parameterBag = $parameterBag ?: new EnvPlaceholderParameterBag(); } /** @@ -88,16 +75,18 @@ public function compile() $this->parameterBag->resolve(); $this->parameterBag = new FrozenParameterBag($this->parameterBag->all()); + + $this->compiled = true; } /** - * Returns true if the container parameter bag are frozen. + * Returns true if the container is compiled. * - * @return bool true if the container parameter bag are frozen, false otherwise + * @return bool */ - public function isFrozen() + public function isCompiled() { - return $this->parameterBag instanceof FrozenParameterBag; + return $this->compiled; } /** @@ -153,58 +142,30 @@ public function setParameter($name, $value) * Setting a service to null resets the service: has() returns false and get() * behaves in the same way as if the service was never created. * - * Note: The $scope parameter is deprecated since version 2.8 and will be removed in 3.0. - * * @param string $id The service identifier * @param object $service The service instance - * @param string $scope The scope of the service - * - * @throws RuntimeException When trying to set a service in an inactive scope - * @throws InvalidArgumentException When trying to set a service in the prototype scope */ - public function set($id, $service, $scope = self::SCOPE_CONTAINER) + public function set($id, $service) { - if (!in_array($scope, array('container', 'request')) || ('request' === $scope && 'request' !== $id)) { - @trigger_error('The concept of container scopes is deprecated since version 2.8 and will be removed in 3.0. Omit the third parameter.', E_USER_DEPRECATED); - } - - if (self::SCOPE_PROTOTYPE === $scope) { - throw new InvalidArgumentException(sprintf('You cannot set service "%s" of scope "prototype".', $id)); - } - - $id = strtolower($id); - if ('service_container' === $id) { - // BC: 'service_container' is no longer a self-reference but always - // $this, so ignore this call. - // @todo Throw InvalidArgumentException in next major release. - return; + throw new InvalidArgumentException('You cannot set service "service_container".'); } - if (self::SCOPE_CONTAINER !== $scope) { - if (!isset($this->scopedServices[$scope])) { - throw new RuntimeException(sprintf('You cannot set service "%s" of inactive scope.', $id)); - } - $this->scopedServices[$scope][$id] = $service; + if (isset($this->fileMap[$id]) || isset($this->methodMap[$id])) { + throw new InvalidArgumentException(sprintf('You cannot set the pre-defined service "%s".', $id)); } if (isset($this->aliases[$id])) { unset($this->aliases[$id]); } - $this->services[$id] = $service; - - if (method_exists($this, $method = 'synchronize'.strtr($id, $this->underscoreMap).'Service')) { - $this->$method(); - } - if (null === $service) { - if (self::SCOPE_CONTAINER !== $scope) { - unset($this->scopedServices[$scope][$id]); - } - unset($this->services[$id]); + + return; } + + $this->services[$id] = $service; } /** @@ -216,28 +177,22 @@ public function set($id, $service, $scope = self::SCOPE_CONTAINER) */ public function has($id) { - for ($i = 2;;) { - if ('service_container' === $id - || isset($this->aliases[$id]) - || isset($this->services[$id]) - || array_key_exists($id, $this->services) - ) { - return true; - } - if (--$i && $id !== $lcId = strtolower($id)) { - $id = $lcId; - } else { - return method_exists($this, 'get'.strtr($id, $this->underscoreMap).'Service'); - } + if (isset($this->aliases[$id])) { + $id = $this->aliases[$id]; + } + if (isset($this->services[$id])) { + return true; } + if ('service_container' === $id) { + return true; + } + + return isset($this->fileMap[$id]) || isset($this->methodMap[$id]); } /** * Gets a service. * - * If a service is defined both through a set() method and - * with a get{$id}Service() method, the former has always precedence. - * * @param string $id The service identifier * @param int $invalidBehavior The behavior when the service does not exist * @@ -251,76 +206,52 @@ public function has($id) */ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) { - // Attempt to retrieve the service by checking first aliases then - // available services. Service IDs are case insensitive, however since - // this method can be called thousands of times during a request, avoid - // calling strtolower() unless necessary. - for ($i = 2;;) { - if (isset($this->aliases[$id])) { - $id = $this->aliases[$id]; - } - // Re-use shared service instance if it exists. - if (isset($this->services[$id]) || array_key_exists($id, $this->services)) { - return $this->services[$id]; - } - if ('service_container' === $id) { - return $this; - } + if (isset($this->aliases[$id])) { + $id = $this->aliases[$id]; + } - if (isset($this->loading[$id])) { - throw new ServiceCircularReferenceException($id, array_keys($this->loading)); - } + // Re-use shared service instance if it exists. + if (isset($this->services[$id])) { + return $this->services[$id]; + } + if ('service_container' === $id) { + return $this; + } - if (isset($this->methodMap[$id])) { - $method = $this->methodMap[$id]; - } elseif (--$i && $id !== $lcId = strtolower($id)) { - $id = $lcId; - continue; - } elseif (method_exists($this, $method = 'get'.strtr($id, $this->underscoreMap).'Service')) { - // $method is set to the right value, proceed - } else { - if (self::EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior) { - if (!$id) { - throw new ServiceNotFoundException($id); - } - - $alternatives = array(); - foreach ($this->getServiceIds() as $knownId) { - $lev = levenshtein($id, $knownId); - if ($lev <= strlen($id) / 3 || false !== strpos($knownId, $id)) { - $alternatives[] = $knownId; - } - } - - throw new ServiceNotFoundException($id, null, null, $alternatives); - } + if (isset($this->loading[$id])) { + throw new ServiceCircularReferenceException($id, array_keys($this->loading)); + } - return; + $this->loading[$id] = true; + + try { + if (isset($this->fileMap[$id])) { + return $this->load($this->fileMap[$id]); + } elseif (isset($this->methodMap[$id])) { + return $this->{$this->methodMap[$id]}(); } + } catch (\Exception $e) { + unset($this->services[$id]); - $this->loading[$id] = true; + throw $e; + } finally { + unset($this->loading[$id]); + } - try { - $service = $this->$method(); - } catch (\Exception $e) { - unset($this->loading[$id]); - unset($this->services[$id]); + if (self::EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior) { + if (!$id) { + throw new ServiceNotFoundException($id); + } - if ($e instanceof InactiveScopeException && self::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) { - return; + $alternatives = array(); + foreach ($this->getServiceIds() as $knownId) { + $lev = levenshtein($id, $knownId); + if ($lev <= strlen($id) / 3 || false !== strpos($knownId, $id)) { + $alternatives[] = $knownId; } - - throw $e; - } catch (\Throwable $e) { - unset($this->loading[$id]); - unset($this->services[$id]); - - throw $e; } - unset($this->loading[$id]); - - return $service; + throw new ServiceNotFoundException($id, null, null, $alternatives); } } @@ -333,19 +264,15 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE */ public function initialized($id) { - $id = strtolower($id); - if (isset($this->aliases[$id])) { $id = $this->aliases[$id]; } if ('service_container' === $id) { - // BC: 'service_container' was a synthetic service previously. - // @todo Change to false in next major release. - return true; + return false; } - return isset($this->services[$id]) || array_key_exists($id, $this->services); + return isset($this->services[$id]); } /** @@ -353,10 +280,6 @@ public function initialized($id) */ public function reset() { - if (!empty($this->scopedServices)) { - throw new LogicException('Resetting the container is not allowed when a scope is active.'); - } - $this->services = array(); } @@ -367,216 +290,71 @@ public function reset() */ public function getServiceIds() { - $ids = array(); - foreach (get_class_methods($this) as $method) { - if (preg_match('/^get(.+)Service$/', $method, $match)) { - $ids[] = self::underscore($match[1]); - } - } - $ids[] = 'service_container'; - - return array_unique(array_merge($ids, array_keys($this->services))); + return array_unique(array_merge(array('service_container'), array_keys($this->fileMap), array_keys($this->methodMap), array_keys($this->services))); } /** - * This is called when you enter a scope. - * - * @param string $name + * Camelizes a string. * - * @throws RuntimeException When the parent scope is inactive - * @throws InvalidArgumentException When the scope does not exist + * @param string $id A string to camelize * - * @deprecated since version 2.8, to be removed in 3.0. + * @return string The camelized string */ - public function enterScope($name) + public static function camelize($id) { - if ('request' !== $name) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - if (!isset($this->scopes[$name])) { - throw new InvalidArgumentException(sprintf('The scope "%s" does not exist.', $name)); - } - - if (self::SCOPE_CONTAINER !== $this->scopes[$name] && !isset($this->scopedServices[$this->scopes[$name]])) { - throw new RuntimeException(sprintf('The parent scope "%s" must be active when entering this scope.', $this->scopes[$name])); - } - - // check if a scope of this name is already active, if so we need to - // remove all services of this scope, and those of any of its child - // scopes from the global services map - if (isset($this->scopedServices[$name])) { - $services = array($this->services, $name => $this->scopedServices[$name]); - unset($this->scopedServices[$name]); - - foreach ($this->scopeChildren[$name] as $child) { - if (isset($this->scopedServices[$child])) { - $services[$child] = $this->scopedServices[$child]; - unset($this->scopedServices[$child]); - } - } - - // update global map - $this->services = call_user_func_array('array_diff_key', $services); - array_shift($services); - - // add stack entry for this scope so we can restore the removed services later - if (!isset($this->scopeStacks[$name])) { - $this->scopeStacks[$name] = new \SplStack(); - } - $this->scopeStacks[$name]->push($services); - } - - $this->scopedServices[$name] = array(); + return strtr(ucwords(strtr($id, array('_' => ' ', '.' => '_ ', '\\' => '_ '))), array(' ' => '')); } /** - * This is called to leave the current scope, and move back to the parent - * scope. - * - * @param string $name The name of the scope to leave + * A string to underscore. * - * @throws InvalidArgumentException if the scope is not active + * @param string $id The string to underscore * - * @deprecated since version 2.8, to be removed in 3.0. + * @return string The underscored string */ - public function leaveScope($name) + public static function underscore($id) { - if ('request' !== $name) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - if (!isset($this->scopedServices[$name])) { - throw new InvalidArgumentException(sprintf('The scope "%s" is not active.', $name)); - } - - // remove all services of this scope, or any of its child scopes from - // the global service map - $services = array($this->services, $this->scopedServices[$name]); - unset($this->scopedServices[$name]); - - foreach ($this->scopeChildren[$name] as $child) { - if (isset($this->scopedServices[$child])) { - $services[] = $this->scopedServices[$child]; - unset($this->scopedServices[$child]); - } - } - - // update global map - $this->services = call_user_func_array('array_diff_key', $services); - - // check if we need to restore services of a previous scope of this type - if (isset($this->scopeStacks[$name]) && count($this->scopeStacks[$name]) > 0) { - $services = $this->scopeStacks[$name]->pop(); - $this->scopedServices += $services; - - if ($this->scopeStacks[$name]->isEmpty()) { - unset($this->scopeStacks[$name]); - } - - foreach ($services as $array) { - foreach ($array as $id => $service) { - $this->set($id, $service, $name); - } - } - } + return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), str_replace('_', '.', $id))); } /** - * Adds a scope to the container. - * - * @param ScopeInterface $scope + * Creates a service by requiring its factory file. * - * @throws InvalidArgumentException - * - * @deprecated since version 2.8, to be removed in 3.0. + * @return object The service created by the file */ - public function addScope(ScopeInterface $scope) + protected function load($file) { - $name = $scope->getName(); - $parentScope = $scope->getParentName(); - - if ('request' !== $name) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - if (self::SCOPE_CONTAINER === $name || self::SCOPE_PROTOTYPE === $name) { - throw new InvalidArgumentException(sprintf('The scope "%s" is reserved.', $name)); - } - if (isset($this->scopes[$name])) { - throw new InvalidArgumentException(sprintf('A scope with name "%s" already exists.', $name)); - } - if (self::SCOPE_CONTAINER !== $parentScope && !isset($this->scopes[$parentScope])) { - throw new InvalidArgumentException(sprintf('The parent scope "%s" does not exist, or is invalid.', $parentScope)); - } - - $this->scopes[$name] = $parentScope; - $this->scopeChildren[$name] = array(); - - // normalize the child relations - while ($parentScope !== self::SCOPE_CONTAINER) { - $this->scopeChildren[$parentScope][] = $name; - $parentScope = $this->scopes[$parentScope]; - } + return require $file; } /** - * Returns whether this container has a certain scope. + * Fetches a variable from the environment. * - * @param string $name The name of the scope + * @param string $name The name of the environment variable * - * @return bool + * @return mixed The value to use for the provided environment variable name * - * @deprecated since version 2.8, to be removed in 3.0. + * @throws EnvNotFoundException When the environment variable is not found and has no default value */ - public function hasScope($name) + protected function getEnv($name) { - if ('request' !== $name) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); + if (isset($this->envCache[$name]) || array_key_exists($name, $this->envCache)) { + return $this->envCache[$name]; + } + if (0 !== strpos($name, 'HTTP_') && isset($_SERVER[$name])) { + return $this->envCache[$name] = $_SERVER[$name]; + } + if (isset($_ENV[$name])) { + return $this->envCache[$name] = $_ENV[$name]; + } + if (false !== $env = getenv($name)) { + return $this->envCache[$name] = $env; + } + if (!$this->hasParameter("env($name)")) { + throw new EnvNotFoundException($name); } - return isset($this->scopes[$name]); - } - - /** - * Returns whether this scope is currently active. - * - * This does not actually check if the passed scope actually exists. - * - * @param string $name - * - * @return bool - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function isScopeActive($name) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - - return isset($this->scopedServices[$name]); - } - - /** - * Camelizes a string. - * - * @param string $id A string to camelize - * - * @return string The camelized string - */ - public static function camelize($id) - { - return strtr(ucwords(strtr($id, array('_' => ' ', '.' => '_ ', '\\' => '_ '))), array(' ' => '')); - } - - /** - * A string to underscore. - * - * @param string $id The string to underscore - * - * @return string The underscored string - */ - public static function underscore($id) - { - return strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), str_replace('_', '.', $id))); + return $this->envCache[$name] = $this->getParameter("env($name)"); } private function __clone() diff --git a/src/Symfony/Component/DependencyInjection/ContainerAware.php b/src/Symfony/Component/DependencyInjection/ContainerAware.php deleted file mode 100644 index f3f2a5065c311..0000000000000 --- a/src/Symfony/Component/DependencyInjection/ContainerAware.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection; - -/** - * A simple implementation of ContainerAwareInterface. - * - * @author Fabien Potencier - * - * @deprecated since version 2.8, to be removed in 3.0. Use the ContainerAwareTrait instead. - */ -abstract class ContainerAware implements ContainerAwareInterface -{ - /** - * @var ContainerInterface - */ - protected $container; - - /** - * {@inheritdoc} - */ - public function setContainer(ContainerInterface $container = null) - { - $this->container = $container; - } -} diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 262defd376bad..89105cd32f9b5 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -11,18 +11,29 @@ namespace Symfony\Component\DependencyInjection; +use Psr\Container\ContainerInterface as PsrContainerInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\Compiler; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Exception\BadMethodCallException; -use Symfony\Component\DependencyInjection\Exception\InactiveScopeException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Config\Resource\ClassExistenceResource; +use Symfony\Component\Config\Resource\ComposerResource; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\GlobResource; +use Symfony\Component\Config\Resource\ReflectionClassResource; use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface; use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator; @@ -52,11 +63,6 @@ class ContainerBuilder extends Container implements TaggedContainerInterface */ private $definitions = array(); - /** - * @var Definition[] - */ - private $obsoleteDefinitions = array(); - /** * @var Alias[] */ @@ -91,17 +97,42 @@ class ContainerBuilder extends Container implements TaggedContainerInterface */ private $expressionLanguageProviders = array(); + /** + * @var string[] with tag names used by findTaggedServiceIds + */ + private $usedTags = array(); + + /** + * @var string[][] a map of env var names to their placeholders + */ + private $envPlaceholders = array(); + + /** + * @var int[] a map of env vars to their resolution counter + */ + private $envCounters = array(); + + /** + * @var string[] the list of vendor directories + */ + private $vendors; + + private $autoconfiguredInstanceof = array(); + public function __construct(ParameterBagInterface $parameterBag = null) { parent::__construct($parameterBag); $this->trackResources = interface_exists('Symfony\Component\Config\Resource\ResourceInterface'); + $this->setDefinition('service_container', (new Definition(ContainerInterface::class))->setSynthetic(true)); + $this->setAlias(PsrContainerInterface::class, new Alias('service_container', false)); + $this->setAlias(ContainerInterface::class, new Alias('service_container', false)); } /** - * @var string[] with tag names used by findTaggedServiceIds + * @var \ReflectionClass[] a list of class reflectors */ - private $usedTags = array(); + private $classReflectors; /** * Sets the track resources flag. @@ -201,7 +232,7 @@ public function hasExtension($name) */ public function getResources() { - return array_unique($this->resources); + return array_values($this->resources); } /** @@ -217,7 +248,11 @@ public function addResource(ResourceInterface $resource) return $this; } - $this->resources[] = $resource; + if ($resource instanceof GlobResource && $this->inVendors($resource->getPrefix())) { + return $this; + } + + $this->resources[(string) $resource] = $resource; return $this; } @@ -243,39 +278,125 @@ public function setResources(array $resources) /** * Adds the object class hierarchy as resources. * - * @param object $object An object instance + * @param object|string $object An object instance or class name * * @return $this */ public function addObjectResource($object) { if ($this->trackResources) { - $this->addClassResource(new \ReflectionClass($object)); + if (is_object($object)) { + $object = get_class($object); + } + if (!isset($this->classReflectors[$object])) { + $this->classReflectors[$object] = new \ReflectionClass($object); + } + $class = $this->classReflectors[$object]; + + foreach ($class->getInterfaceNames() as $name) { + if (null === $interface = &$this->classReflectors[$name]) { + $interface = new \ReflectionClass($name); + } + $file = $interface->getFileName(); + if (false !== $file && file_exists($file)) { + $this->fileExists($file); + } + } + do { + $file = $class->getFileName(); + if (false !== $file && file_exists($file)) { + $this->fileExists($file); + } + foreach ($class->getTraitNames() as $name) { + $this->addObjectResource($name); + } + } while ($class = $class->getParentClass()); } return $this; } /** - * Adds the given class hierarchy as resources. + * Retrieves the requested reflection class and registers it for resource tracking. * - * @param \ReflectionClass $class + * @param string $class + * @param bool $throw * - * @return $this + * @return \ReflectionClass|null + * + * @throws \ReflectionException when a parent class/interface/trait is not found and $throw is true + * + * @final */ - public function addClassResource(\ReflectionClass $class) + public function getReflectionClass($class, $throw = true) { - if (!$this->trackResources) { - return $this; + if (!$class = $this->getParameterBag()->resolveValue($class)) { + return; } + $resource = null; - do { - if (is_file($class->getFileName())) { - $this->addResource(new FileResource($class->getFileName())); + try { + if (isset($this->classReflectors[$class])) { + $classReflector = $this->classReflectors[$class]; + } else { + $resource = new ClassExistenceResource($class, false); + $classReflector = $resource->isFresh(0) ? false : new \ReflectionClass($class); } - } while ($class = $class->getParentClass()); + } catch (\ReflectionException $e) { + if ($throw) { + throw $e; + } + $classReflector = false; + } - return $this; + if ($this->trackResources) { + if (!$classReflector) { + $this->addResource($resource ?: new ClassExistenceResource($class, false)); + } elseif (!$classReflector->isInternal()) { + $path = $classReflector->getFileName(); + + if (!$this->inVendors($path)) { + $this->addResource(new ReflectionClassResource($classReflector, $this->vendors)); + } + } + $this->classReflectors[$class] = $classReflector; + } + + return $classReflector ?: null; + } + + /** + * Checks whether the requested file or directory exists and registers the result for resource tracking. + * + * @param string $path The file or directory path for which to check the existence + * @param bool|string $trackContents Whether to track contents of the given resource. If a string is passed, + * it will be used as pattern for tracking contents of the requested directory + * + * @return bool + * + * @final + */ + public function fileExists($path, $trackContents = true) + { + $exists = file_exists($path); + + if (!$this->trackResources || $this->inVendors($path)) { + return $exists; + } + + if (!$exists) { + $this->addResource(new FileExistenceResource($path)); + + return $exists; + } + + if ($trackContents && is_dir($path)) { + $this->addResource(new DirectoryResource($path, is_string($trackContents) ? $trackContents : null)); + } elseif ($trackContents || is_dir($path)) { + $this->addResource(new FileResource($path)); + } + + return $exists; } /** @@ -286,13 +407,13 @@ public function addClassResource(\ReflectionClass $class) * * @return $this * - * @throws BadMethodCallException When this ContainerBuilder is frozen - * @throws \LogicException if the container is frozen + * @throws BadMethodCallException When this ContainerBuilder is compiled + * @throws \LogicException if the extension is not registered */ public function loadFromExtension($extension, array $values = array()) { - if ($this->isFrozen()) { - throw new BadMethodCallException('Cannot load from an extension on a frozen container.'); + if ($this->isCompiled()) { + throw new BadMethodCallException('Cannot load from an extension on a compiled container.'); } $namespace = $this->getExtension($extension)->getAlias(); @@ -305,14 +426,15 @@ public function loadFromExtension($extension, array $values = array()) /** * Adds a compiler pass. * - * @param CompilerPassInterface $pass A compiler pass - * @param string $type The type of compiler pass + * @param CompilerPassInterface $pass A compiler pass + * @param string $type The type of compiler pass + * @param int $priority Used to sort the passes * * @return $this */ - public function addCompilerPass(CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION) + public function addCompilerPass(CompilerPassInterface $pass, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0) { - $this->getCompiler()->addPass($pass, $type); + $this->getCompiler()->addPass($pass, $type, $priority); $this->addObjectResource($pass); @@ -343,70 +465,24 @@ public function getCompiler() return $this->compiler; } - /** - * Returns all Scopes. - * - * @return array An array of scopes - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function getScopes($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - return $this->scopes; - } - - /** - * Returns all Scope children. - * - * @return array An array of scope children - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function getScopeChildren($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - return $this->scopeChildren; - } - /** * Sets a service. * - * Note: The $scope parameter is deprecated since version 2.8 and will be removed in 3.0. - * * @param string $id The service identifier * @param object $service The service instance - * @param string $scope The scope * - * @throws BadMethodCallException When this ContainerBuilder is frozen + * @throws BadMethodCallException When this ContainerBuilder is compiled */ - public function set($id, $service, $scope = self::SCOPE_CONTAINER) + public function set($id, $service) { - $id = strtolower($id); - $set = isset($this->definitions[$id]); - - if ($this->isFrozen() && ($set || isset($this->obsoleteDefinitions[$id])) && !$this->{$set ? 'definitions' : 'obsoleteDefinitions'}[$id]->isSynthetic()) { - // setting a synthetic service on a frozen container is alright - throw new BadMethodCallException(sprintf('Setting service "%s" on a frozen container is not allowed.', $id)); - } - - if ($set) { - $this->obsoleteDefinitions[$id] = $this->definitions[$id]; + if ($this->isCompiled() && (isset($this->definitions[$id]) && !$this->definitions[$id]->isSynthetic())) { + // setting a synthetic service on a compiled container is alright + throw new BadMethodCallException(sprintf('Setting service "%s" for an unknown or non-synthetic service definition on a compiled container is not allowed.', $id)); } unset($this->definitions[$id], $this->aliasDefinitions[$id]); - parent::set($id, $service, $scope); - - if (isset($this->obsoleteDefinitions[$id]) && $this->obsoleteDefinitions[$id]->isSynchronized(false)) { - $this->synchronize($id); - } + parent::set($id, $service); } /** @@ -416,7 +492,7 @@ public function set($id, $service, $scope = self::SCOPE_CONTAINER) */ public function removeDefinition($id) { - unset($this->definitions[strtolower($id)]); + unset($this->definitions[$id]); } /** @@ -428,8 +504,6 @@ public function removeDefinition($id) */ public function has($id) { - $id = strtolower($id); - return isset($this->definitions[$id]) || isset($this->aliasDefinitions[$id]) || parent::has($id); } @@ -450,13 +524,11 @@ public function has($id) */ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { - $id = strtolower($id); - if ($service = parent::get($id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) { return $service; } - if (!array_key_exists($id, $this->definitions) && isset($this->aliasDefinitions[$id])) { + if (!isset($this->definitions[$id]) && isset($this->aliasDefinitions[$id])) { return $this->get((string) $this->aliasDefinitions[$id], $invalidBehavior); } @@ -474,22 +546,10 @@ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INV try { $service = $this->createService($definition, $id); - } catch (\Exception $e) { + } finally { unset($this->loading[$id]); - - if ($e instanceof InactiveScopeException && self::EXCEPTION_ON_INVALID_REFERENCE !== $invalidBehavior) { - return; - } - - throw $e; - } catch (\Throwable $e) { - unset($this->loading[$id]); - - throw $e; } - unset($this->loading[$id]); - return $service; } @@ -513,12 +573,12 @@ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INV * * @param ContainerBuilder $container The ContainerBuilder instance to merge * - * @throws BadMethodCallException When this ContainerBuilder is frozen + * @throws BadMethodCallException When this ContainerBuilder is compiled */ public function merge(ContainerBuilder $container) { - if ($this->isFrozen()) { - throw new BadMethodCallException('Cannot merge on a frozen container.'); + if ($this->isCompiled()) { + throw new BadMethodCallException('Cannot merge on a compiled container.'); } $this->addDefinitions($container->getDefinitions()); @@ -538,6 +598,26 @@ public function merge(ContainerBuilder $container) $this->extensionConfigs[$name] = array_merge($this->extensionConfigs[$name], $container->getExtensionConfig($name)); } + + if ($this->getParameterBag() instanceof EnvPlaceholderParameterBag && $container->getParameterBag() instanceof EnvPlaceholderParameterBag) { + $this->getParameterBag()->mergeEnvPlaceholders($container->getParameterBag()); + } + + foreach ($container->envCounters as $env => $count) { + if (!isset($this->envCounters[$env])) { + $this->envCounters[$env] = $count; + } else { + $this->envCounters[$env] += $count; + } + } + + foreach ($container->getAutoconfiguredInstanceof() as $interface => $childDefinition) { + if (isset($this->autoconfiguredInstanceof[$interface])) { + throw new InvalidArgumentException(sprintf('"%s" has already been autoconfigured and merge() does not support merging autoconfiguration for the same class/interface.', $interface)); + } + + $this->autoconfiguredInstanceof[$interface] = $childDefinition; + } } /** @@ -584,8 +664,13 @@ public function prependExtensionConfig($name, array $config) * * Parameter values are resolved; * * The parameter bag is frozen; * * Extension loading is disabled. + * + * @param bool $resolveEnvPlaceholders Whether %env()% parameters should be resolved using the current + * env vars or be replaced by uniquely identifiable placeholders. + * Set to "true" when you want to use the current ContainerBuilder + * directly, keep to "false" when the container is dumped instead. */ - public function compile() + public function compile(bool $resolveEnvPlaceholders = false) { $compiler = $this->getCompiler(); @@ -594,20 +679,30 @@ public function compile() $this->addObjectResource($pass); } } + $bag = $this->getParameterBag(); + + if ($resolveEnvPlaceholders && $bag instanceof EnvPlaceholderParameterBag) { + $bag->resolveEnvReferences(); + $this->parameterBag = new ParameterBag($bag->all()); + $this->envPlaceholders = $bag->getEnvPlaceholders(); + $this->parameterBag = $bag = new ParameterBag($this->resolveEnvPlaceholders($bag->all(), true)); + } $compiler->compile($this); - if ($this->trackResources) { - foreach ($this->definitions as $definition) { - if ($definition->isLazy() && ($class = $definition->getClass()) && class_exists($class)) { - $this->addClassResource(new \ReflectionClass($class)); - } + foreach ($this->definitions as $id => $definition) { + if ($this->trackResources && $definition->isLazy()) { + $this->getReflectionClass($definition->getClass()); } } $this->extensionConfigs = array(); parent::compile(); + + if ($bag instanceof EnvPlaceholderParameterBag) { + $this->envPlaceholders = $bag->getEnvPlaceholders(); + } } /** @@ -654,8 +749,6 @@ public function setAliases(array $aliases) */ public function setAlias($alias, $id) { - $alias = strtolower($alias); - if (is_string($id)) { $id = new Alias($id); } elseif (!$id instanceof Alias) { @@ -678,7 +771,7 @@ public function setAlias($alias, $id) */ public function removeAlias($alias) { - unset($this->aliasDefinitions[strtolower($alias)]); + unset($this->aliasDefinitions[$alias]); } /** @@ -690,7 +783,7 @@ public function removeAlias($alias) */ public function hasAlias($id) { - return isset($this->aliasDefinitions[strtolower($id)]); + return isset($this->aliasDefinitions[$id]); } /** @@ -714,8 +807,6 @@ public function getAliases() */ public function getAlias($id) { - $id = strtolower($id); - if (!isset($this->aliasDefinitions[$id])) { throw new InvalidArgumentException(sprintf('The service alias "%s" does not exist.', $id)); } @@ -739,6 +830,22 @@ public function register($id, $class = null) return $this->setDefinition($id, new Definition($class)); } + /** + * Registers an autowired service definition. + * + * This method implements a shortcut for using setDefinition() with + * an autowired definition. + * + * @param string $id The service identifier + * @param null|string $class The service class + * + * @return Definition The created definition + */ + public function autowire($id, $class = null) + { + return $this->setDefinition($id, (new Definition($class))->setAutowired(true)); + } + /** * Adds the service definitions. * @@ -780,16 +887,14 @@ public function getDefinitions() * * @return Definition the service definition * - * @throws BadMethodCallException When this ContainerBuilder is frozen + * @throws BadMethodCallException When this ContainerBuilder is compiled */ public function setDefinition($id, Definition $definition) { - if ($this->isFrozen()) { - throw new BadMethodCallException('Adding definition to a frozen container is not allowed'); + if ($this->isCompiled()) { + throw new BadMethodCallException('Adding definition to a compiled container is not allowed'); } - $id = strtolower($id); - unset($this->aliasDefinitions[$id]); return $this->definitions[$id] = $definition; @@ -804,7 +909,7 @@ public function setDefinition($id, Definition $definition) */ public function hasDefinition($id) { - return array_key_exists(strtolower($id), $this->definitions); + return isset($this->definitions[$id]); } /** @@ -818,9 +923,7 @@ public function hasDefinition($id) */ public function getDefinition($id) { - $id = strtolower($id); - - if (!array_key_exists($id, $this->definitions)) { + if (!isset($this->definitions[$id])) { throw new ServiceNotFoundException($id); } @@ -840,8 +943,6 @@ public function getDefinition($id) */ public function findDefinition($id) { - $id = strtolower($id); - while (isset($this->aliasDefinitions[$id])) { $id = (string) $this->aliasDefinitions[$id]; } @@ -858,16 +959,13 @@ public function findDefinition($id) * * @return object The service described by the service definition * - * @throws RuntimeException When the scope is inactive * @throws RuntimeException When the factory definition is incomplete * @throws RuntimeException When the service is a synthetic service * @throws InvalidArgumentException When configure callable is not callable - * - * @internal this method is public because of PHP 5.3 limitations, do not use it explicitly in your code */ - public function createService(Definition $definition, $id, $tryProxy = true) + private function createService(Definition $definition, $id, $tryProxy = true) { - if ($definition instanceof DefinitionDecorator) { + if ($definition instanceof ChildDefinition) { throw new RuntimeException(sprintf('Constructing service "%s" from a parent definition is not supported at build time.', $id)); } @@ -880,15 +978,13 @@ public function createService(Definition $definition, $id, $tryProxy = true) } if ($tryProxy && $definition->isLazy()) { - $container = $this; - $proxy = $this ->getProxyInstantiator() ->instantiateProxy( - $container, + $this, $definition, - $id, function () use ($definition, $id, $container) { - return $container->createService($definition, $id, false); + $id, function () use ($definition, $id) { + return $this->createService($definition, $id, false); } ); $this->shareService($definition, $proxy, $id); @@ -920,18 +1016,8 @@ public function createService(Definition $definition, $id, $tryProxy = true) @trigger_error(sprintf('The "%s" service relies on the deprecated "%s" factory class. It should either be deprecated or its factory upgraded.', $id, $r->name), E_USER_DEPRECATED); } } - } elseif (null !== $definition->getFactoryMethod(false)) { - if (null !== $definition->getFactoryClass(false)) { - $factory = $parameterBag->resolveValue($definition->getFactoryClass(false)); - } elseif (null !== $definition->getFactoryService(false)) { - $factory = $this->get($parameterBag->resolveValue($definition->getFactoryService(false))); - } else { - throw new RuntimeException(sprintf('Cannot create service "%s" from factory method without a factory service or factory class.', $id)); - } - - $service = call_user_func_array(array($factory, $definition->getFactoryMethod(false)), $arguments); } else { - $r = new \ReflectionClass($parameterBag->resolveValue($definition->getClass())); + $r = new \ReflectionClass($class = $parameterBag->resolveValue($definition->getClass())); $service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments); @@ -989,6 +1075,36 @@ public function resolveServices($value) foreach ($value as $k => $v) { $value[$k] = $this->resolveServices($v); } + } elseif ($value instanceof ServiceClosureArgument) { + $reference = $value->getValues()[0]; + $value = function () use ($reference) { + return $this->resolveServices($reference); + }; + } elseif ($value instanceof IteratorArgument) { + $value = new RewindableGenerator(function () use ($value) { + foreach ($value->getValues() as $k => $v) { + foreach (self::getServiceConditionals($v) as $s) { + if (!$this->has($s)) { + continue 2; + } + } + + yield $k => $this->resolveServices($v); + } + }, function () use ($value) { + $count = 0; + foreach ($value->getValues() as $v) { + foreach (self::getServiceConditionals($v) as $s) { + if (!$this->has($s)) { + continue 2; + } + } + + ++$count; + } + + return $count; + }); } elseif ($value instanceof Reference) { $value = $this->get((string) $value, $value->getInvalidBehavior()); } elseif ($value instanceof Definition) { @@ -1014,16 +1130,20 @@ public function resolveServices($value) * } * } * - * @param string $name The tag name + * @param string $name + * @param bool $throwOnAbstract * * @return array An array of tags with the tagged service as key, holding a list of attribute arrays */ - public function findTaggedServiceIds($name) + public function findTaggedServiceIds($name, $throwOnAbstract = false) { $this->usedTags[] = $name; $tags = array(); foreach ($this->getDefinitions() as $id => $definition) { if ($definition->hasTag($name)) { + if ($throwOnAbstract && $definition->isAbstract()) { + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must not be abstract.', $id, $name)); + } $tags[$id] = $definition->getTag($name); } } @@ -1069,6 +1189,113 @@ public function getExpressionLanguageProviders() return $this->expressionLanguageProviders; } + /** + * Returns a ChildDefinition that will be used for autoconfiguring the interface/class. + * + * @param string $interface The class or interface to match + * + * @return ChildDefinition + */ + public function registerForAutoconfiguration($interface) + { + if (!isset($this->autoconfiguredInstanceof[$interface])) { + $this->autoconfiguredInstanceof[$interface] = new ChildDefinition(''); + } + + return $this->autoconfiguredInstanceof[$interface]; + } + + /** + * Returns an array of ChildDefinition[] keyed by interface. + * + * @return ChildDefinition[] + */ + public function getAutoconfiguredInstanceof() + { + return $this->autoconfiguredInstanceof; + } + + /** + * Resolves env parameter placeholders in a string or an array. + * + * @param mixed $value The value to resolve + * @param string|true|null $format A sprintf() format returning the replacement for each env var name or + * null to resolve back to the original "%env(VAR)%" format or + * true to resolve to the actual values of the referenced env vars + * @param array &$usedEnvs Env vars found while resolving are added to this array + * + * @return string The string with env parameters resolved + */ + public function resolveEnvPlaceholders($value, $format = null, array &$usedEnvs = null) + { + if (null === $format) { + $format = '%%env(%s)%%'; + } + + if (is_array($value)) { + $result = array(); + foreach ($value as $k => $v) { + $result[$this->resolveEnvPlaceholders($k, $format, $usedEnvs)] = $this->resolveEnvPlaceholders($v, $format, $usedEnvs); + } + + return $result; + } + + if (!is_string($value)) { + return $value; + } + + $bag = $this->getParameterBag(); + if (true === $format) { + $value = $bag->resolveValue($value); + } + $envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders; + + foreach ($envPlaceholders as $env => $placeholders) { + foreach ($placeholders as $placeholder) { + if (false !== stripos($value, $placeholder)) { + if (true !== $format) { + $resolved = sprintf($format, $env); + } elseif ($placeholder === $resolved = $bag->escapeValue($this->getEnv($env))) { + $resolved = $bag->all()[strtolower("env($env)")]; + } + $value = str_ireplace($placeholder, $resolved, $value); + $usedEnvs[$env] = $env; + $this->envCounters[$env] = isset($this->envCounters[$env]) ? 1 + $this->envCounters[$env] : 1; + } + } + } + + return $value; + } + + /** + * Get statistics about env usage. + * + * @return int[] The number of time each env vars has been resolved + */ + public function getEnvCounters() + { + $bag = $this->getParameterBag(); + $envPlaceholders = $bag instanceof EnvPlaceholderParameterBag ? $bag->getEnvPlaceholders() : $this->envPlaceholders; + + foreach ($envPlaceholders as $env => $placeholders) { + if (!isset($this->envCounters[$env])) { + $this->envCounters[$env] = 0; + } + } + + return $this->envCounters; + } + + /** + * @final + */ + public function log(CompilerPassInterface $pass, $message) + { + $this->getCompiler()->log($pass, $message); + } + /** * Returns the Service Conditionals. * @@ -1092,49 +1319,31 @@ public static function getServiceConditionals($value) } /** - * Retrieves the currently set proxy instantiator or instantiates one. + * Computes a reasonably unique hash of a value. * - * @return InstantiatorInterface + * @param mixed $value A serializable value + * + * @return string */ - private function getProxyInstantiator() + public static function hash($value) { - if (!$this->proxyInstantiator) { - $this->proxyInstantiator = new RealServiceInstantiator(); - } + $hash = substr(base64_encode(hash('sha256', serialize($value), true)), 0, 7); - return $this->proxyInstantiator; + return str_replace(array('/', '+'), array('.', '_'), strtolower($hash)); } /** - * Synchronizes a service change. - * - * This method updates all services that depend on the given - * service by calling all methods referencing it. - * - * @param string $id A service id + * Retrieves the currently set proxy instantiator or instantiates one. * - * @deprecated since version 2.7, will be removed in 3.0. + * @return InstantiatorInterface */ - private function synchronize($id) + private function getProxyInstantiator() { - if ('request' !== $id) { - @trigger_error('The '.__METHOD__.' method is deprecated in version 2.7 and will be removed in version 3.0.', E_USER_DEPRECATED); + if (!$this->proxyInstantiator) { + $this->proxyInstantiator = new RealServiceInstantiator(); } - foreach ($this->definitions as $definitionId => $definition) { - // only check initialized services - if (!$this->initialized($definitionId)) { - continue; - } - - foreach ($definition->getMethodCalls() as $call) { - foreach ($call[1] as $argument) { - if ($argument instanceof Reference && $id == (string) $argument) { - $this->callMethod($this->get($definitionId), $call); - } - } - } - } + return $this->proxyInstantiator; } private function callMethod($service, $call) @@ -1154,23 +1363,13 @@ private function callMethod($service, $call) * Shares a given service in the container. * * @param Definition $definition - * @param mixed $service + * @param object $service * @param string|null $id - * - * @throws InactiveScopeException */ private function shareService(Definition $definition, $service, $id) { - if (null !== $id && $definition->isShared() && self::SCOPE_PROTOTYPE !== $scope = $definition->getScope(false)) { - if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) { - throw new InactiveScopeException($id, $scope); - } - - $this->services[$lowerId = strtolower($id)] = $service; - - if (self::SCOPE_CONTAINER !== $scope) { - $this->scopedServices[$scope][$lowerId] = $service; - } + if (null !== $id && $definition->isShared()) { + $this->services[$id] = $service; } } @@ -1185,4 +1384,22 @@ private function getExpressionLanguage() return $this->expressionLanguage; } + + private function inVendors($path) + { + if (null === $this->vendors) { + $resource = new ComposerResource(); + $this->vendors = $resource->getVendors(); + $this->addResource($resource); + } + $path = realpath($path) ?: $path; + + foreach ($this->vendors as $vendor) { + if (0 === strpos($path, $vendor) && false !== strpbrk(substr($path, strlen($vendor), 1), '/'.DIRECTORY_SEPARATOR)) { + return true; + } + } + + return false; + } } diff --git a/src/Symfony/Component/DependencyInjection/ContainerInterface.php b/src/Symfony/Component/DependencyInjection/ContainerInterface.php index d9076eb1f8768..cfbc828722d8a 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerInterface.php +++ b/src/Symfony/Component/DependencyInjection/ContainerInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection; +use Psr\Container\ContainerInterface as PsrContainerInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; @@ -21,24 +22,19 @@ * @author Fabien Potencier * @author Johannes M. Schmitt */ -interface ContainerInterface +interface ContainerInterface extends PsrContainerInterface { const EXCEPTION_ON_INVALID_REFERENCE = 1; const NULL_ON_INVALID_REFERENCE = 2; const IGNORE_ON_INVALID_REFERENCE = 3; - const SCOPE_CONTAINER = 'container'; - const SCOPE_PROTOTYPE = 'prototype'; /** * Sets a service. * - * Note: The $scope parameter is deprecated since version 2.8 and will be removed in 3.0. - * * @param string $id The service identifier * @param object $service The service instance - * @param string $scope The scope of the service */ - public function set($id, $service, $scope = self::SCOPE_CONTAINER); + public function set($id, $service); /** * Gets a service. @@ -64,6 +60,15 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE */ public function has($id); + /** + * Check for whether or not a service has been initialized. + * + * @param string $id + * + * @return bool true if the service has been initialized, false otherwise + */ + public function initialized($id); + /** * Gets a parameter. * @@ -91,55 +96,4 @@ public function hasParameter($name); * @param mixed $value The parameter value */ public function setParameter($name, $value); - - /** - * Enters the given scope. - * - * @param string $name - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function enterScope($name); - - /** - * Leaves the current scope, and re-enters the parent scope. - * - * @param string $name - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function leaveScope($name); - - /** - * Adds a scope to the container. - * - * @param ScopeInterface $scope - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function addScope(ScopeInterface $scope); - - /** - * Whether this container has the given scope. - * - * @param string $name - * - * @return bool - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function hasScope($name); - - /** - * Determines whether the given scope is currently active. - * - * It does however not check if the scope actually exists. - * - * @param string $name - * - * @return bool - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function isScopeActive($name); } diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 8eb90c9c86828..5a93ac63a49ea 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; @@ -24,29 +25,27 @@ class Definition private $class; private $file; private $factory; - private $factoryClass; - private $factoryMethod; - private $factoryService; private $shared = true; private $deprecated = false; private $deprecationTemplate; - private $scope = ContainerInterface::SCOPE_CONTAINER; private $properties = array(); private $calls = array(); + private $instanceof = array(); + private $autoconfigured = false; private $configurator; private $tags = array(); private $public = true; private $synthetic = false; private $abstract = false; - private $synchronized = false; private $lazy = false; private $decoratedService; private $autowired = false; - private $autowiringTypes = array(); + private $changes = array(); + private $bindings = array(); - private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'; + protected $arguments = array(); - protected $arguments; + private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'; /** * @param string|null $class The service class @@ -54,89 +53,64 @@ class Definition */ public function __construct($class = null, array $arguments = array()) { - $this->class = $class; - $this->arguments = $arguments; - } - - /** - * Sets a factory. - * - * @param string|array $factory A PHP function or an array containing a class/Reference and a method to call - * - * @return $this - */ - public function setFactory($factory) - { - if (is_string($factory) && strpos($factory, '::') !== false) { - $factory = explode('::', $factory, 2); + if (null !== $class) { + $this->setClass($class); } - - $this->factory = $factory; - - return $this; + $this->arguments = $arguments; } /** - * Gets the factory. + * Returns all changes tracked for the Definition object. * - * @return string|array The PHP function or an array containing a class/Reference and a method to call + * @return array An array of changes for this Definition */ - public function getFactory() + public function getChanges() { - return $this->factory; + return $this->changes; } /** - * Sets the name of the class that acts as a factory using the factory method, - * which will be invoked statically. + * Sets the tracked changes for the Definition object. * - * @param string $factoryClass The factory class name + * @param array $changes An array of changes for this Definition * * @return $this - * - * @deprecated since version 2.6, to be removed in 3.0. */ - public function setFactoryClass($factoryClass) + public function setChanges(array $changes) { - @trigger_error(sprintf('%s(%s) is deprecated since version 2.6 and will be removed in 3.0. Use Definition::setFactory() instead.', __METHOD__, $factoryClass), E_USER_DEPRECATED); - - $this->factoryClass = $factoryClass; + $this->changes = $changes; return $this; } /** - * Gets the factory class. + * Sets a factory. * - * @return string|null The factory class name + * @param string|array $factory A PHP function or an array containing a class/Reference and a method to call * - * @deprecated since version 2.6, to be removed in 3.0. + * @return $this */ - public function getFactoryClass($triggerDeprecationError = true) + public function setFactory($factory) { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0.', E_USER_DEPRECATED); + $this->changes['factory'] = true; + + if (is_string($factory) && strpos($factory, '::') !== false) { + $factory = explode('::', $factory, 2); } - return $this->factoryClass; + $this->factory = $factory; + + return $this; } /** - * Sets the factory method able to create an instance of this class. - * - * @param string $factoryMethod The factory method name - * - * @return $this + * Gets the factory. * - * @deprecated since version 2.6, to be removed in 3.0. + * @return string|array The PHP function or an array containing a class/Reference and a method to call */ - public function setFactoryMethod($factoryMethod) + public function getFactory() { - @trigger_error(sprintf('%s(%s) is deprecated since version 2.6 and will be removed in 3.0. Use Definition::setFactory() instead.', __METHOD__, $factoryMethod), E_USER_DEPRECATED); - - $this->factoryMethod = $factoryMethod; - - return $this; + return $this->factory; } /** @@ -153,9 +127,11 @@ public function setFactoryMethod($factoryMethod) public function setDecoratedService($id, $renamedId = null, $priority = 0) { if ($renamedId && $id == $renamedId) { - throw new \InvalidArgumentException(sprintf('The decorated service inner name for "%s" must be different than the service name itself.', $id)); + throw new InvalidArgumentException(sprintf('The decorated service inner name for "%s" must be different than the service name itself.', $id)); } + $this->changes['decorated_service'] = true; + if (null === $id) { $this->decoratedService = null; } else { @@ -175,58 +151,6 @@ public function getDecoratedService() return $this->decoratedService; } - /** - * Gets the factory method. - * - * @return string|null The factory method name - * - * @deprecated since version 2.6, to be removed in 3.0. - */ - public function getFactoryMethod($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - return $this->factoryMethod; - } - - /** - * Sets the name of the service that acts as a factory using the factory method. - * - * @param string $factoryService The factory service id - * - * @return $this - * - * @deprecated since version 2.6, to be removed in 3.0. - */ - public function setFactoryService($factoryService, $triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error(sprintf('%s(%s) is deprecated since version 2.6 and will be removed in 3.0. Use Definition::setFactory() instead.', __METHOD__, $factoryService), E_USER_DEPRECATED); - } - - $this->factoryService = $factoryService; - - return $this; - } - - /** - * Gets the factory service id. - * - * @return string|null The factory service id - * - * @deprecated since version 2.6, to be removed in 3.0. - */ - public function getFactoryService($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - return $this->factoryService; - } - /** * Sets the service class. * @@ -236,6 +160,8 @@ public function getFactoryService($triggerDeprecationError = true) */ public function setClass($class) { + $this->changes['class'] = true; + $this->class = $class; return $this; @@ -321,8 +247,8 @@ public function addArgument($argument) /** * Replaces a specific argument. * - * @param int $index - * @param mixed $argument + * @param int|string $index + * @param mixed $argument * * @return $this * @@ -334,15 +260,34 @@ public function replaceArgument($index, $argument) throw new OutOfBoundsException('Cannot replace arguments if none have been configured yet.'); } - if ($index < 0 || $index > count($this->arguments) - 1) { + if (is_int($index) && ($index < 0 || $index > count($this->arguments) - 1)) { throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [0, %d].', $index, count($this->arguments) - 1)); } + if (!array_key_exists($index, $this->arguments)) { + throw new OutOfBoundsException(sprintf('The argument "%s" doesn\'t exist.', $index)); + } + $this->arguments[$index] = $argument; return $this; } + /** + * Sets a specific argument. + * + * @param int|string $key + * @param mixed $value + * + * @return $this + */ + public function setArgument($key, $value) + { + $this->arguments[$key] = $value; + + return $this; + } + /** * Gets the arguments to pass to the service constructor/factory method. * @@ -356,7 +301,7 @@ public function getArguments() /** * Gets an argument to pass to the service constructor/factory method. * - * @param int $index + * @param int|string $index * * @return mixed The argument value * @@ -364,8 +309,8 @@ public function getArguments() */ public function getArgument($index) { - if ($index < 0 || $index > count($this->arguments) - 1) { - throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [0, %d].', $index, count($this->arguments) - 1)); + if (!array_key_exists($index, $this->arguments)) { + throw new OutOfBoundsException(sprintf('The argument "%s" doesn\'t exist.', $index)); } return $this->arguments[$index]; @@ -455,6 +400,54 @@ public function getMethodCalls() return $this->calls; } + /** + * Sets the definition templates to conditionally apply on the current definition, keyed by parent interface/class. + * + * @param $instanceof ChildDefinition[] + * + * @return $this + */ + public function setInstanceofConditionals(array $instanceof) + { + $this->instanceof = $instanceof; + + return $this; + } + + /** + * Gets the definition templates to conditionally apply on the current definition, keyed by parent interface/class. + * + * @return ChildDefinition[] + */ + public function getInstanceofConditionals() + { + return $this->instanceof; + } + + /** + * Sets whether or not instanceof conditionals should be prepended with a global set. + * + * @param bool $autoconfigured + * + * @return $this + */ + public function setAutoconfigured($autoconfigured) + { + $this->changes['autoconfigured'] = true; + + $this->autoconfigured = $autoconfigured; + + return $this; + } + + /** + * @return bool + */ + public function isAutoconfigured() + { + return $this->autoconfigured; + } + /** * Sets tags for this definition. * @@ -553,6 +546,8 @@ public function clearTags() */ public function setFile($file) { + $this->changes['file'] = true; + $this->file = $file; return $this; @@ -577,6 +572,8 @@ public function getFile() */ public function setShared($shared) { + $this->changes['shared'] = true; + $this->shared = (bool) $shared; return $this; @@ -592,46 +589,6 @@ public function isShared() return $this->shared; } - /** - * Sets the scope of the service. - * - * @param string $scope Whether the service must be shared or not - * - * @return $this - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function setScope($scope, $triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - if (ContainerInterface::SCOPE_PROTOTYPE === $scope) { - $this->setShared(false); - } - - $this->scope = $scope; - - return $this; - } - - /** - * Returns the scope of the service. - * - * @return string - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function getScope($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - return $this->scope; - } - /** * Sets the visibility of this service. * @@ -641,6 +598,8 @@ public function getScope($triggerDeprecationError = true) */ public function setPublic($boolean) { + $this->changes['public'] = true; + $this->public = (bool) $boolean; return $this; @@ -656,42 +615,6 @@ public function isPublic() return $this->public; } - /** - * Sets the synchronized flag of this service. - * - * @param bool $boolean - * - * @return $this - * - * @deprecated since version 2.7, will be removed in 3.0. - */ - public function setSynchronized($boolean, $triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - $this->synchronized = (bool) $boolean; - - return $this; - } - - /** - * Whether this service is synchronized. - * - * @return bool - * - * @deprecated since version 2.7, will be removed in 3.0. - */ - public function isSynchronized($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - return $this->synchronized; - } - /** * Sets the lazy flag of this service. * @@ -701,6 +624,8 @@ public function isSynchronized($triggerDeprecationError = true) */ public function setLazy($lazy) { + $this->changes['lazy'] = true; + $this->lazy = (bool) $lazy; return $this; @@ -793,6 +718,8 @@ public function setDeprecated($status = true, $template = null) $this->deprecationTemplate = $template; } + $this->changes['deprecated'] = true; + $this->deprecated = (bool) $status; return $this; @@ -824,13 +751,19 @@ public function getDeprecationMessage($id) /** * Sets a configurator to call after the service is fully initialized. * - * @param callable $callable A PHP callable + * @param string|array $configurator A PHP callable * * @return $this */ - public function setConfigurator($callable) + public function setConfigurator($configurator) { - $this->configurator = $callable; + $this->changes['configurator'] = true; + + if (is_string($configurator) && strpos($configurator, '::') !== false) { + $configurator = explode('::', $configurator, 2); + } + + $this->configurator = $configurator; return $this; } @@ -845,24 +778,6 @@ public function getConfigurator() return $this->configurator; } - /** - * Sets types that will default to this definition. - * - * @param string[] $types - * - * @return $this - */ - public function setAutowiringTypes(array $types) - { - $this->autowiringTypes = array(); - - foreach ($types as $type) { - $this->autowiringTypes[$type] = true; - } - - return $this; - } - /** * Is the definition autowired? * @@ -882,58 +797,44 @@ public function isAutowired() */ public function setAutowired($autowired) { - $this->autowired = $autowired; + $this->changes['autowired'] = true; + + $this->autowired = (bool) $autowired; return $this; } /** - * Gets autowiring types that will default to this definition. + * Gets bindings. * - * @return string[] + * @return array */ - public function getAutowiringTypes() + public function getBindings() { - return array_keys($this->autowiringTypes); + return $this->bindings; } /** - * Adds a type that will default to this definition. + * Sets bindings. * - * @param string $type + * Bindings map $named or FQCN arguments to values that should be + * injected in the matching parameters (of the constructor, of methods + * called and of controller actions). * - * @return $this - */ - public function addAutowiringType($type) - { - $this->autowiringTypes[$type] = true; - - return $this; - } - - /** - * Removes a type. - * - * @param string $type + * @param array $bindings * * @return $this */ - public function removeAutowiringType($type) + public function setBindings(array $bindings) { - unset($this->autowiringTypes[$type]); + foreach ($bindings as $key => $binding) { + if (!$binding instanceof BoundArgument) { + $bindings[$key] = new BoundArgument($binding); + } + } - return $this; - } + $this->bindings = $bindings; - /** - * Will this definition default for the given type? - * - * @param string $type - * - * @return bool - */ - public function hasAutowiringType($type) - { - return isset($this->autowiringTypes[$type]); + return $this; } } diff --git a/src/Symfony/Component/DependencyInjection/DefinitionDecorator.php b/src/Symfony/Component/DependencyInjection/DefinitionDecorator.php deleted file mode 100644 index 4d0f694aa51fe..0000000000000 --- a/src/Symfony/Component/DependencyInjection/DefinitionDecorator.php +++ /dev/null @@ -1,229 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection; - -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; - -/** - * This definition decorates another definition. - * - * @author Johannes M. Schmitt - */ -class DefinitionDecorator extends Definition -{ - private $parent; - private $changes = array(); - - /** - * @param string $parent The id of Definition instance to decorate - */ - public function __construct($parent) - { - parent::__construct(); - - $this->parent = $parent; - } - - /** - * Returns the Definition being decorated. - * - * @return string - */ - public function getParent() - { - return $this->parent; - } - - /** - * Returns all changes tracked for the Definition object. - * - * @return array An array of changes for this Definition - */ - public function getChanges() - { - return $this->changes; - } - - /** - * {@inheritdoc} - */ - public function setClass($class) - { - $this->changes['class'] = true; - - return parent::setClass($class); - } - - /** - * {@inheritdoc} - */ - public function setFactory($callable) - { - $this->changes['factory'] = true; - - return parent::setFactory($callable); - } - - /** - * {@inheritdoc} - */ - public function setFactoryClass($class) - { - $this->changes['factory_class'] = true; - - return parent::setFactoryClass($class); - } - - /** - * {@inheritdoc} - */ - public function setFactoryMethod($method) - { - $this->changes['factory_method'] = true; - - return parent::setFactoryMethod($method); - } - - /** - * {@inheritdoc} - */ - public function setFactoryService($service, $triggerDeprecationError = true) - { - $this->changes['factory_service'] = true; - - return parent::setFactoryService($service, $triggerDeprecationError); - } - - /** - * {@inheritdoc} - */ - public function setConfigurator($callable) - { - $this->changes['configurator'] = true; - - return parent::setConfigurator($callable); - } - - /** - * {@inheritdoc} - */ - public function setFile($file) - { - $this->changes['file'] = true; - - return parent::setFile($file); - } - - /** - * {@inheritdoc} - */ - public function setPublic($boolean) - { - $this->changes['public'] = true; - - return parent::setPublic($boolean); - } - - /** - * {@inheritdoc} - */ - public function setLazy($boolean) - { - $this->changes['lazy'] = true; - - return parent::setLazy($boolean); - } - - /** - * {@inheritdoc} - */ - public function setDecoratedService($id, $renamedId = null, $priority = 0) - { - $this->changes['decorated_service'] = true; - - return parent::setDecoratedService($id, $renamedId, $priority); - } - - /** - * {@inheritdoc} - */ - public function setDeprecated($boolean = true, $template = null) - { - $this->changes['deprecated'] = true; - - return parent::setDeprecated($boolean, $template); - } - - /** - * {@inheritdoc} - */ - public function setAutowired($autowired) - { - $this->changes['autowire'] = true; - - return parent::setAutowired($autowired); - } - - /** - * Gets an argument to pass to the service constructor/factory method. - * - * If replaceArgument() has been used to replace an argument, this method - * will return the replacement value. - * - * @param int $index - * - * @return mixed The argument value - * - * @throws OutOfBoundsException When the argument does not exist - */ - public function getArgument($index) - { - if (array_key_exists('index_'.$index, $this->arguments)) { - return $this->arguments['index_'.$index]; - } - - $lastIndex = count(array_filter(array_keys($this->arguments), 'is_int')) - 1; - - if ($index < 0 || $index > $lastIndex) { - throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [0, %d].', $index, $lastIndex)); - } - - return $this->arguments[$index]; - } - - /** - * You should always use this method when overwriting existing arguments - * of the parent definition. - * - * If you directly call setArguments() keep in mind that you must follow - * certain conventions when you want to overwrite the arguments of the - * parent definition, otherwise your arguments will only be appended. - * - * @param int $index - * @param mixed $value - * - * @return $this - * - * @throws InvalidArgumentException when $index isn't an integer - */ - public function replaceArgument($index, $value) - { - if (!is_int($index)) { - throw new InvalidArgumentException('$index must be an integer.'); - } - - $this->arguments['index_'.$index] = $value; - - return $this; - } -} diff --git a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php index 4172b3d7ea839..57a6e5c100b9a 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php @@ -11,14 +11,13 @@ namespace Symfony\Component\DependencyInjection\Dumper; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Parameter; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; -use Symfony\Component\DependencyInjection\Scope; /** * GraphvizDumper dumps a service container as a graphviz file. @@ -83,7 +82,7 @@ public function dump(array $options = array()) } } - return $this->startDot().$this->addNodes().$this->addEdges().$this->endDot(); + return $this->container->resolveEnvPlaceholders($this->startDot().$this->addNodes().$this->addEdges().$this->endDot(), '__ENV_%s__'); } /** @@ -113,7 +112,7 @@ private function addEdges() $code = ''; foreach ($this->edges as $id => $edges) { foreach ($edges as $edge) { - $code .= sprintf(" node_%s -> node_%s [label=\"%s\" style=\"%s\"];\n", $this->dotize($id), $this->dotize($edge['to']), $edge['name'], $edge['required'] ? 'filled' : 'dashed'); + $code .= sprintf(" node_%s -> node_%s [label=\"%s\" style=\"%s\"%s];\n", $this->dotize($id), $this->dotize($edge['to']), $edge['name'], $edge['required'] ? 'filled' : 'dashed', $edge['lazy'] ? ' color="#9999ff"' : ''); } } @@ -130,7 +129,7 @@ private function addEdges() * * @return array An array of edges */ - private function findEdges($id, array $arguments, $required, $name) + private function findEdges($id, array $arguments, $required, $name, $lazy = false) { $edges = array(); foreach ($arguments as $argument) { @@ -141,13 +140,19 @@ private function findEdges($id, array $arguments, $required, $name) } if ($argument instanceof Reference) { + $lazyEdge = $lazy; + if (!$this->container->has((string) $argument)) { $this->nodes[(string) $argument] = array('name' => $name, 'required' => $required, 'class' => '', 'attributes' => $this->options['node.missing']); + } elseif ('service_container' !== (string) $argument) { + $lazyEdge = $lazy || $this->container->getDefinition((string) $argument)->isLazy(); } - $edges[] = array('name' => $name, 'required' => $required, 'to' => $argument); + $edges[] = array('name' => $name, 'required' => $required, 'to' => $argument, 'lazy' => $lazyEdge); + } elseif ($argument instanceof ArgumentInterface) { + $edges = array_merge($edges, $this->findEdges($id, $argument->getValues(), $required, $name, true)); } elseif (is_array($argument)) { - $edges = array_merge($edges, $this->findEdges($id, $argument, $required, $name)); + $edges = array_merge($edges, $this->findEdges($id, $argument, $required, $name, $lazy)); } } @@ -177,20 +182,17 @@ private function findNodes() } catch (ParameterNotFoundException $e) { } - $nodes[$id] = array('class' => str_replace('\\', '\\\\', $class), 'attributes' => array_merge($this->options['node.definition'], array('style' => $definition->isShared() && ContainerInterface::SCOPE_PROTOTYPE !== $definition->getScope(false) ? 'filled' : 'dotted'))); + $nodes[$id] = array('class' => str_replace('\\', '\\\\', $class), 'attributes' => array_merge($this->options['node.definition'], array('style' => $definition->isShared() ? 'filled' : 'dotted'))); $container->setDefinition($id, new Definition('stdClass')); } foreach ($container->getServiceIds() as $id) { - $service = $container->get($id); - if (array_key_exists($id, $container->getAliases())) { continue; } if (!$container->hasDefinition($id)) { - $class = ('service_container' === $id) ? get_class($this->container) : get_class($service); - $nodes[$id] = array('class' => str_replace('\\', '\\\\', $class), 'attributes' => $this->options['node.instance']); + $nodes[$id] = array('class' => str_replace('\\', '\\\\', get_class($container->get($id))), 'attributes' => $this->options['node.instance']); } } @@ -205,9 +207,6 @@ private function cloneContainer() $container->setDefinitions($this->container->getDefinitions()); $container->setAliases($this->container->getAliases()); $container->setResources($this->container->getResources()); - foreach ($this->container->getScopes(false) as $scope => $parentScope) { - $container->addScope(new Scope($scope, $parentScope)); - } foreach ($this->container->getExtensions() as $extension) { $container->registerExtension($extension); } @@ -282,7 +281,7 @@ private function addOptions(array $options) */ private function dotize($id) { - return strtolower(preg_replace('/\W/i', '_', $id)); + return preg_replace('/\W/i', '_', $id); } /** diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index aea4a9121b703..091bb91a635ab 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -11,21 +11,26 @@ namespace Symfony\Component\DependencyInjection\Dumper; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\Parameter; +use Symfony\Component\DependencyInjection\Exception\EnvParameterException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper; use Symfony\Component\DependencyInjection\ExpressionLanguage; use Symfony\Component\ExpressionLanguage\Expression; -use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\HttpKernel\Kernel; /** @@ -59,11 +64,10 @@ class PhpDumper extends Dumper private $targetDirRegex; private $targetDirMaxMatches; private $docStar; - - /** - * @var ExpressionFunctionProviderInterface[] - */ - private $expressionLanguageProviders = array(); + private $serviceIdToMethodNameMap; + private $usedMethodNames; + private $namespace; + private $asFiles; /** * @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface @@ -75,6 +79,10 @@ class PhpDumper extends Dumper */ public function __construct(ContainerBuilder $container) { + if (!$container->isCompiled()) { + throw new LogicException('Cannot dump an uncompiled container.'); + } + parent::__construct($container); $this->inlinedDefinitions = new \SplObjectStorage(); @@ -98,10 +106,13 @@ public function setProxyDumper(ProxyDumper $proxyDumper) * * class: The class name * * base_class: The base class name * * namespace: The class namespace + * * as_files: To split the container in several files * * @param array $options An array of options * - * @return string A PHP class representing of the service container + * @return string|array A PHP class representing the service container or an array of PHP files if the "as_files" option is set + * + * @throws EnvParameterException When an env var exists but has not been dumped */ public function dump(array $options = array()) { @@ -110,8 +121,14 @@ public function dump(array $options = array()) 'class' => 'ProjectServiceContainer', 'base_class' => 'Container', 'namespace' => '', + 'as_files' => false, 'debug' => true, ), $options); + + $this->namespace = $options['namespace']; + $this->asFiles = $options['as_files']; + $this->initializeMethodNamesMap($options['base_class']); + $this->docStar = $options['debug'] ? '*' : ''; if (!empty($options['file']) && is_dir($dir = dirname($options['file']))) { @@ -139,24 +156,74 @@ public function dump(array $options = array()) } } - $code = $this->startClass($options['class'], $options['base_class'], $options['namespace']); + $code = + $this->startClass($options['class'], $options['base_class']). + $this->addServices(). + $this->addDefaultParametersMethod(). + $this->endClass() + ; + + if ($this->asFiles) { + $fileStart = <<generateServiceFiles() as $file => $c) { + $files[$file] = $fileStart.$c; + } + foreach ($this->generateProxyClasses() as $file => $c) { + $files[$file] = " $c) { + $code["Container{$hash}/{$file}"] = $c; + } + array_pop($code); + $code["Container{$hash}/Container.php"] = implode("\nclass Container{$hash}", explode("\nclass {$options['class']}", $files['Container.php'], 2)); + $namespaceLine = $this->namespace ? "\nnamespace {$this->namespace};\n" : ''; + + $code[$options['class'].'.php'] = <<container->isFrozen()) { - $code .= $this->addFrozenConstructor(); - $code .= $this->addFrozenCompile(); - $code .= $this->addIsFrozenMethod(); +EOF; } else { - $code .= $this->addConstructor(); + foreach ($this->generateProxyClasses() as $c) { + $code .= $c; + } } - $code .= - $this->addServices(). - $this->addDefaultParametersMethod(). - $this->endClass(). - $this->addProxyClasses() - ; $this->targetDirRegex = null; + $unusedEnvs = array(); + foreach ($this->container->getEnvCounters() as $env => $use) { + if (!$use) { + $unusedEnvs[] = $env; + } + } + if ($unusedEnvs) { + throw new EnvParameterException($unusedEnvs, null, 'Environment variables "%s" are never used. Please, check your container\'s configuration.'); + } + return $code; } @@ -177,22 +244,20 @@ private function getProxyDumper() /** * Generates Service local temp variables. * - * @param string $cId - * @param string $definition + * @param string $cId + * @param Definition $definition + * @param array $inlinedDefinitions * * @return string */ - private function addServiceLocalTempVariables($cId, $definition) + private function addServiceLocalTempVariables($cId, Definition $definition, array $inlinedDefinitions) { static $template = " \$%s = %s;\n"; - $localDefinitions = array_merge( - array($definition), - $this->getInlinedDefinitions($definition) - ); + array_unshift($inlinedDefinitions, $definition); $calls = $behavior = array(); - foreach ($localDefinitions as $iDefinition) { + foreach ($inlinedDefinitions as $iDefinition) { $this->getServiceCallsFromArguments($iDefinition->getArguments(), $calls, $behavior); $this->getServiceCallsFromArguments($iDefinition->getMethodCalls(), $calls, $behavior); $this->getServiceCallsFromArguments($iDefinition->getProperties(), $calls, $behavior); @@ -226,40 +291,38 @@ private function addServiceLocalTempVariables($cId, $definition) } /** - * Generates code for the proxies to be attached after the container class. + * Generates code for the proxies. * * @return string */ - private function addProxyClasses() + private function generateProxyClasses() { - /* @var $definitions Definition[] */ - $definitions = array_filter( - $this->container->getDefinitions(), - array($this->getProxyDumper(), 'isProxyCandidate') - ); - $code = ''; + $definitions = $this->container->getDefinitions(); $strip = '' === $this->docStar && method_exists('Symfony\Component\HttpKernel\Kernel', 'stripComments'); - + $proxyDumper = $this->getProxyDumper(); + ksort($definitions); foreach ($definitions as $definition) { - $proxyCode = "\n".$this->getProxyDumper()->getProxyCode($definition); + if (!$proxyDumper->isProxyCandidate($definition)) { + continue; + } + $proxyCode = "\n".$proxyDumper->getProxyCode($definition); if ($strip) { $proxyCode = " $proxyCode; } - - return $code; } /** * Generates the require_once statement for service includes. * * @param Definition $definition + * @param array $inlinedDefinitions * * @return string */ - private function addServiceInclude($definition) + private function addServiceInclude(Definition $definition, array $inlinedDefinitions) { $template = " require_once %s;\n"; $code = ''; @@ -268,7 +331,7 @@ private function addServiceInclude($definition) $code .= sprintf($template, $this->dumpValue($file)); } - foreach ($this->getInlinedDefinitions($definition) as $definition) { + foreach ($inlinedDefinitions as $definition) { if (null !== $file = $definition->getFile()) { $code .= sprintf($template, $this->dumpValue($file)); } @@ -284,21 +347,20 @@ private function addServiceInclude($definition) /** * Generates the inline definition of a service. * - * @param string $id - * @param Definition $definition + * @param string $id + * @param array $inlinedDefinitions * * @return string * * @throws RuntimeException When the factory definition is incomplete * @throws ServiceCircularReferenceException When a circular reference is detected */ - private function addServiceInlinedDefinitions($id, $definition) + private function addServiceInlinedDefinitions($id, array $inlinedDefinitions) { $code = ''; $variableMap = $this->definitionVariables; $nbOccurrences = new \SplObjectStorage(); $processed = new \SplObjectStorage(); - $inlinedDefinitions = $this->getInlinedDefinitions($definition); foreach ($inlinedDefinitions as $definition) { if (false === $nbOccurrences->contains($definition)) { @@ -331,7 +393,7 @@ private function addServiceInlinedDefinitions($id, $definition) throw new ServiceCircularReferenceException($id, array($id)); } - $code .= $this->addNewInstance($id, $sDefinition, '$'.$name, ' = '); + $code .= $this->addNewInstance($sDefinition, '$'.$name, ' = ', $id); if (!$this->hasReference($id, $sDefinition->getMethodCalls(), true) && !$this->hasReference($id, $sDefinition->getProperties(), true)) { $code .= $this->addServiceProperties($sDefinition, $name); @@ -346,64 +408,45 @@ private function addServiceInlinedDefinitions($id, $definition) return $code; } - /** - * Adds the service return statement. - * - * @param string $id Service id - * @param Definition $definition - * - * @return string - */ - private function addServiceReturn($id, $definition) - { - if ($this->isSimpleInstance($id, $definition)) { - return " }\n"; - } - - return "\n return \$instance;\n }\n"; - } - /** * Generates the service instance. * * @param string $id * @param Definition $definition + * @param bool $isSimpleInstance * * @return string * * @throws InvalidArgumentException * @throws RuntimeException */ - private function addServiceInstance($id, Definition $definition) + private function addServiceInstance($id, Definition $definition, $isSimpleInstance) { $class = $this->dumpValue($definition->getClass()); - if (0 === strpos($class, "'") && !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) { + if (0 === strpos($class, "'") && false === strpos($class, '$') && !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) { throw new InvalidArgumentException(sprintf('"%s" is not a valid class name for the "%s" service.', $class, $id)); } - $simple = $this->isSimpleInstance($id, $definition); $isProxyCandidate = $this->getProxyDumper()->isProxyCandidate($definition); $instantiation = ''; - if (!$isProxyCandidate && $definition->isShared() && ContainerInterface::SCOPE_CONTAINER === $definition->getScope(false)) { - $instantiation = "\$this->services['$id'] = ".($simple ? '' : '$instance'); - } elseif (!$isProxyCandidate && $definition->isShared() && ContainerInterface::SCOPE_PROTOTYPE !== $scope = $definition->getScope(false)) { - $instantiation = "\$this->services['$id'] = \$this->scopedServices['$scope']['$id'] = ".($simple ? '' : '$instance'); - } elseif (!$simple) { + if (!$isProxyCandidate && $definition->isShared()) { + $instantiation = sprintf('$this->%s[\'%s\'] = %s', $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates', $id, $isSimpleInstance ? '' : '$instance'); + } elseif (!$isSimpleInstance) { $instantiation = '$instance'; } $return = ''; - if ($simple) { + if ($isSimpleInstance) { $return = 'return '; } else { $instantiation .= ' = '; } - $code = $this->addNewInstance($id, $definition, $return, $instantiation); + $code = $this->addNewInstance($definition, $return, $instantiation, $id); - if (!$simple) { + if (!$isSimpleInstance) { $code .= "\n"; } @@ -433,6 +476,50 @@ private function isSimpleInstance($id, Definition $definition) return true; } + /** + * Checks if the definition is a trivial instance. + * + * @param Definition $definition + * + * @return bool + */ + private function isTrivialInstance(Definition $definition) + { + if ($definition->isPublic() || $definition->getMethodCalls() || $definition->getProperties() || $definition->getConfigurator()) { + return false; + } + if ($definition->isDeprecated() || $definition->isLazy() || $definition->getFactory() || 3 < count($definition->getArguments())) { + return false; + } + + foreach ($definition->getArguments() as $arg) { + if (!$arg || ($arg instanceof Reference && 'service_container' !== (string) $arg)) { + continue; + } + if (is_array($arg) && 3 >= count($arg)) { + foreach ($arg as $k => $v) { + if ($this->dumpValue($k) !== $this->dumpValue($k, false)) { + return false; + } + if (!$v || ($v instanceof Reference && 'service_container' !== (string) $v)) { + continue; + } + if (!is_scalar($v) || $this->dumpValue($v) !== $this->dumpValue($v, false)) { + return false; + } + } + } elseif (!is_scalar($arg) || $this->dumpValue($arg) !== $this->dumpValue($arg, false)) { + return false; + } + } + + if (false !== strpos($this->dumpLiteralClass($this->dumpValue($definition->getClass())), '$')) { + return false; + } + + return true; + } + /** * Adds method calls to a service definition. * @@ -469,20 +556,21 @@ private function addServiceProperties(Definition $definition, $variableName = 'i /** * Generates the inline definition setup. * - * @param string $id - * @param Definition $definition + * @param string $id + * @param array $inlinedDefinitions + * @param bool $isSimpleInstance * * @return string * * @throws ServiceCircularReferenceException when the container contains a circular reference */ - private function addServiceInlinedDefinitionsSetup($id, Definition $definition) + private function addServiceInlinedDefinitionsSetup($id, array $inlinedDefinitions, $isSimpleInstance) { $this->referenceVariables[$id] = new Variable('instance'); $code = ''; $processed = new \SplObjectStorage(); - foreach ($this->getInlinedDefinitions($definition) as $iDefinition) { + foreach ($inlinedDefinitions as $iDefinition) { if ($processed->contains($iDefinition)) { continue; } @@ -494,7 +582,7 @@ private function addServiceInlinedDefinitionsSetup($id, Definition $definition) // if the instance is simple, the return statement has already been generated // so, the only possible way to get there is because of a circular reference - if ($this->isSimpleInstance($id, $definition)) { + if ($isSimpleInstance) { throw new ServiceCircularReferenceException($id, array($id)); } @@ -532,12 +620,16 @@ private function addServiceConfigurator(Definition $definition, $variableName = } $class = $this->dumpValue($callable[0]); - // If the class is a string we can optimize call_user_func away - if (strpos($class, "'") === 0) { + // If the class is a string we can optimize away + if (0 === strpos($class, "'") && false === strpos($class, '$')) { return sprintf(" %s::%s(\$%s);\n", $this->dumpLiteralClass($class), $callable[1], $variableName); } - return sprintf(" call_user_func(array(%s, '%s'), \$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName); + if (0 === strpos($class, 'new ')) { + return sprintf(" (%s)->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName); + } + + return sprintf(" [%s, '%s'](\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName); } return sprintf(" %s(\$%s);\n", $callable, $variableName); @@ -548,10 +640,11 @@ private function addServiceConfigurator(Definition $definition, $variableName = * * @param string $id * @param Definition $definition + * @param string &$file * * @return string */ - private function addService($id, Definition $definition) + private function addService($id, Definition $definition, &$file = null) { $this->definitionVariables = new \SplObjectStorage(); $this->referenceVariables = array(); @@ -559,9 +652,8 @@ private function addService($id, Definition $definition) $return = array(); - if ($definition->isSynthetic()) { - $return[] = '@throws RuntimeException always since this service is expected to be injected dynamically'; - } elseif ($class = $definition->getClass()) { + if ($class = $definition->getClass()) { + $class = $this->container->resolveEnvPlaceholders($class); $return[] = sprintf(0 === strpos($class, '%') ? '@return object A %1$s instance' : '@return \%s', ltrim($class, '\\')); } elseif ($definition->getFactory()) { $factory = $definition->getFactory(); @@ -574,18 +666,6 @@ private function addService($id, Definition $definition) $return[] = sprintf('@return object An instance returned by %s::%s()', $factory[0]->getClass(), $factory[1]); } } - } elseif ($definition->getFactoryClass(false)) { - $return[] = sprintf('@return object An instance returned by %s::%s()', $definition->getFactoryClass(false), $definition->getFactoryMethod(false)); - } elseif ($definition->getFactoryService(false)) { - $return[] = sprintf('@return object An instance returned by %s::%s()', $definition->getFactoryService(false), $definition->getFactoryMethod(false)); - } - - $scope = $definition->getScope(false); - if (!in_array($scope, array(ContainerInterface::SCOPE_CONTAINER, ContainerInterface::SCOPE_PROTOTYPE))) { - if ($return && 0 === strpos($return[count($return) - 1], '@return')) { - $return[] = ''; - } - $return[] = sprintf("@throws InactiveScopeException when the '%s' service is requested while the '%s' scope is not active", $id, $scope); } if ($definition->isDeprecated()) { @@ -597,8 +677,9 @@ private function addService($id, Definition $definition) } $return = str_replace("\n * \n", "\n *\n", implode("\n * ", $return)); + $return = $this->container->resolveEnvPlaceholders($return); - $shared = $definition->isShared() && ContainerInterface::SCOPE_PROTOTYPE !== $scope ? ' shared' : ''; + $shared = $definition->isShared() ? ' shared' : ''; $public = $definition->isPublic() ? 'public' : 'private'; $autowired = $definition->isAutowired() ? ' autowired' : ''; @@ -608,51 +689,53 @@ private function addService($id, Definition $definition) $lazyInitialization = ''; } - // with proxies, for 5.3.3 compatibility, the getter must be public to be accessible to the initializer - $isProxyCandidate = $this->getProxyDumper()->isProxyCandidate($definition); - $visibility = $isProxyCandidate ? 'public' : 'protected'; - $code = <<asFiles && $definition->isShared(); + $methodName = $this->generateMethodName($id); + if ($asFile) { + $file = $methodName.'.php'; + $code = " // Returns the $public '$id'$shared$autowired service.\n\n"; + } else { + $code = <<docStar} * Gets the $public '$id'$shared$autowired service. * * $return */ - {$visibility} function get{$this->camelize($id)}Service($lazyInitialization) + protected function {$methodName}($lazyInitialization) { EOF; + } - $code .= $isProxyCandidate ? $this->getProxyDumper()->getProxyFactoryCode($definition, $id) : ''; + if ($this->getProxyDumper()->isProxyCandidate($definition)) { + $factoryCode = $asFile ? "\$this->load(__DIR__.'/%s.php', false)" : '$this->%s(false)'; + $code .= $this->getProxyDumper()->getProxyFactoryCode($definition, $id, sprintf($factoryCode, $methodName)); + } - if (!in_array($scope, array(ContainerInterface::SCOPE_CONTAINER, ContainerInterface::SCOPE_PROTOTYPE))) { - $code .= <<scopedServices['$scope'])) { - throw new InactiveScopeException('$id', '$scope'); + if ($definition->isDeprecated()) { + $code .= sprintf(" @trigger_error(%s, E_USER_DEPRECATED);\n\n", $this->export($definition->getDeprecationMessage($id))); } + $inlinedDefinitions = $this->getInlinedDefinitions($definition); + $isSimpleInstance = $this->isSimpleInstance($id, $definition, $inlinedDefinitions); -EOF; - } + $code .= + $this->addServiceInclude($definition, $inlinedDefinitions). + $this->addServiceLocalTempVariables($id, $definition, $inlinedDefinitions). + $this->addServiceInlinedDefinitions($id, $inlinedDefinitions). + $this->addServiceInstance($id, $definition, $isSimpleInstance). + $this->addServiceInlinedDefinitionsSetup($id, $inlinedDefinitions, $isSimpleInstance). + $this->addServiceProperties($definition). + $this->addServiceMethodCalls($definition). + $this->addServiceConfigurator($definition). + (!$isSimpleInstance ? "\n return \$instance;\n" : '') + ; - if ($definition->isSynthetic()) { - $code .= sprintf(" throw new RuntimeException('You have requested a synthetic service (\"%s\"). The DIC does not know how to construct this service.');\n }\n", $id); + if ($asFile) { + $code = implode("\n", array_map(function ($line) { return $line ? substr($line, 8) : $line; }, explode("\n", $code))); } else { - if ($definition->isDeprecated()) { - $code .= sprintf(" @trigger_error(%s, E_USER_DEPRECATED);\n\n", var_export($definition->getDeprecationMessage($id), true)); - } - - $code .= - $this->addServiceInclude($definition). - $this->addServiceLocalTempVariables($id, $definition). - $this->addServiceInlinedDefinitions($id, $definition). - $this->addServiceInstance($id, $definition). - $this->addServiceInlinedDefinitionsSetup($id, $definition). - $this->addServiceProperties($definition). - $this->addServiceMethodCalls($definition). - $this->addServiceConfigurator($definition). - $this->addServiceReturn($id, $definition) - ; + $code .= " }\n"; } $this->definitionVariables = null; @@ -668,82 +751,36 @@ private function addService($id, Definition $definition) */ private function addServices() { - $publicServices = $privateServices = $synchronizers = ''; + $publicServices = $privateServices = ''; $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { + if ($definition->isSynthetic() || ($this->asFiles && $definition->isShared())) { + continue; + } if ($definition->isPublic()) { $publicServices .= $this->addService($id, $definition); - } else { + } elseif (!$this->isTrivialInstance($definition)) { $privateServices .= $this->addService($id, $definition); } - - $synchronizers .= $this->addServiceSynchronizer($id, $definition); } - return $publicServices.$synchronizers.$privateServices; + return $publicServices.$privateServices; } - /** - * Adds synchronizer methods. - * - * @param string $id A service identifier - * @param Definition $definition A Definition instance - * - * @return string|null - * - * @deprecated since version 2.7, will be removed in 3.0. - */ - private function addServiceSynchronizer($id, Definition $definition) + private function generateServiceFiles() { - if (!$definition->isSynchronized(false)) { - return; - } - - if ('request' !== $id) { - @trigger_error('Synchronized services were deprecated in version 2.7 and won\'t work anymore in 3.0.', E_USER_DEPRECATED); - } - - $code = ''; - foreach ($this->container->getDefinitions() as $definitionId => $definition) { - foreach ($definition->getMethodCalls() as $call) { - foreach ($call[1] as $argument) { - if ($argument instanceof Reference && $id == (string) $argument) { - $arguments = array(); - foreach ($call[1] as $value) { - $arguments[] = $this->dumpValue($value); - } - - $call = $this->wrapServiceConditionals($call[1], sprintf("\$this->get('%s')->%s(%s);", $definitionId, $call[0], implode(', ', $arguments))); - - $code .= <<initialized('$definitionId')) { - $call - } - -EOF; - } - } + $definitions = $this->container->getDefinitions(); + ksort($definitions); + foreach ($definitions as $id => $definition) { + if (!$definition->isSynthetic() && $definition->isShared()) { + $code = $this->addService($id, $definition, $file); + yield $file => $code; } } - - if (!$code) { - return; - } - - return <<docStar} - * Updates the '$id' service. - */ - protected function synchronize{$this->camelize($id)}Service() - { -$code } - -EOF; } - private function addNewInstance($id, Definition $definition, $return, $instantiation) + private function addNewInstance(Definition $definition, $return, $instantiation, $id) { $class = $this->dumpValue($definition->getClass()); @@ -765,32 +802,23 @@ private function addNewInstance($id, Definition $definition, $return, $instantia } $class = $this->dumpValue($callable[0]); - // If the class is a string we can optimize call_user_func away - if (strpos($class, "'") === 0) { + // If the class is a string we can optimize away + if (0 === strpos($class, "'") && false === strpos($class, '$')) { + if ("''" === $class) { + throw new RuntimeException(sprintf('Cannot dump definition: The "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id)); + } + return sprintf(" $return{$instantiation}%s::%s(%s);\n", $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : ''); } - return sprintf(" $return{$instantiation}call_user_func(array(%s, '%s')%s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? ', '.implode(', ', $arguments) : ''); - } - - return sprintf(" $return{$instantiation}\\%s(%s);\n", $callable, $arguments ? implode(', ', $arguments) : ''); - } elseif (null !== $definition->getFactoryMethod(false)) { - if (null !== $definition->getFactoryClass(false)) { - $class = $this->dumpValue($definition->getFactoryClass(false)); - - // If the class is a string we can optimize call_user_func away - if (strpos($class, "'") === 0) { - return sprintf(" $return{$instantiation}%s::%s(%s);\n", $this->dumpLiteralClass($class), $definition->getFactoryMethod(false), $arguments ? implode(', ', $arguments) : ''); + if (0 === strpos($class, 'new ')) { + return sprintf(" $return{$instantiation}(%s)->%s(%s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : ''); } - return sprintf(" $return{$instantiation}call_user_func(array(%s, '%s')%s);\n", $this->dumpValue($definition->getFactoryClass(false)), $definition->getFactoryMethod(false), $arguments ? ', '.implode(', ', $arguments) : ''); + return sprintf(" $return{$instantiation}[%s, '%s'](%s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : ''); } - if (null !== $definition->getFactoryService(false)) { - return sprintf(" $return{$instantiation}%s->%s(%s);\n", $this->getServiceCall($definition->getFactoryService(false)), $definition->getFactoryMethod(false), implode(', ', $arguments)); - } - - throw new RuntimeException(sprintf('Factory method requires a factory service or factory class in service definition for %s', $id)); + return sprintf(" $return{$instantiation}%s(%s);\n", $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : ''); } if (false !== strpos($class, '$')) { @@ -805,165 +833,135 @@ private function addNewInstance($id, Definition $definition, $return, $instantia * * @param string $class Class name * @param string $baseClass The name of the base class - * @param string $namespace The class namespace * * @return string */ - private function startClass($class, $baseClass, $namespace) + private function startClass($class, $baseClass) { - $bagClass = $this->container->isFrozen() ? 'use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;' : 'use Symfony\Component\DependencyInjection\ParameterBag\\ParameterBag;'; - $namespaceLine = $namespace ? "\nnamespace $namespace;\n" : ''; + $namespaceLine = $this->namespace ? "\nnamespace {$this->namespace};\n" : ''; - return <<docStar} * $class. * * This class has been auto-generated * by the Symfony Dependency Injection Component. + * + * @final since Symfony 3.3 */ class $class extends $baseClass { private \$parameters; private \$targetDirs = array(); - -EOF; - } - - /** - * Adds the constructor. - * - * @return string - */ - private function addConstructor() - { - $targetDirs = $this->exportTargetDirs(); - $arguments = $this->container->getParameterBag()->all() ? 'new ParameterBag($this->getDefaultParameters())' : null; - - $code = <<docStar} * Constructor. */ public function __construct() - {{$targetDirs} - parent::__construct($arguments); - -EOF; - - if (count($scopes = $this->container->getScopes(false)) > 0) { - $code .= "\n"; - $code .= ' $this->scopes = '.$this->dumpValue($scopes).";\n"; - $code .= ' $this->scopeChildren = '.$this->dumpValue($this->container->getScopeChildren(false)).";\n"; - } - - $code .= $this->addMethodMap(); - $code .= $this->addAliases(); - - $code .= <<<'EOF' - } - -EOF; - - return $code; - } - - /** - * Adds the constructor for a frozen container. - * - * @return string - */ - private function addFrozenConstructor() { - $targetDirs = $this->exportTargetDirs(); - $code = <<docStar} - * Constructor. - */ - public function __construct() - {{$targetDirs} EOF; - - if ($this->container->getParameterBag()->all()) { - $code .= "\n \$this->parameters = \$this->getDefaultParameters();\n"; + if (null !== $this->targetDirRegex) { + $dir = $this->asFiles ? '$this->targetDirs[0] = dirname(__DIR__)' : '__DIR__'; + $code .= <<targetDirMaxMatches}; ++\$i) { + \$this->targetDirs[\$i] = \$dir = dirname(\$dir); } - $code .= <<<'EOF' - - $this->services = - $this->scopedServices = - $this->scopeStacks = array(); EOF; + } - $code .= "\n"; - if (count($scopes = $this->container->getScopes(false)) > 0) { - $code .= ' $this->scopes = '.$this->dumpValue($scopes).";\n"; - $code .= ' $this->scopeChildren = '.$this->dumpValue($this->container->getScopeChildren(false)).";\n"; - } else { - $code .= " \$this->scopes = array();\n"; - $code .= " \$this->scopeChildren = array();\n"; + if ($this->container->getParameterBag()->all()) { + $code .= " \$this->parameters = \$this->getDefaultParameters();\n\n"; } + $code .= " \$this->services = \$this->privates = array();\n"; $code .= $this->addMethodMap(); + $code .= $this->asFiles ? $this->addFileMap() : ''; $code .= $this->addAliases(); - - $code .= <<<'EOF' + $code .= <<docStar} + * {@inheritdoc} + */ + public function reset() + { + \$this->privates = array(); + parent::reset(); } - /** - * Adds the constructor for a frozen container. - * - * @return string + /*{$this->docStar} + * {@inheritdoc} */ - private function addFrozenCompile() + public function compile() { - return <<docStar} * {@inheritdoc} */ - public function compile() + public function isCompiled() { - throw new LogicException('You cannot compile a dumped frozen container.'); + return true; } EOF; - } - /** - * Adds the isFrozen method for a frozen container. - * - * @return string - */ - private function addIsFrozenMethod() - { - return <<asFiles) { + $code .= <<docStar} * {@inheritdoc} */ - public function isFrozen() + protected function load(\$file, \$lazyLoad = true) { - return true; + return require \$file; } EOF; + } + + $proxyDumper = $this->getProxyDumper(); + foreach ($this->container->getDefinitions() as $definition) { + if (!$proxyDumper->isProxyCandidate($definition)) { + continue; + } + if ($this->asFiles) { + $proxyLoader = '$this->load(__DIR__."/{$class}.php")'; + } elseif ($this->namespace) { + $proxyLoader = 'class_alias("'.$this->namespace.'\\\\{$class}", $class, false)'; + } else { + $proxyLoader = ''; + } + if ($proxyLoader) { + $proxyLoader = "class_exists(\$class, false) || {$proxyLoader};\n\n "; + } + $code .= <<container->getDefinitions()) { - return ''; + $code = ''; + $definitions = $this->container->getDefinitions(); + ksort($definitions); + foreach ($definitions as $id => $definition) { + if (!$definition->isSynthetic() && $definition->isPublic() && (!$this->asFiles || !$definition->isShared())) { + $code .= ' '.$this->export($id).' => '.$this->export($this->generateMethodName($id)).",\n"; + } } - $code = " \$this->methodMap = array(\n"; + return $code ? " \$this->methodMap = array(\n{$code} );\n" : ''; + } + + /** + * Adds the fileMap property definition. + * + * @return string + */ + private function addFileMap() + { + $code = ''; + $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { - $code .= ' '.var_export($id, true).' => '.var_export('get'.$this->camelize($id).'Service', true).",\n"; + if (!$definition->isSynthetic() && $definition->isPublic() && $definition->isShared()) { + $code .= sprintf(" %s => __DIR__.'/%s.php',\n", $this->export($id), $this->generateMethodName($id)); + } } - return $code." );\n"; + return $code ? " \$this->fileMap = array(\n{$code} );\n" : ''; } /** @@ -994,7 +1010,7 @@ private function addMethodMap() private function addAliases() { if (!$aliases = $this->container->getAliases()) { - return $this->container->isFrozen() ? "\n \$this->aliases = array();\n" : ''; + return "\n \$this->aliases = array();\n"; } $code = " \$this->aliases = array(\n"; @@ -1004,7 +1020,7 @@ private function addAliases() while (isset($aliases[$id])) { $id = (string) $aliases[$id]; } - $code .= ' '.var_export($alias, true).' => '.var_export($id, true).",\n"; + $code .= ' '.$this->export($alias).' => '.$this->export($id).",\n"; } return $code." );\n"; @@ -1021,21 +1037,40 @@ private function addDefaultParametersMethod() return ''; } - $parameters = $this->exportParameters($this->container->getParameterBag()->all()); + $php = array(); + $dynamicPhp = array(); - $code = ''; - if ($this->container->isFrozen()) { - $code .= <<<'EOF' + foreach ($this->container->getParameterBag()->all() as $key => $value) { + if ($key !== $resolvedKey = $this->container->resolveEnvPlaceholders($key)) { + throw new InvalidArgumentException(sprintf('Parameter name cannot use env parameters: %s.', $resolvedKey)); + } + $export = $this->exportParameters(array($value)); + $export = explode('0 => ', substr(rtrim($export, " )\n"), 7, -1), 2); + + if (preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $export[1])) { + $dynamicPhp[$key] = sprintf('%scase %s: $value = %s; break;', $export[0], $this->export($key), $export[1]); + } else { + $php[] = sprintf('%s%s => %s,', $export[0], $this->export($key), $export[1]); + } + } + $parameters = sprintf("array(\n%s\n%s)", implode("\n", $php), str_repeat(' ', 8)); + + $code = <<<'EOF' /** * {@inheritdoc} */ public function getParameter($name) { - $name = strtolower($name); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { - throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); } return $this->parameters[$name]; @@ -1048,7 +1083,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); } /** @@ -1065,20 +1100,57 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; } EOF; - if ('' === $this->docStar) { - $code = str_replace('/**', '/*', $code); - } + if ('' === $this->docStar) { + $code = str_replace('/**', '/*', $code); + } + + if ($dynamicPhp) { + $loadedDynamicParameters = $this->exportParameters(array_combine(array_keys($dynamicPhp), array_fill(0, count($dynamicPhp), false)), '', 8); + $getDynamicParameter = <<<'EOF' + switch ($name) { +%s + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; +EOF; + $getDynamicParameter = sprintf($getDynamicParameter, implode("\n", $dynamicPhp)); + } else { + $loadedDynamicParameters = 'array()'; + $getDynamicParameter = str_repeat(' ', 8).'throw new InvalidArgumentException(sprintf(\'The dynamic parameter "%s" must be defined.\', $name));'; } $code .= <<docStar} + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter(\$name) + { +{$getDynamicParameter} + } + /*{$this->docStar} * Gets the default parameters. * @@ -1111,6 +1183,8 @@ private function exportParameters(array $parameters, $path = '', $indent = 12) foreach ($parameters as $key => $value) { if (is_array($value)) { $value = $this->exportParameters($value, $path.'/'.$key, $indent + 4); + } elseif ($value instanceof ArgumentInterface) { + throw new InvalidArgumentException(sprintf('You cannot dump a container with parameters that contain special arguments. "%s" found in "%s".', get_class($value), $path.'/'.$key)); } elseif ($value instanceof Variable) { throw new InvalidArgumentException(sprintf('You cannot dump a container with parameters that contain variable references. Variable "%s" found in "%s".', $value, $path.'/'.$key)); } elseif ($value instanceof Definition) { @@ -1123,7 +1197,7 @@ private function exportParameters(array $parameters, $path = '', $indent = 12) $value = $this->export($value); } - $php[] = sprintf('%s%s => %s,', str_repeat(' ', $indent), var_export($key, true), $value); + $php[] = sprintf('%s%s => %s,', str_repeat(' ', $indent), $this->export($key), $value); } return sprintf("array(\n%s\n%s)", implode("\n", $php), str_repeat(' ', $indent - 4)); @@ -1152,19 +1226,43 @@ private function endClass() */ private function wrapServiceConditionals($value, $code) { - if (!$services = ContainerBuilder::getServiceConditionals($value)) { + if (!$condition = $this->getServiceConditionals($value)) { return $code; } + // re-indent the wrapped code + $code = implode("\n", array_map(function ($line) { return $line ? ' '.$line : $line; }, explode("\n", $code))); + + return sprintf(" if (%s) {\n%s }\n", $condition, $code); + } + + /** + * Get the conditions to execute for conditional services. + * + * @param string $value + * + * @return null|string + */ + private function getServiceConditionals($value) + { + if (!$services = ContainerBuilder::getServiceConditionals($value)) { + return null; + } + $conditions = array(); foreach ($services as $service) { + if ($this->container->hasDefinition($service) && !$this->container->getDefinition($service)->isPublic()) { + continue; + } + $conditions[] = sprintf("\$this->has('%s')", $service); } - // re-indent the wrapped code - $code = implode("\n", array_map(function ($line) { return $line ? ' '.$line : $line; }, explode("\n", $code))); + if (!$conditions) { + return ''; + } - return sprintf(" if (%s) {\n%s }\n", implode(' && ', $conditions), $code); + return implode(' && ', $conditions); } /** @@ -1312,6 +1410,57 @@ private function dumpValue($value, $interpolate = true) } return sprintf('array(%s)', implode(', ', $code)); + } elseif ($value instanceof ArgumentInterface) { + $scope = array($this->definitionVariables, $this->referenceVariables, $this->variableCount); + $this->definitionVariables = $this->referenceVariables = null; + + try { + if ($value instanceof ServiceClosureArgument) { + $value = $value->getValues()[0]; + $code = $this->dumpValue($value, $interpolate); + + $returnedType = ''; + if ($value instanceof TypedReference) { + $returnedType = sprintf(': %s\%s', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior() ? '' : '?', $value->getType()); + } + + $code = sprintf('return %s;', $code); + + return sprintf("function ()%s {\n %s\n }", $returnedType, $code); + } + + if ($value instanceof IteratorArgument) { + $operands = array(0); + $code = array(); + $code[] = 'new RewindableGenerator(function () {'; + + if (!$values = $value->getValues()) { + $code[] = ' return new \EmptyIterator();'; + } else { + $countCode = array(); + $countCode[] = 'function () {'; + + foreach ($values as $k => $v) { + ($c = $this->getServiceConditionals($v)) ? $operands[] = "(int) ($c)" : ++$operands[0]; + $v = $this->wrapServiceConditionals($v, sprintf(" yield %s => %s;\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate))); + foreach (explode("\n", $v) as $v) { + if ($v) { + $code[] = ' '.$v; + } + } + } + + $countCode[] = sprintf(' return %s;', implode(' + ', $operands)); + $countCode[] = ' }'; + } + + $code[] = sprintf(' }, %s)', count($operands) > 1 ? implode("\n", $countCode) : $operands[0]); + + return implode("\n", $code); + } + } finally { + list($this->definitionVariables, $this->referenceVariables, $this->variableCount) = $scope; + } } elseif ($value instanceof Definition) { if (null !== $this->definitionVariables && $this->definitionVariables->contains($value)) { return $this->dumpValue($this->definitionVariables->offsetGet($value), $interpolate); @@ -1335,7 +1484,7 @@ private function dumpValue($value, $interpolate = true) $factory = $value->getFactory(); if (is_string($factory)) { - return sprintf('\\%s(%s)', $factory, implode(', ', $arguments)); + return sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($factory)), implode(', ', $arguments)); } if (is_array($factory)) { @@ -1348,7 +1497,7 @@ private function dumpValue($value, $interpolate = true) } if ($factory[0] instanceof Definition) { - return sprintf("call_user_func(array(%s, '%s')%s)", $this->dumpValue($factory[0]), $factory[1], count($arguments) > 0 ? ', '.implode(', ', $arguments) : ''); + return sprintf("[%s, '%s'](%s)", $this->dumpValue($factory[0]), $factory[1], implode(', ', $arguments)); } if ($factory[0] instanceof Reference) { @@ -1359,18 +1508,6 @@ private function dumpValue($value, $interpolate = true) throw new RuntimeException('Cannot dump definition because of invalid factory'); } - if (null !== $value->getFactoryMethod(false)) { - if (null !== $value->getFactoryClass(false)) { - return sprintf("call_user_func(array(%s, '%s')%s)", $this->dumpValue($value->getFactoryClass(false)), $value->getFactoryMethod(false), count($arguments) > 0 ? ', '.implode(', ', $arguments) : ''); - } elseif (null !== $value->getFactoryService(false)) { - $service = $this->dumpValue($value->getFactoryService(false)); - - return sprintf('%s->%s(%s)', 0 === strpos($service, '$') ? sprintf('$this->get(%s)', $service) : $this->getServiceCall($value->getFactoryService(false)), $value->getFactoryMethod(false), implode(', ', $arguments)); - } - - throw new RuntimeException('Cannot dump definitions which have factory method without factory service or factory class.'); - } - $class = $value->getClass(); if (null === $class) { throw new RuntimeException('Cannot dump definitions which have no class nor factory.'); @@ -1395,9 +1532,8 @@ private function dumpValue($value, $interpolate = true) // the preg_replace_callback converts them to strings return $this->dumpParameter(strtolower($match[1])); } else { - $that = $this; - $replaceParameters = function ($match) use ($that) { - return "'.".$that->dumpParameter(strtolower($match[2])).".'"; + $replaceParameters = function ($match) { + return "'.".$this->dumpParameter(strtolower($match[2])).".'"; }; $code = str_replace('%%', '%', preg_replace_callback('/(?export($value))); @@ -1423,7 +1559,7 @@ private function dumpValue($value, $interpolate = true) private function dumpLiteralClass($class) { if (false !== strpos($class, '$')) { - throw new RuntimeException('Cannot dump definitions which have a variable class name.'); + return sprintf('${($_ = %s) && false ?: "_"}', $class); } if (0 !== strpos($class, "'") || !preg_match('/^\'(?:\\\{2})?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\{2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*\'$/', $class)) { throw new RuntimeException(sprintf('Cannot dump definition because of invalid class name (%s)', $class ?: 'n/a')); @@ -1441,26 +1577,24 @@ private function dumpLiteralClass($class) * * @return string */ - public function dumpParameter($name) + private function dumpParameter($name) { - if ($this->container->isFrozen() && $this->container->hasParameter($name)) { - return $this->dumpValue($this->container->getParameter($name), false); - } + $name = strtolower($name); - return sprintf("\$this->getParameter('%s')", strtolower($name)); - } + if ($this->container->isCompiled() && $this->container->hasParameter($name)) { + $value = $this->container->getParameter($name); + $dumpedValue = $this->dumpValue($value, false); - /** - * @deprecated since version 2.6.2, to be removed in 3.0. - * Use \Symfony\Component\DependencyInjection\ContainerBuilder::addExpressionLanguageProvider instead. - * - * @param ExpressionFunctionProviderInterface $provider - */ - public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) - { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6.2 and will be removed in 3.0. Use the Symfony\Component\DependencyInjection\ContainerBuilder::addExpressionLanguageProvider method instead.', E_USER_DEPRECATED); + if (!$value || !is_array($value)) { + return $dumpedValue; + } + + if (!preg_match("/\\\$this->(?:getEnv\('\w++'\)|targetDirs\[\d++\])/", $dumpedValue)) { + return sprintf("\$this->parameters['%s']", $name); + } + } - $this->expressionLanguageProviders[] = $provider; + return sprintf("\$this->getParameter('%s')", $name); } /** @@ -1480,12 +1614,45 @@ private function getServiceCall($id, Reference $reference = null) if ('service_container' === $id) { return '$this'; } + + if ($this->container->hasDefinition($id)) { + $definition = $this->container->getDefinition($id); - if (null !== $reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) { - return sprintf('$this->get(\'%s\', ContainerInterface::NULL_ON_INVALID_REFERENCE)', $id); + if ($this->isTrivialInstance($definition)) { + $code = substr($this->addNewInstance($definition, '', '', $id), 8, -2); + $code = sprintf('($this->privates[\'%s\'] = %s)', $id, $code); + } elseif ($this->asFiles && $definition->isShared()) { + $code = sprintf("\$this->load(__DIR__.'/%s.php')", $this->generateMethodName($id)); + } else { + $code = sprintf('$this->%s()', $this->generateMethodName($id)); + } + if ($definition->isShared()) { + $code = sprintf('($this->%s[\'%s\'] ?? %s)', $definition->isPublic() ? 'services' : 'privates', $id, $code); + } + } elseif (null !== $reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) { + $code = sprintf('$this->get(\'%s\', ContainerInterface::NULL_ON_INVALID_REFERENCE)', $id); + } else { + $code = sprintf('$this->get(\'%s\')', $id); } - return sprintf('$this->get(\'%s\')', $id); + return $code; + } + + /** + * Initializes the method names map to avoid conflicts with the Container methods. + * + * @param string $class the container base class + */ + private function initializeMethodNamesMap($class) + { + $this->serviceIdToMethodNameMap = array(); + $this->usedMethodNames = array(); + + if ($reflectionClass = $this->container->getReflectionClass($class)) { + foreach ($reflectionClass->getMethods() as $method) { + $this->usedMethodNames[strtolower($method->getName())] = true; + } + } } /** @@ -1497,15 +1664,27 @@ private function getServiceCall($id, Reference $reference = null) * * @throws InvalidArgumentException */ - private function camelize($id) + private function generateMethodName($id) { - $name = Container::camelize($id); + if (isset($this->serviceIdToMethodNameMap[$id])) { + return $this->serviceIdToMethodNameMap[$id]; + } + + $i = strrpos($id, '\\'); + $name = Container::camelize(false !== $i && isset($id[1 + $i]) ? substr($id, 1 + $i) : $id); + $name = preg_replace('/[^a-zA-Z0-9_\x7f-\xff]/', '', $name); + $methodName = 'get'.$name.'Service'; + $suffix = 1; - if (!preg_match('/^[a-zA-Z0-9_\x7f-\xff]+$/', $name)) { - throw new InvalidArgumentException(sprintf('Service id "%s" cannot be converted to a valid PHP method name.', $id)); + while (isset($this->usedMethodNames[strtolower($methodName)])) { + ++$suffix; + $methodName = 'get'.$name.$suffix.'Service'; } - return $name; + $this->serviceIdToMethodNameMap[$id] = $methodName; + $this->usedMethodNames[strtolower($methodName)] = true; + + return $methodName; } /** @@ -1552,8 +1731,16 @@ private function getExpressionLanguage() if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); } - $providers = array_merge($this->container->getExpressionLanguageProviders(), $this->expressionLanguageProviders); - $this->expressionLanguage = new ExpressionLanguage(null, $providers); + $providers = $this->container->getExpressionLanguageProviders(); + $this->expressionLanguage = new ExpressionLanguage(null, $providers, function ($arg) { + $id = '""' === substr_replace($arg, '', 1, -1) ? stripcslashes(substr($arg, 1, -1)) : null; + + if (null !== $id && ($this->container->hasAlias($id) || $this->container->hasDefinition($id))) { + return $this->getServiceCall($id); + } + + return sprintf('$this->get(%s)', $arg); + }); if ($this->container->isTrackingResources()) { foreach ($providers as $provider) { @@ -1565,26 +1752,16 @@ private function getExpressionLanguage() return $this->expressionLanguage; } - private function exportTargetDirs() - { - return null === $this->targetDirRegex ? '' : <<targetDirMaxMatches}; ++\$i) { - \$this->targetDirs[\$i] = \$dir = dirname(\$dir); - } -EOF; - } - private function export($value) { if (null !== $this->targetDirRegex && is_string($value) && preg_match($this->targetDirRegex, $value, $matches, PREG_OFFSET_CAPTURE)) { - $prefix = $matches[0][1] ? var_export(substr($value, 0, $matches[0][1]), true).'.' : ''; + $prefix = $matches[0][1] ? $this->doExport(substr($value, 0, $matches[0][1])).'.' : ''; $suffix = $matches[0][1] + strlen($matches[0][0]); - $suffix = isset($value[$suffix]) ? '.'.var_export(substr($value, $suffix), true) : ''; + $suffix = isset($value[$suffix]) ? '.'.$this->doExport(substr($value, $suffix)) : ''; $dirname = '__DIR__'; + $offset = 1 + $this->targetDirMaxMatches - count($matches); - if (0 < $offset = 1 + $this->targetDirMaxMatches - count($matches)) { + if ($this->asFiles || 0 < $offset) { $dirname = sprintf('$this->targetDirs[%d]', $offset); } @@ -1595,6 +1772,23 @@ private function export($value) return $dirname; } - return var_export($value, true); + return $this->doExport($value); + } + + private function doExport($value) + { + $export = var_export($value, true); + + if ("'" === $export[0] && $export !== $resolvedExport = $this->container->resolveEnvPlaceholders($export, "'.\$this->getEnv('%s').'")) { + $export = $resolvedExport; + if ("'" === $export[1]) { + $export = substr($export, 3); + } + if (".''" === substr($export, -3)) { + $export = substr($export, 0, -3); + } + } + + return $export; } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index ce7ec49d80c04..ea8de6e8c5d1e 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Dumper; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; @@ -55,7 +57,7 @@ public function dump(array $options = array()) $xml = $this->document->saveXML(); $this->document = null; - return $xml; + return $this->container->resolveEnvPlaceholders($xml); } /** @@ -70,7 +72,7 @@ private function addParameters(\DOMElement $parent) return; } - if ($this->container->isFrozen()) { + if ($this->container->isCompiled()) { $data = $this->escape($data); } @@ -117,30 +119,15 @@ private function addService($definition, $id, \DOMElement $parent) $service->setAttribute('class', $class); } - if ($definition->getFactoryMethod(false)) { - $service->setAttribute('factory-method', $definition->getFactoryMethod(false)); - } - if ($definition->getFactoryClass(false)) { - $service->setAttribute('factory-class', $definition->getFactoryClass(false)); - } - if ($definition->getFactoryService(false)) { - $service->setAttribute('factory-service', $definition->getFactoryService(false)); - } if (!$definition->isShared()) { $service->setAttribute('shared', 'false'); } - if (ContainerInterface::SCOPE_CONTAINER !== $scope = $definition->getScope(false)) { - $service->setAttribute('scope', $scope); - } if (!$definition->isPublic()) { $service->setAttribute('public', 'false'); } if ($definition->isSynthetic()) { $service->setAttribute('synthetic', 'true'); } - if ($definition->isSynchronized(false)) { - $service->setAttribute('synchronized', 'true'); - } if ($definition->isLazy()) { $service->setAttribute('lazy', 'true'); } @@ -189,7 +176,9 @@ private function addService($definition, $id, \DOMElement $parent) $this->addService($callable[0], null, $factory); $factory->setAttribute('method', $callable[1]); } elseif (is_array($callable)) { - $factory->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]); + if (null !== $callable[0]) { + $factory->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]); + } $factory->setAttribute('method', $callable[1]); } else { $factory->setAttribute('function', $callable); @@ -208,11 +197,8 @@ private function addService($definition, $id, \DOMElement $parent) $service->setAttribute('autowire', 'true'); } - foreach ($definition->getAutowiringTypes() as $autowiringTypeValue) { - $autowiringType = $this->document->createElement('autowiring-type'); - $autowiringType->appendChild($this->document->createTextNode($autowiringTypeValue)); - - $service->appendChild($autowiringType); + if ($definition->isAutoconfigured()) { + $service->setAttribute('autoconfigure', 'true'); } if ($definition->isAbstract()) { @@ -299,9 +285,15 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent $element->setAttribute($keyAttribute, $key); } + if ($value instanceof ServiceClosureArgument) { + $value = $value->getValues()[0]; + } if (is_array($value)) { $element->setAttribute('type', 'collection'); $this->convertParameters($value, $type, $element, 'key'); + } elseif ($value instanceof IteratorArgument) { + $element->setAttribute('type', 'iterator'); + $this->convertParameters($value->getValues(), $type, $element, 'key'); } elseif ($value instanceof Reference) { $element->setAttribute('type', 'service'); $element->setAttribute('id', (string) $value); @@ -311,9 +303,6 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent } elseif ($behaviour == ContainerInterface::IGNORE_ON_INVALID_REFERENCE) { $element->setAttribute('on-invalid', 'ignore'); } - if (!$value->isStrict(false)) { - $element->setAttribute('strict', 'false'); - } } elseif ($value instanceof Definition) { $element->setAttribute('type', 'service'); $this->addService($value, null, $element); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index 9e59ba32e28c6..4c5890e59898a 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -12,7 +12,13 @@ namespace Symfony\Component\DependencyInjection\Dumper; use Symfony\Component\Yaml\Dumper as YmlDumper; +use Symfony\Component\Yaml\Parser; +use Symfony\Component\Yaml\Tag\TaggedValue; +use Symfony\Component\Yaml\Yaml; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Parameter; @@ -46,7 +52,7 @@ public function dump(array $options = array()) $this->dumper = new YmlDumper(); } - return $this->addParameters()."\n".$this->addServices(); + return $this->container->resolveEnvPlaceholders($this->addParameters()."\n".$this->addServices()); } /** @@ -96,10 +102,6 @@ private function addService($id, $definition) $code .= " synthetic: true\n"; } - if ($definition->isSynchronized(false)) { - $code .= " synchronized: true\n"; - } - if ($definition->isDeprecated()) { $code .= sprintf(" deprecated: %s\n", $definition->getDeprecationMessage('%service_id%')); } @@ -108,16 +110,8 @@ private function addService($id, $definition) $code .= " autowire: true\n"; } - $autowiringTypesCode = ''; - foreach ($definition->getAutowiringTypes() as $autowiringType) { - $autowiringTypesCode .= sprintf(" - %s\n", $this->dumper->dump($autowiringType)); - } - if ($autowiringTypesCode) { - $code .= sprintf(" autowiring_types:\n%s", $autowiringTypesCode); - } - - if ($definition->getFactoryClass(false)) { - $code .= sprintf(" factory_class: %s\n", $this->dumper->dump($definition->getFactoryClass(false))); + if ($definition->isAutoconfigured()) { + $code .= " autoconfigure: true\n"; } if ($definition->isAbstract()) { @@ -128,14 +122,6 @@ private function addService($id, $definition) $code .= " lazy: true\n"; } - if ($definition->getFactoryMethod(false)) { - $code .= sprintf(" factory_method: %s\n", $this->dumper->dump($definition->getFactoryMethod(false))); - } - - if ($definition->getFactoryService(false)) { - $code .= sprintf(" factory_service: %s\n", $this->dumper->dump($definition->getFactoryService(false))); - } - if ($definition->getArguments()) { $code .= sprintf(" arguments: %s\n", $this->dumper->dump($this->dumpValue($definition->getArguments()), 0)); } @@ -152,10 +138,6 @@ private function addService($id, $definition) $code .= " shared: false\n"; } - if (ContainerInterface::SCOPE_CONTAINER !== $scope = $definition->getScope(false)) { - $code .= sprintf(" scope: %s\n", $this->dumper->dump($scope)); - } - if (null !== $decorated = $definition->getDecoratedService()) { list($decorated, $renamedId, $priority) = $decorated; $code .= sprintf(" decorates: %s\n", $decorated); @@ -233,7 +215,7 @@ private function addParameters() return ''; } - $parameters = $this->prepareParameters($this->container->getParameterBag()->all(), $this->container->isFrozen()); + $parameters = $this->prepareParameters($this->container->getParameterBag()->all(), $this->container->isCompiled()); return $this->dumper->dump(array('parameters' => $parameters), 2); } @@ -269,6 +251,19 @@ private function dumpCallable($callable) */ private function dumpValue($value) { + if ($value instanceof ServiceClosureArgument) { + $value = $value->getValues()[0]; + } + if ($value instanceof ArgumentInterface) { + if ($value instanceof IteratorArgument) { + $tag = 'iterator'; + } else { + throw new RuntimeException(sprintf('Unspecified Yaml tag for type "%s".', get_class($value))); + } + + return new TaggedValue($tag, $this->dumpValue($value->getValues())); + } + if (is_array($value)) { $code = array(); foreach ($value as $k => $v) { @@ -282,6 +277,8 @@ private function dumpValue($value) return $this->getParameterCall((string) $value); } elseif ($value instanceof Expression) { return $this->getExpressionCall((string) $value); + } elseif ($value instanceof Definition) { + return new TaggedValue('service', (new Parser())->parse("_:\n".$this->addService('_', $value), Yaml::PARSE_CUSTOM_TAGS)['_']['_']); } elseif (is_object($value) || is_resource($value)) { throw new RuntimeException('Unable to dump a service container if a parameter is an object or a resource.'); } diff --git a/src/Symfony/Component/DependencyInjection/Exception/AutowiringFailedException.php b/src/Symfony/Component/DependencyInjection/Exception/AutowiringFailedException.php new file mode 100644 index 0000000000000..145cd8cbdcf24 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/AutowiringFailedException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +/** + * Thrown when a definition cannot be autowired. + */ +class AutowiringFailedException extends RuntimeException +{ + private $serviceId; + + public function __construct($serviceId, $message = '', $code = 0, \Exception $previous = null) + { + $this->serviceId = $serviceId; + + parent::__construct($message, $code, $previous); + } + + public function getServiceId() + { + return $this->serviceId; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Exception/EnvNotFoundException.php b/src/Symfony/Component/DependencyInjection/Exception/EnvNotFoundException.php new file mode 100644 index 0000000000000..577095e88b493 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/EnvNotFoundException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +/** + * This exception is thrown when an environment variable is not found. + * + * @author Nicolas Grekas + */ +class EnvNotFoundException extends InvalidArgumentException +{ + public function __construct($name) + { + parent::__construct(sprintf('Environment variable not found: "%s".', $name)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Exception/EnvParameterException.php b/src/Symfony/Component/DependencyInjection/Exception/EnvParameterException.php new file mode 100644 index 0000000000000..3839a4633be40 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/EnvParameterException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Exception; + +/** + * This exception wraps exceptions whose messages contain a reference to an env parameter. + * + * @author Nicolas Grekas + */ +class EnvParameterException extends InvalidArgumentException +{ + public function __construct(array $envs, \Exception $previous = null, $message = 'Incompatible use of dynamic environment variables "%s" found in parameters.') + { + parent::__construct(sprintf($message, implode('", "', $envs)), 0, $previous); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Exception/ExceptionInterface.php b/src/Symfony/Component/DependencyInjection/Exception/ExceptionInterface.php index f5e9099f11199..5bec478695f6f 100644 --- a/src/Symfony/Component/DependencyInjection/Exception/ExceptionInterface.php +++ b/src/Symfony/Component/DependencyInjection/Exception/ExceptionInterface.php @@ -11,12 +11,14 @@ namespace Symfony\Component\DependencyInjection\Exception; +use Psr\Container\ContainerExceptionInterface; + /** * Base ExceptionInterface for Dependency Injection component. * * @author Fabien Potencier * @author Bulat Shakirzyanov */ -interface ExceptionInterface +interface ExceptionInterface extends ContainerExceptionInterface { } diff --git a/src/Symfony/Component/DependencyInjection/Exception/InactiveScopeException.php b/src/Symfony/Component/DependencyInjection/Exception/InactiveScopeException.php deleted file mode 100644 index 6b3dd3ebb1acd..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Exception/InactiveScopeException.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Exception; - -/** - * This exception is thrown when you try to create a service of an inactive scope. - * - * @author Johannes M. Schmitt - */ -class InactiveScopeException extends RuntimeException -{ - private $serviceId; - private $scope; - - public function __construct($serviceId, $scope, \Exception $previous = null) - { - parent::__construct(sprintf('You cannot create a service ("%s") of an inactive scope ("%s").', $serviceId, $scope), 0, $previous); - - $this->serviceId = $serviceId; - $this->scope = $scope; - } - - public function getServiceId() - { - return $this->serviceId; - } - - public function getScope() - { - return $this->scope; - } -} diff --git a/src/Symfony/Component/DependencyInjection/Exception/ParameterNotFoundException.php b/src/Symfony/Component/DependencyInjection/Exception/ParameterNotFoundException.php index ab7b86d5acc90..40c01b05081d8 100644 --- a/src/Symfony/Component/DependencyInjection/Exception/ParameterNotFoundException.php +++ b/src/Symfony/Component/DependencyInjection/Exception/ParameterNotFoundException.php @@ -22,20 +22,23 @@ class ParameterNotFoundException extends InvalidArgumentException private $sourceId; private $sourceKey; private $alternatives; + private $nonNestedAlternative; /** - * @param string $key The requested parameter key - * @param string $sourceId The service id that references the non-existent parameter - * @param string $sourceKey The parameter key that references the non-existent parameter - * @param \Exception $previous The previous exception - * @param string[] $alternatives Some parameter name alternatives + * @param string $key The requested parameter key + * @param string $sourceId The service id that references the non-existent parameter + * @param string $sourceKey The parameter key that references the non-existent parameter + * @param \Exception $previous The previous exception + * @param string[] $alternatives Some parameter name alternatives + * @param string|null $nonNestedAlternative The alternative parameter name when the user expected dot notation for nested parameters */ - public function __construct($key, $sourceId = null, $sourceKey = null, \Exception $previous = null, array $alternatives = array()) + public function __construct($key, $sourceId = null, $sourceKey = null, \Exception $previous = null, array $alternatives = array(), $nonNestedAlternative = null) { $this->key = $key; $this->sourceId = $sourceId; $this->sourceKey = $sourceKey; $this->alternatives = $alternatives; + $this->nonNestedAlternative = $nonNestedAlternative; parent::__construct('', 0, $previous); @@ -59,6 +62,8 @@ public function updateRepr() $this->message .= ' Did you mean one of these: "'; } $this->message .= implode('", "', $this->alternatives).'"?'; + } elseif (null !== $this->nonNestedAlternative) { + $this->message .= ' You cannot access nested array items, do you want to inject "'.$this->nonNestedAlternative.'" instead?'; } } diff --git a/src/Symfony/Component/DependencyInjection/Exception/ScopeCrossingInjectionException.php b/src/Symfony/Component/DependencyInjection/Exception/ScopeCrossingInjectionException.php deleted file mode 100644 index 661fbab3697f8..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Exception/ScopeCrossingInjectionException.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Exception; - -/** - * This exception is thrown when the a scope crossing injection is detected. - * - * @author Johannes M. Schmitt - */ -class ScopeCrossingInjectionException extends RuntimeException -{ - private $sourceServiceId; - private $sourceScope; - private $destServiceId; - private $destScope; - - public function __construct($sourceServiceId, $sourceScope, $destServiceId, $destScope, \Exception $previous = null) - { - parent::__construct(sprintf( - 'Scope Crossing Injection detected: The definition "%s" references the service "%s" which belongs to another scope hierarchy. ' - .'This service might not be available consistently. Generally, it is safer to either move the definition "%s" to scope "%s", or ' - .'declare "%s" as a child scope of "%s". If you can be sure that the other scope is always active, you can set the reference to strict=false to get rid of this error.', - $sourceServiceId, - $destServiceId, - $sourceServiceId, - $destScope, - $sourceScope, - $destScope - ), 0, $previous); - - $this->sourceServiceId = $sourceServiceId; - $this->sourceScope = $sourceScope; - $this->destServiceId = $destServiceId; - $this->destScope = $destScope; - } - - public function getSourceServiceId() - { - return $this->sourceServiceId; - } - - public function getSourceScope() - { - return $this->sourceScope; - } - - public function getDestServiceId() - { - return $this->destServiceId; - } - - public function getDestScope() - { - return $this->destScope; - } -} diff --git a/src/Symfony/Component/DependencyInjection/Exception/ScopeWideningInjectionException.php b/src/Symfony/Component/DependencyInjection/Exception/ScopeWideningInjectionException.php deleted file mode 100644 index 86a668419510f..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Exception/ScopeWideningInjectionException.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Exception; - -/** - * Thrown when a scope widening injection is detected. - * - * @author Johannes M. Schmitt - */ -class ScopeWideningInjectionException extends RuntimeException -{ - private $sourceServiceId; - private $sourceScope; - private $destServiceId; - private $destScope; - - public function __construct($sourceServiceId, $sourceScope, $destServiceId, $destScope, \Exception $previous = null) - { - parent::__construct(sprintf( - 'Scope Widening Injection detected: The definition "%s" references the service "%s" which belongs to a narrower scope. ' - .'Generally, it is safer to either move "%s" to scope "%s" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "%s" each time it is needed. ' - .'In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error.', - $sourceServiceId, - $destServiceId, - $sourceServiceId, - $destScope, - $destServiceId - ), 0, $previous); - - $this->sourceServiceId = $sourceServiceId; - $this->sourceScope = $sourceScope; - $this->destServiceId = $destServiceId; - $this->destScope = $destScope; - } - - public function getSourceServiceId() - { - return $this->sourceServiceId; - } - - public function getSourceScope() - { - return $this->sourceScope; - } - - public function getDestServiceId() - { - return $this->destServiceId; - } - - public function getDestScope() - { - return $this->destScope; - } -} diff --git a/src/Symfony/Component/DependencyInjection/Exception/ServiceNotFoundException.php b/src/Symfony/Component/DependencyInjection/Exception/ServiceNotFoundException.php index e65da506bb515..0194c4f372279 100644 --- a/src/Symfony/Component/DependencyInjection/Exception/ServiceNotFoundException.php +++ b/src/Symfony/Component/DependencyInjection/Exception/ServiceNotFoundException.php @@ -11,12 +11,14 @@ namespace Symfony\Component\DependencyInjection\Exception; +use Psr\Container\NotFoundExceptionInterface; + /** * This exception is thrown when a non-existent service is requested. * * @author Johannes M. Schmitt */ -class ServiceNotFoundException extends InvalidArgumentException +class ServiceNotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface { private $id; private $sourceId; diff --git a/src/Symfony/Component/DependencyInjection/ExpressionLanguage.php b/src/Symfony/Component/DependencyInjection/ExpressionLanguage.php index acc97bcf4973d..a64561c3a9715 100644 --- a/src/Symfony/Component/DependencyInjection/ExpressionLanguage.php +++ b/src/Symfony/Component/DependencyInjection/ExpressionLanguage.php @@ -11,8 +11,8 @@ namespace Symfony\Component\DependencyInjection; +use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; -use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface; /** * Adds some function to the default ExpressionLanguage. @@ -23,10 +23,13 @@ */ class ExpressionLanguage extends BaseExpressionLanguage { - public function __construct(ParserCacheInterface $cache = null, array $providers = array()) + /** + * {@inheritdoc} + */ + public function __construct(CacheItemPoolInterface $cache = null, array $providers = array(), callable $serviceCompiler = null) { // prepend the default provider to let users override it easily - array_unshift($providers, new ExpressionLanguageProvider()); + array_unshift($providers, new ExpressionLanguageProvider($serviceCompiler)); parent::__construct($cache, $providers); } diff --git a/src/Symfony/Component/DependencyInjection/ExpressionLanguageProvider.php b/src/Symfony/Component/DependencyInjection/ExpressionLanguageProvider.php index ce6d69522e191..e2084aa85da6d 100644 --- a/src/Symfony/Component/DependencyInjection/ExpressionLanguageProvider.php +++ b/src/Symfony/Component/DependencyInjection/ExpressionLanguageProvider.php @@ -24,10 +24,17 @@ */ class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface { + private $serviceCompiler; + + public function __construct(callable $serviceCompiler = null) + { + $this->serviceCompiler = $serviceCompiler; + } + public function getFunctions() { return array( - new ExpressionFunction('service', function ($arg) { + new ExpressionFunction('service', $this->serviceCompiler ?: function ($arg) { return sprintf('$this->get(%s)', $arg); }, function (array $variables, $value) { return $variables['container']->get($value); diff --git a/src/Symfony/Component/DependencyInjection/Extension/Extension.php b/src/Symfony/Component/DependencyInjection/Extension/Extension.php index ced39f7281b6c..117ee58c111f4 100644 --- a/src/Symfony/Component/DependencyInjection/Extension/Extension.php +++ b/src/Symfony/Component/DependencyInjection/Extension/Extension.php @@ -14,7 +14,6 @@ use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Exception\BadMethodCallException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -78,17 +77,13 @@ public function getAlias() */ public function getConfiguration(array $config, ContainerBuilder $container) { - $reflected = new \ReflectionClass($this); - $namespace = $reflected->getNamespaceName(); + $class = get_class($this); + $class = substr_replace($class, '\Configuration', strrpos($class, '\\')); + $class = $container->getReflectionClass($class); + $constructor = $class ? $class->getConstructor() : null; - $class = $namespace.'\\Configuration'; - if (class_exists($class)) { - $r = new \ReflectionClass($class); - $container->addResource(new FileResource($r->getFileName())); - - if (!method_exists($class, '__construct')) { - return new $class(); - } + if ($class && (!$constructor || !$constructor->getNumberOfRequiredParameters())) { + return $class->newInstance(); } } diff --git a/src/Symfony/Component/DependencyInjection/IntrospectableContainerInterface.php b/src/Symfony/Component/DependencyInjection/IntrospectableContainerInterface.php deleted file mode 100644 index 4aa0059992e5b..0000000000000 --- a/src/Symfony/Component/DependencyInjection/IntrospectableContainerInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection; - -/** - * IntrospectableContainerInterface defines additional introspection functionality - * for containers, allowing logic to be implemented based on a Container's state. - * - * @author Evan Villemez - * - * @deprecated since version 2.8, to be merged with ContainerInterface in 3.0. - */ -interface IntrospectableContainerInterface extends ContainerInterface -{ - /** - * Check for whether or not a service has been initialized. - * - * @param string $id - * - * @return bool true if the service has been initialized, false otherwise - */ - public function initialized($id); -} diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php index 878d965b1c39d..58907aa317cba 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/DumperInterface.php @@ -33,11 +33,12 @@ public function isProxyCandidate(Definition $definition); * Generates the code to be used to instantiate a proxy in the dumped factory code. * * @param Definition $definition - * @param string $id service identifier + * @param string $id service identifier + * @param string $factoryCode the code to execute to create the service * * @return string */ - public function getProxyFactoryCode(Definition $definition, $id); + public function getProxyFactoryCode(Definition $definition, $id, $factoryCode); /** * Generates the code for the lazy proxy. diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php index 30911d3a5e83a..67f9fae94dbf8 100644 --- a/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/PhpDumper/NullDumper.php @@ -17,6 +17,8 @@ * Null dumper, negates any proxy code generation for any given service definition. * * @author Marco Pivetta + * + * @final since version 3.3 */ class NullDumper implements DumperInterface { @@ -31,7 +33,7 @@ public function isProxyCandidate(Definition $definition) /** * {@inheritdoc} */ - public function getProxyFactoryCode(Definition $definition, $id) + public function getProxyFactoryCode(Definition $definition, $id, $factoryCode = null) { return ''; } diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php b/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php new file mode 100644 index 0000000000000..6e3d75bb0b9de --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/LazyProxy/ProxyHelper.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\LazyProxy; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class ProxyHelper +{ + /** + * @return string|null The FQCN or builtin name of the type hint, or null when the type hint references an invalid self|parent context + */ + public static function getTypeHint(\ReflectionFunctionAbstract $r, \ReflectionParameter $p = null, $noBuiltin = false) + { + if ($p instanceof \ReflectionParameter) { + $type = $p->getType(); + } else { + $type = $r->getReturnType(); + } + if (!$type) { + return; + } + if (!is_string($type)) { + $name = $type->getName(); + + if ($type->isBuiltin()) { + return $noBuiltin ? null : $name; + } + } + $lcName = strtolower($name); + $prefix = $noBuiltin ? '' : '\\'; + + if ('self' !== $lcName && 'parent' !== $lcName) { + return $prefix.$name; + } + if (!$r instanceof \ReflectionMethod) { + return; + } + if ('self' === $lcName) { + return $prefix.$r->getDeclaringClass()->name; + } + if ($parent = $r->getDeclaringClass()->getParentClass()) { + return $prefix.$parent->name; + } + } + + private static function export($value) + { + if (!is_array($value)) { + return var_export($value, true); + } + $code = array(); + foreach ($value as $k => $v) { + $code[] = sprintf('%s => %s', var_export($k, true), self::export($v)); + } + + return sprintf('array(%s)', implode(', ', $code)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/DirectoryLoader.php b/src/Symfony/Component/DependencyInjection/Loader/DirectoryLoader.php index ffb8853011134..3ab4c5dc82e7e 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/DirectoryLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/DirectoryLoader.php @@ -27,7 +27,7 @@ public function load($file, $type = null) { $file = rtrim($file, '/'); $path = $this->locator->locate($file); - $this->container->addResource(new DirectoryResource($path)); + $this->container->fileExists($path, false); foreach (scandir($path) as $dir) { if ('.' !== $dir[0]) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 90cd6bcfafa4d..5bc7f347359e9 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -11,9 +11,13 @@ namespace Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader; use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Resource\GlobResource; /** * FileLoader is the abstract class used by all built-in loaders that are file based. @@ -23,6 +27,8 @@ abstract class FileLoader extends BaseFileLoader { protected $container; + protected $isLoadingInstanceof = false; + protected $instanceof = array(); /** * @param ContainerBuilder $container A ContainerBuilder instance @@ -34,4 +40,113 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l parent::__construct($locator); } + + /** + * Registers a set of classes as services using PSR-4 for discovery. + * + * @param Definition $prototype A definition to use as template + * @param string $namespace The namespace prefix of classes in the scanned directory + * @param string $resource The directory to look for classes, glob-patterns allowed + * @param string $exclude A globed path of files to exclude + */ + public function registerClasses(Definition $prototype, $namespace, $resource, $exclude = null) + { + if ('\\' !== substr($namespace, -1)) { + throw new InvalidArgumentException(sprintf('Namespace prefix must end with a "\\": %s.', $namespace)); + } + if (!preg_match('/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++$/', $namespace)) { + throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: %s.', $namespace)); + } + + $classes = $this->findClasses($namespace, $resource, $exclude); + // prepare for deep cloning + $prototype = serialize($prototype); + + foreach ($classes as $class) { + $this->setDefinition($class, unserialize($prototype)); + } + } + + /** + * Registers a definition in the container with its instanceof-conditionals. + * + * @param string $id + * @param Definition $definition + */ + protected function setDefinition($id, Definition $definition) + { + if ($this->isLoadingInstanceof) { + if (!$definition instanceof ChildDefinition) { + throw new InvalidArgumentException(sprintf('Invalid type definition "%s": ChildDefinition expected, "%s" given.', $id, get_class($definition))); + } + $this->instanceof[$id] = $definition; + } else { + $this->container->setDefinition($id, $definition instanceof ChildDefinition ? $definition : $definition->setInstanceofConditionals($this->instanceof)); + } + } + + private function findClasses($namespace, $pattern, $excludePattern) + { + $parameterBag = $this->container->getParameterBag(); + + $excludePaths = array(); + $excludePrefix = null; + if ($excludePattern) { + $excludePattern = $parameterBag->unescapeValue($parameterBag->resolveValue($excludePattern)); + foreach ($this->glob($excludePattern, true, $resource) as $path => $info) { + if (null === $excludePrefix) { + $excludePrefix = $resource->getPrefix(); + } + + // normalize Windows slashes + $excludePaths[str_replace('\\', '/', $path)] = true; + } + } + + $pattern = $parameterBag->unescapeValue($parameterBag->resolveValue($pattern)); + $classes = array(); + $extRegexp = '/\\.php$/'; + $prefixLen = null; + foreach ($this->glob($pattern, true, $resource) as $path => $info) { + if (null === $prefixLen) { + $prefixLen = strlen($resource->getPrefix()); + + if ($excludePrefix && strpos($excludePrefix, $resource->getPrefix()) !== 0) { + throw new InvalidArgumentException(sprintf('Invalid "exclude" pattern when importing classes for "%s": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s)', $namespace, $excludePattern, $pattern)); + } + } + + if (isset($excludePaths[str_replace('\\', '/', $path)])) { + continue; + } + + if (!preg_match($extRegexp, $path, $m) || !$info->isReadable()) { + continue; + } + $class = $namespace.ltrim(str_replace('/', '\\', substr($path, $prefixLen, -strlen($m[0]))), '\\'); + + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class)) { + continue; + } + // check to make sure the expected class exists + if (!$r = $this->container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern)); + } + + if (!$r->isInterface() && !$r->isTrait() && !$r->isAbstract()) { + $classes[] = $class; + } + } + + // track only for new & removed files + if ($resource instanceof GlobResource) { + $this->container->addResource($resource); + } else { + foreach ($resource as $path) { + $this->container->fileExists($path, false); + } + } + + return $classes; + } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/IniFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/IniFileLoader.php index 16ddf87f85112..170e726396a5e 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/IniFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/IniFileLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; /** @@ -28,16 +29,20 @@ public function load($resource, $type = null) { $path = $this->locator->locate($resource); - $this->container->addResource(new FileResource($path)); + $this->container->fileExists($path); + // first pass to catch parsing errors $result = parse_ini_file($path, true); if (false === $result || array() === $result) { throw new InvalidArgumentException(sprintf('The "%s" file is not valid.', $resource)); } + // real raw parsing + $result = parse_ini_file($path, true, INI_SCANNER_RAW); + if (isset($result['parameters']) && is_array($result['parameters'])) { foreach ($result['parameters'] as $key => $value) { - $this->container->setParameter($key, $value); + $this->container->setParameter($key, $this->phpize($value)); } } } @@ -47,6 +52,43 @@ public function load($resource, $type = null) */ public function supports($resource, $type = null) { - return is_string($resource) && 'ini' === pathinfo($resource, PATHINFO_EXTENSION); + if (!is_string($resource)) { + return false; + } + + if (null === $type && 'ini' === pathinfo($resource, PATHINFO_EXTENSION)) { + return true; + } + + return 'ini' === $type; + } + + /** + * Note that the following features are not supported: + * * strings with escaped quotes are not supported "foo\"bar"; + * * string concatenation ("foo" "bar"). + */ + private function phpize($value) + { + // trim on the right as comments removal keep whitespaces + $value = rtrim($value); + $lowercaseValue = strtolower($value); + + switch (true) { + case defined($value): + return constant($value); + case 'yes' === $lowercaseValue || 'on' === $lowercaseValue: + return true; + case 'no' === $lowercaseValue || 'off' === $lowercaseValue || 'none' === $lowercaseValue: + return false; + case isset($value[1]) && ( + ("'" === $value[0] && "'" === $value[strlen($value) - 1]) || + ('"' === $value[0] && '"' === $value[strlen($value) - 1]) + ): + // quoted string + return substr($value, 1, -1); + default: + return XmlUtils::phpize($value); + } } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index 08c1d9af4f653..36ef0d4752bc9 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -34,7 +34,7 @@ public function load($resource, $type = null) $path = $this->locator->locate($resource); $this->setCurrentDir(dirname($path)); - $this->container->addResource(new FileResource($path)); + $this->container->fileExists($path); include $path; } @@ -44,6 +44,14 @@ public function load($resource, $type = null) */ public function supports($resource, $type = null) { - return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION); + if (!is_string($resource)) { + return false; + } + + if (null === $type && 'php' === pathinfo($resource, PATHINFO_EXTENSION)) { + return true; + } + + return 'php' === $type; } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index dc7ca933f7cf9..f620f0785d32a 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -11,12 +11,14 @@ namespace Symfony\Component\DependencyInjection\Loader; -use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Util\XmlUtils; -use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -40,22 +42,28 @@ public function load($resource, $type = null) $xml = $this->parseFileToDOM($path); - $this->container->addResource(new FileResource($path)); + $this->container->fileExists($path); + + $defaults = $this->getServiceDefaults($xml, $path); // anonymous services - $this->processAnonymousServices($xml, $path); + $this->processAnonymousServices($xml, $path, $defaults); // imports $this->parseImports($xml, $path); // parameters - $this->parseParameters($xml); + $this->parseParameters($xml, $path); // extensions $this->loadFromExtensions($xml); // services - $this->parseDefinitions($xml, $path); + try { + $this->parseDefinitions($xml, $path, $defaults); + } finally { + $this->instanceof = array(); + } } /** @@ -63,18 +71,27 @@ public function load($resource, $type = null) */ public function supports($resource, $type = null) { - return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION); + if (!is_string($resource)) { + return false; + } + + if (null === $type && 'xml' === pathinfo($resource, PATHINFO_EXTENSION)) { + return true; + } + + return 'xml' === $type; } /** * Parses parameters. * * @param \DOMDocument $xml + * @param string $file */ - private function parseParameters(\DOMDocument $xml) + private function parseParameters(\DOMDocument $xml, $file) { if ($parameters = $this->getChildren($xml->documentElement, 'parameters')) { - $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter')); + $this->container->getParameterBag()->add($this->getArgumentsAsPhp($parameters[0], 'parameter', $file)); } } @@ -96,7 +113,7 @@ private function parseImports(\DOMDocument $xml, $file) $defaultDirectory = dirname($file); foreach ($imports as $import) { $this->setCurrentDir($defaultDirectory); - $this->import($import->getAttribute('resource'), null, (bool) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file); + $this->import($import->getAttribute('resource'), XmlUtils::phpize($import->getAttribute('type')) ?: null, (bool) XmlUtils::phpize($import->getAttribute('ignore-errors')), $file); } } @@ -106,54 +123,146 @@ private function parseImports(\DOMDocument $xml, $file) * @param \DOMDocument $xml * @param string $file */ - private function parseDefinitions(\DOMDocument $xml, $file) + private function parseDefinitions(\DOMDocument $xml, $file, $defaults) { $xpath = new \DOMXPath($xml); $xpath->registerNamespace('container', self::NS); - if (false === $services = $xpath->query('//container:services/container:service')) { + if (false === $services = $xpath->query('//container:services/container:service|//container:services/container:prototype')) { return; } + $this->setCurrentDir(dirname($file)); + + $this->instanceof = array(); + $this->isLoadingInstanceof = true; + $instanceof = $xpath->query('//container:services/container:instanceof'); + foreach ($instanceof as $service) { + $this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, array())); + } + $this->isLoadingInstanceof = false; foreach ($services as $service) { - if (null !== $definition = $this->parseDefinition($service, $file)) { - $this->container->setDefinition((string) $service->getAttribute('id'), $definition); + if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) { + if ('prototype' === $service->tagName) { + $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), (string) $service->getAttribute('exclude')); + } else { + $this->setDefinition((string) $service->getAttribute('id'), $definition); + } } } } + /** + * Get service defaults. + * + * @return array + */ + private function getServiceDefaults(\DOMDocument $xml, $file) + { + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + + if (null === $defaultsNode = $xpath->query('//container:services/container:defaults')->item(0)) { + return array(); + } + $defaults = array( + 'tags' => $this->getChildren($defaultsNode, 'tag'), + 'bind' => array_map(function ($v) { return new BoundArgument($v); }, $this->getArgumentsAsPhp($defaultsNode, 'bind', $file)), + ); + + foreach ($defaults['tags'] as $tag) { + if ('' === $tag->getAttribute('name')) { + throw new InvalidArgumentException(sprintf('The tag name for tag "" in %s must be a non-empty string.', $file)); + } + } + + if ($defaultsNode->hasAttribute('autowire')) { + $defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire')); + } + if ($defaultsNode->hasAttribute('public')) { + $defaults['public'] = XmlUtils::phpize($defaultsNode->getAttribute('public')); + } + if ($defaultsNode->hasAttribute('autoconfigure')) { + $defaults['autoconfigure'] = XmlUtils::phpize($defaultsNode->getAttribute('autoconfigure')); + } + + return $defaults; + } + /** * Parses an individual Definition. * * @param \DOMElement $service * @param string $file + * @param array $defaults * * @return Definition|null */ - private function parseDefinition(\DOMElement $service, $file) + private function parseDefinition(\DOMElement $service, $file, array $defaults) { if ($alias = $service->getAttribute('alias')) { + $this->validateAlias($service, $file); + $public = true; if ($publicAttr = $service->getAttribute('public')) { $public = XmlUtils::phpize($publicAttr); + } elseif (isset($defaults['public'])) { + $public = $defaults['public']; } $this->container->setAlias((string) $service->getAttribute('id'), new Alias($alias, $public)); return; } - if ($parent = $service->getAttribute('parent')) { - $definition = new DefinitionDecorator($parent); + if ($this->isLoadingInstanceof) { + $definition = new ChildDefinition(''); + } elseif ($parent = $service->getAttribute('parent')) { + if (!empty($this->instanceof)) { + throw new InvalidArgumentException(sprintf('The service "%s" cannot use the "parent" option in the same file where "instanceof" configuration is defined as using both is not supported. Move your child definitions to a separate file.', $service->getAttribute('id'))); + } + + foreach ($defaults as $k => $v) { + if ('tags' === $k) { + // since tags are never inherited from parents, there is no confusion + // thus we can safely add them as defaults to ChildDefinition + continue; + } + if ('bind' === $k) { + if ($defaults['bind']) { + throw new InvalidArgumentException(sprintf('Bound values on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file.', $k, $service->getAttribute('id'))); + } + + continue; + } + if (!$service->hasAttribute($k)) { + throw new InvalidArgumentException(sprintf('Attribute "%s" on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.', $k, $service->getAttribute('id'))); + } + } + + $definition = new ChildDefinition($parent); } else { $definition = new Definition(); + + if (isset($defaults['public'])) { + $definition->setPublic($defaults['public']); + } + if (isset($defaults['autowire'])) { + $definition->setAutowired($defaults['autowire']); + } + if (isset($defaults['autoconfigure'])) { + $definition->setAutoconfigured($defaults['autoconfigure']); + } + + $definition->setChanges(array()); + } + + if ($publicAttr = $service->getAttribute('public')) { + $definition->setPublic(XmlUtils::phpize($publicAttr)); } - foreach (array('class', 'shared', 'public', 'factory-class', 'factory-method', 'factory-service', 'synthetic', 'lazy', 'abstract') as $key) { + foreach (array('class', 'shared', 'synthetic', 'lazy', 'abstract') as $key) { if ($value = $service->getAttribute($key)) { - if (in_array($key, array('factory-class', 'factory-method', 'factory-service'))) { - @trigger_error(sprintf('The "%s" attribute of service "%s" in file "%s" is deprecated since version 2.6 and will be removed in 3.0. Use the "factory" element instead.', $key, (string) $service->getAttribute('id'), $file), E_USER_DEPRECATED); - } - $method = 'set'.str_replace('-', '', $key); + $method = 'set'.$key; $definition->$method(XmlUtils::phpize($value)); } } @@ -162,24 +271,12 @@ private function parseDefinition(\DOMElement $service, $file) $definition->setAutowired(XmlUtils::phpize($value)); } - if ($value = $service->getAttribute('scope')) { - $triggerDeprecation = 'request' !== (string) $service->getAttribute('id'); - - if ($triggerDeprecation) { - @trigger_error(sprintf('The "scope" attribute of service "%s" in file "%s" is deprecated since version 2.8 and will be removed in 3.0.', (string) $service->getAttribute('id'), $file), E_USER_DEPRECATED); + if ($value = $service->getAttribute('autoconfigure')) { + if (!$definition instanceof ChildDefinition) { + $definition->setAutoconfigured(XmlUtils::phpize($value)); + } elseif ($value = XmlUtils::phpize($value)) { + throw new InvalidArgumentException(sprintf('The service "%s" cannot have a "parent" and also have "autoconfigure". Try setting autoconfigure="false" for the service.', $service->getAttribute('id'))); } - - $definition->setScope(XmlUtils::phpize($value), false); - } - - if ($value = $service->getAttribute('synchronized')) { - $triggerDeprecation = 'request' !== (string) $service->getAttribute('id'); - - if ($triggerDeprecation) { - @trigger_error(sprintf('The "synchronized" attribute of service "%s" in file "%s" is deprecated since version 2.7 and will be removed in 3.0.', (string) $service->getAttribute('id'), $file), E_USER_DEPRECATED); - } - - $definition->setSynchronized(XmlUtils::phpize($value), $triggerDeprecation); } if ($files = $this->getChildren($service, 'file')) { @@ -190,22 +287,18 @@ private function parseDefinition(\DOMElement $service, $file) $definition->setDeprecated(true, $deprecated[0]->nodeValue ?: null); } - $definition->setArguments($this->getArgumentsAsPhp($service, 'argument')); - $definition->setProperties($this->getArgumentsAsPhp($service, 'property')); + $definition->setArguments($this->getArgumentsAsPhp($service, 'argument', $file, false, $definition instanceof ChildDefinition)); + $definition->setProperties($this->getArgumentsAsPhp($service, 'property', $file)); if ($factories = $this->getChildren($service, 'factory')) { $factory = $factories[0]; if ($function = $factory->getAttribute('function')) { $definition->setFactory($function); } else { - $factoryService = $this->getChildren($factory, 'service'); - - if (isset($factoryService[0])) { - $class = $this->parseDefinition($factoryService[0], $file); - } elseif ($childService = $factory->getAttribute('service')) { - $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false); + if ($childService = $factory->getAttribute('service')) { + $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); } else { - $class = $factory->getAttribute('class'); + $class = $factory->hasAttribute('class') ? $factory->getAttribute('class') : null; } $definition->setFactory(array($class, $factory->getAttribute('method'))); @@ -217,12 +310,8 @@ private function parseDefinition(\DOMElement $service, $file) if ($function = $configurator->getAttribute('function')) { $definition->setConfigurator($function); } else { - $configuratorService = $this->getChildren($configurator, 'service'); - - if (isset($configuratorService[0])) { - $class = $this->parseDefinition($configuratorService[0], $file); - } elseif ($childService = $configurator->getAttribute('service')) { - $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false); + if ($childService = $configurator->getAttribute('service')) { + $class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE); } else { $class = $configurator->getAttribute('class'); } @@ -232,10 +321,16 @@ private function parseDefinition(\DOMElement $service, $file) } foreach ($this->getChildren($service, 'call') as $call) { - $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument')); + $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file)); } - foreach ($this->getChildren($service, 'tag') as $tag) { + $tags = $this->getChildren($service, 'tag'); + + if (!empty($defaults['tags'])) { + $tags = array_merge($tags, $defaults['tags']); + } + + foreach ($tags as $tag) { $parameters = array(); foreach ($tag->attributes as $name => $node) { if ('name' === $name) { @@ -245,7 +340,7 @@ private function parseDefinition(\DOMElement $service, $file) if (false !== strpos($name, '-') && false === strpos($name, '_') && !array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); } - // keep not normalized key for BC too + // keep not normalized key $parameters[$name] = XmlUtils::phpize($node->nodeValue); } @@ -256,8 +351,13 @@ private function parseDefinition(\DOMElement $service, $file) $definition->addTag($tag->getAttribute('name'), $parameters); } - foreach ($this->getChildren($service, 'autowiring-type') as $type) { - $definition->addAutowiringType($type->textContent); + $bindings = $this->getArgumentsAsPhp($service, 'bind', $file); + if (isset($defaults['bind'])) { + // deep clone, to avoid multiple process of the same instance in the passes + $bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings); + } + if ($bindings) { + $definition->setBindings($bindings); } if ($value = $service->getAttribute('decorates')) { @@ -296,24 +396,27 @@ private function parseFileToDOM($file) * * @param \DOMDocument $xml * @param string $file + * @param array $defaults */ - private function processAnonymousServices(\DOMDocument $xml, $file) + private function processAnonymousServices(\DOMDocument $xml, $file, $defaults) { $definitions = array(); $count = 0; + $suffix = ContainerBuilder::hash($file); $xpath = new \DOMXPath($xml); $xpath->registerNamespace('container', self::NS); // anonymous services as arguments/properties - if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]')) { + if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:bind[not(@id)]|//container:factory[not(@service)]|//container:configurator[not(@service)]')) { foreach ($nodes as $node) { - // give it a unique name - $id = sprintf('%s_%d', hash('sha256', $file), ++$count); - $node->setAttribute('id', $id); - if ($services = $this->getChildren($node, 'service')) { - $definitions[$id] = array($services[0], $file, false); + // give it a unique name + $id = sprintf('%d_%s', ++$count, preg_replace('/^.*\\\\/', '', $node->getAttribute('class')).$suffix); + $node->setAttribute('id', $id); + $node->setAttribute('service', $id); + + $definitions[$id] = array($services[0], $file); $services[0]->setAttribute('id', $id); // anonymous services are always private @@ -326,28 +429,15 @@ private function processAnonymousServices(\DOMDocument $xml, $file) // anonymous services "in the wild" if (false !== $nodes = $xpath->query('//container:services/container:service[not(@id)]')) { foreach ($nodes as $node) { - // give it a unique name - $id = sprintf('%s_%d', hash('sha256', $file), ++$count); - $node->setAttribute('id', $id); - $definitions[$id] = array($node, $file, true); + throw new InvalidArgumentException(sprintf('Top-level services must have "id" attribute, none found in %s at line %d.', $file, $node->getLineNo())); } } // resolve definitions - krsort($definitions); - foreach ($definitions as $id => $def) { - list($domElement, $file, $wild) = $def; - - if (null !== $definition = $this->parseDefinition($domElement, $file)) { - $this->container->setDefinition($id, $definition); - } - - if (true === $wild) { - $tmpDomElement = new \DOMElement('_services', null, self::NS); - $domElement->parentNode->replaceChild($tmpDomElement, $domElement); - $tmpDomElement->setAttribute('id', $id); - } else { - $domElement->parentNode->removeChild($domElement); + uksort($definitions, 'strnatcmp'); + foreach (array_reverse($definitions) as $id => list($domElement, $file)) { + if (null !== $definition = $this->parseDefinition($domElement, $file, array())) { + $this->setDefinition($id, $definition); } } } @@ -357,11 +447,12 @@ private function processAnonymousServices(\DOMDocument $xml, $file) * * @param \DOMElement $node * @param string $name + * @param string $file * @param bool $lowercase * * @return mixed */ - private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) + private function getArgumentsAsPhp(\DOMElement $node, $name, $file, $lowercase = true, $isChildDefinition = false) { $arguments = array(); foreach ($this->getChildren($node, $name) as $arg) { @@ -369,10 +460,10 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) $arg->setAttribute('key', $arg->getAttribute('name')); } - // this is used by DefinitionDecorator to overwrite a specific + // this is used by ChildDefinition to overwrite a specific // argument of the parent definition if ($arg->hasAttribute('index')) { - $key = 'index_'.$arg->getAttribute('index'); + $key = ($isChildDefinition ? 'index_' : '').$arg->getAttribute('index'); } elseif (!$arg->hasAttribute('key')) { // Append an empty argument, then fetch its key to overwrite it later $arguments[] = null; @@ -387,29 +478,35 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true) } } + $onInvalid = $arg->getAttribute('on-invalid'); + $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; + if ('ignore' == $onInvalid) { + $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + } elseif ('null' == $onInvalid) { + $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; + } + switch ($arg->getAttribute('type')) { case 'service': - $onInvalid = $arg->getAttribute('on-invalid'); - $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; - if ('ignore' == $onInvalid) { - $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; - } elseif ('null' == $onInvalid) { - $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; + if (!$arg->getAttribute('id')) { + throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service" has no or empty "id" attribute in "%s".', $name, $file)); } - if ($strict = $arg->getAttribute('strict')) { - $strict = XmlUtils::phpize($strict); - } else { - $strict = true; - } - - $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior, $strict); + $arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior); break; case 'expression': $arguments[$key] = new Expression($arg->nodeValue); break; case 'collection': - $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false); + $arguments[$key] = $this->getArgumentsAsPhp($arg, $name, $file, false); + break; + case 'iterator': + $arg = $this->getArgumentsAsPhp($arg, $name, $file, false); + try { + $arguments[$key] = new IteratorArgument($arg); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="iterator" only accepts collections of type="service" references in "%s".', $name, $file)); + } break; case 'string': $arguments[$key] = $arg->nodeValue; @@ -519,6 +616,27 @@ public function validateSchema(\DOMDocument $dom) return $valid; } + /** + * Validates an alias. + * + * @param \DOMElement $alias + * @param string $file + */ + private function validateAlias(\DOMElement $alias, $file) + { + foreach ($alias->attributes as $name => $node) { + if (!in_array($name, array('alias', 'id', 'public'))) { + throw new InvalidArgumentException(sprintf('Invalid attribute "%s" defined for alias "%s" in "%s".', $name, $alias->getAttribute('id'), $file)); + } + } + + foreach ($alias->childNodes as $child) { + if ($child instanceof \DOMElement && $child->namespaceURI === self::NS) { + throw new InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $alias->getAttribute('id'), $file)); + } + } + } + /** * Validates an extension. * diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index b527dec4fc13e..88382eec3e19d 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -11,29 +11,99 @@ namespace Symfony\Component\DependencyInjection\Loader; -use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; -use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; +use Symfony\Component\Yaml\Tag\TaggedValue; +use Symfony\Component\Yaml\Yaml; use Symfony\Component\ExpressionLanguage\Expression; /** * YamlFileLoader loads YAML files service definitions. * - * The YAML format does not support anonymous services (cf. the XML loader). - * * @author Fabien Potencier */ class YamlFileLoader extends FileLoader { + private static $serviceKeywords = array( + 'alias' => 'alias', + 'parent' => 'parent', + 'class' => 'class', + 'shared' => 'shared', + 'synthetic' => 'synthetic', + 'lazy' => 'lazy', + 'public' => 'public', + 'abstract' => 'abstract', + 'deprecated' => 'deprecated', + 'factory' => 'factory', + 'file' => 'file', + 'arguments' => 'arguments', + 'properties' => 'properties', + 'configurator' => 'configurator', + 'calls' => 'calls', + 'tags' => 'tags', + 'decorates' => 'decorates', + 'decoration_inner_name' => 'decoration_inner_name', + 'decoration_priority' => 'decoration_priority', + 'autowire' => 'autowire', + 'autoconfigure' => 'autoconfigure', + 'bind' => 'bind', + ); + + private static $prototypeKeywords = array( + 'resource' => 'resource', + 'exclude' => 'exclude', + 'parent' => 'parent', + 'shared' => 'shared', + 'lazy' => 'lazy', + 'public' => 'public', + 'abstract' => 'abstract', + 'deprecated' => 'deprecated', + 'factory' => 'factory', + 'arguments' => 'arguments', + 'properties' => 'properties', + 'configurator' => 'configurator', + 'calls' => 'calls', + 'tags' => 'tags', + 'autowire' => 'autowire', + 'autoconfigure' => 'autoconfigure', + 'bind' => 'bind', + ); + + private static $instanceofKeywords = array( + 'shared' => 'shared', + 'lazy' => 'lazy', + 'public' => 'public', + 'properties' => 'properties', + 'configurator' => 'configurator', + 'calls' => 'calls', + 'tags' => 'tags', + 'autowire' => 'autowire', + ); + + private static $defaultsKeywords = array( + 'public' => 'public', + 'tags' => 'tags', + 'autowire' => 'autowire', + 'autoconfigure' => 'autoconfigure', + 'bind' => 'bind', + ); + private $yamlParser; + private $anonymousServicesCount; + private $anonymousServicesSuffix; + /** * {@inheritdoc} */ @@ -43,7 +113,7 @@ public function load($resource, $type = null) $content = $this->loadFile($path); - $this->container->addResource(new FileResource($path)); + $this->container->fileExists($path); // empty file if (null === $content) { @@ -56,11 +126,11 @@ public function load($resource, $type = null) // parameters if (isset($content['parameters'])) { if (!is_array($content['parameters'])) { - throw new InvalidArgumentException(sprintf('The "parameters" key should contain an array in %s. Check your YAML syntax.', $resource)); + throw new InvalidArgumentException(sprintf('The "parameters" key should contain an array in %s. Check your YAML syntax.', $path)); } foreach ($content['parameters'] as $key => $value) { - $this->container->setParameter($key, $this->resolveServices($value)); + $this->container->setParameter($key, $this->resolveServices($value, $path, true)); } } @@ -68,7 +138,14 @@ public function load($resource, $type = null) $this->loadFromExtensions($content); // services - $this->parseDefinitions($content, $resource); + $this->anonymousServicesCount = 0; + $this->anonymousServicesSuffix = ContainerBuilder::hash($path); + $this->setCurrentDir(dirname($path)); + try { + $this->parseDefinitions($content, $path); + } finally { + $this->instanceof = array(); + } } /** @@ -76,7 +153,15 @@ public function load($resource, $type = null) */ public function supports($resource, $type = null) { - return is_string($resource) && in_array(pathinfo($resource, PATHINFO_EXTENSION), array('yml', 'yaml'), true); + if (!is_string($resource)) { + return false; + } + + if (null === $type && in_array(pathinfo($resource, PATHINFO_EXTENSION), array('yaml', 'yml'), true)) { + return true; + } + + return in_array($type, array('yaml', 'yml'), true); } /** @@ -98,11 +183,14 @@ private function parseImports(array $content, $file) $defaultDirectory = dirname($file); foreach ($content['imports'] as $import) { if (!is_array($import)) { - throw new InvalidArgumentException(sprintf('The values in the "imports" key should be arrays in %s. Check your YAML syntax.', $file)); + $import = array('resource' => $import); + } + if (!isset($import['resource'])) { + throw new InvalidArgumentException(sprintf('An import should provide a resource in %s. Check your YAML syntax.', $file)); } $this->setCurrentDir($defaultDirectory); - $this->import($import['resource'], null, isset($import['ignore_errors']) ? (bool) $import['ignore_errors'] : false, $file); + $this->import($import['resource'], isset($import['type']) ? $import['type'] : null, isset($import['ignore_errors']) ? (bool) $import['ignore_errors'] : false, $file); } } @@ -122,43 +210,200 @@ private function parseDefinitions(array $content, $file) throw new InvalidArgumentException(sprintf('The "services" key should contain an array in %s. Check your YAML syntax.', $file)); } + if (array_key_exists('_instanceof', $content['services'])) { + $instanceof = $content['services']['_instanceof']; + unset($content['services']['_instanceof']); + + if (!is_array($instanceof)) { + throw new InvalidArgumentException(sprintf('Service "_instanceof" key must be an array, "%s" given in "%s".', gettype($instanceof), $file)); + } + $this->instanceof = array(); + $this->isLoadingInstanceof = true; + foreach ($instanceof as $id => $service) { + if (!$service || !is_array($service)) { + throw new InvalidArgumentException(sprintf('Type definition "%s" must be a non-empty array within "_instanceof" in %s. Check your YAML syntax.', $id, $file)); + } + if (is_string($service) && 0 === strpos($service, '@')) { + throw new InvalidArgumentException(sprintf('Type definition "%s" cannot be an alias within "_instanceof" in %s. Check your YAML syntax.', $id, $file)); + } + $this->parseDefinition($id, $service, $file, array()); + } + } + + $this->isLoadingInstanceof = false; + $defaults = $this->parseDefaults($content, $file); foreach ($content['services'] as $id => $service) { - $this->parseDefinition($id, $service, $file); + $this->parseDefinition($id, $service, $file, $defaults); } } + /** + * @param array $content + * @param string $file + * + * @return array + * + * @throws InvalidArgumentException + */ + private function parseDefaults(array &$content, $file) + { + if (!array_key_exists('_defaults', $content['services'])) { + return array(); + } + $defaults = $content['services']['_defaults']; + unset($content['services']['_defaults']); + + if (!is_array($defaults)) { + throw new InvalidArgumentException(sprintf('Service "_defaults" key must be an array, "%s" given in "%s".', gettype($defaults), $file)); + } + + foreach ($defaults as $key => $default) { + if (!isset(self::$defaultsKeywords[$key])) { + throw new InvalidArgumentException(sprintf('The configuration key "%s" cannot be used to define a default value in "%s". Allowed keys are "%s".', $key, $file, implode('", "', self::$defaultsKeywords))); + } + } + + if (isset($defaults['tags'])) { + if (!is_array($tags = $defaults['tags'])) { + throw new InvalidArgumentException(sprintf('Parameter "tags" in "_defaults" must be an array in %s. Check your YAML syntax.', $file)); + } + + foreach ($tags as $tag) { + if (!is_array($tag)) { + $tag = array('name' => $tag); + } + + if (!isset($tag['name'])) { + throw new InvalidArgumentException(sprintf('A "tags" entry in "_defaults" is missing a "name" key in %s.', $file)); + } + $name = $tag['name']; + unset($tag['name']); + + if (!is_string($name) || '' === $name) { + throw new InvalidArgumentException(sprintf('The tag name in "_defaults" must be a non-empty string in %s.', $file)); + } + + foreach ($tag as $attribute => $value) { + if (!is_scalar($value) && null !== $value) { + throw new InvalidArgumentException(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type in %s. Check your YAML syntax.', $name, $attribute, $file)); + } + } + } + } + + if (isset($defaults['bind'])) { + if (!is_array($defaults['bind'])) { + throw new InvalidArgumentException(sprintf('Parameter "bind" in "_defaults" must be an array in %s. Check your YAML syntax.', $file)); + } + + $defaults['bind'] = array_map(function ($v) { return new BoundArgument($v); }, $this->resolveServices($defaults['bind'], $file)); + } + + return $defaults; + } + + /** + * @param array $service + * + * @return bool + */ + private function isUsingShortSyntax(array $service) + { + foreach ($service as $key => $value) { + if (is_string($key) && ('' === $key || '$' !== $key[0])) { + return false; + } + } + + return true; + } + /** * Parses a definition. * * @param string $id * @param array|string $service * @param string $file + * @param array $defaults * * @throws InvalidArgumentException When tags are invalid */ - private function parseDefinition($id, $service, $file) + private function parseDefinition($id, $service, $file, array $defaults) { + if (preg_match('/^_[a-zA-Z0-9_]*$/', $id)) { + throw new InvalidArgumentException(sprintf('Service names that start with an underscore are reserved. Rename the "%s" service or define it in XML instead.', $id)); + } + if (is_string($service) && 0 === strpos($service, '@')) { - $this->container->setAlias($id, substr($service, 1)); + $public = isset($defaults['public']) ? $defaults['public'] : true; + $this->container->setAlias($id, new Alias(substr($service, 1), $public)); return; } + if (is_array($service) && $this->isUsingShortSyntax($service)) { + $service = array('arguments' => $service); + } + + if (null === $service) { + $service = array(); + } + if (!is_array($service)) { throw new InvalidArgumentException(sprintf('A service definition must be an array or a string starting with "@" but %s found for service "%s" in %s. Check your YAML syntax.', gettype($service), $id, $file)); } + $this->checkDefinition($id, $service, $file); + if (isset($service['alias'])) { - $public = !array_key_exists('public', $service) || (bool) $service['public']; + $public = array_key_exists('public', $service) ? (bool) $service['public'] : (isset($defaults['public']) ? $defaults['public'] : true); $this->container->setAlias($id, new Alias($service['alias'], $public)); + foreach ($service as $key => $value) { + if (!in_array($key, array('alias', 'public'))) { + throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported for the service "%s" which is defined as an alias in "%s". Allowed configuration keys for service aliases are "alias" and "public".', $key, $id, $file)); + } + } + return; } - if (isset($service['parent'])) { - $definition = new DefinitionDecorator($service['parent']); + if ($this->isLoadingInstanceof) { + $definition = new ChildDefinition(''); + } elseif (isset($service['parent'])) { + if (!empty($this->instanceof)) { + throw new InvalidArgumentException(sprintf('The service "%s" cannot use the "parent" option in the same file where "_instanceof" configuration is defined as using both is not supported. Move your child definitions to a separate file.', $id)); + } + + foreach ($defaults as $k => $v) { + if ('tags' === $k) { + // since tags are never inherited from parents, there is no confusion + // thus we can safely add them as defaults to ChildDefinition + continue; + } + if ('bind' === $k) { + throw new InvalidArgumentException(sprintf('Attribute "bind" on service "%s" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file.', $id)); + } + if (!isset($service[$k])) { + throw new InvalidArgumentException(sprintf('Attribute "%s" on service "%s" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.', $k, $id)); + } + } + + $definition = new ChildDefinition($service['parent']); } else { $definition = new Definition(); + + if (isset($defaults['public'])) { + $definition->setPublic($defaults['public']); + } + if (isset($defaults['autowire'])) { + $definition->setAutowired($defaults['autowire']); + } + if (isset($defaults['autoconfigure'])) { + $definition->setAutoconfigured($defaults['autoconfigure']); + } + + $definition->setChanges(array()); } if (isset($service['class'])) { @@ -169,22 +414,10 @@ private function parseDefinition($id, $service, $file) $definition->setShared($service['shared']); } - if (isset($service['scope'])) { - if ('request' !== $id) { - @trigger_error(sprintf('The "scope" key of service "%s" in file "%s" is deprecated since version 2.8 and will be removed in 3.0.', $id, $file), E_USER_DEPRECATED); - } - $definition->setScope($service['scope'], false); - } - if (isset($service['synthetic'])) { $definition->setSynthetic($service['synthetic']); } - if (isset($service['synchronized'])) { - @trigger_error(sprintf('The "synchronized" key of service "%s" in file "%s" is deprecated since version 2.7 and will be removed in 3.0.', $id, $file), E_USER_DEPRECATED); - $definition->setSynchronized($service['synchronized'], 'request' !== $id); - } - if (isset($service['lazy'])) { $definition->setLazy($service['lazy']); } @@ -202,31 +435,7 @@ private function parseDefinition($id, $service, $file) } if (isset($service['factory'])) { - if (is_string($service['factory'])) { - if (strpos($service['factory'], ':') !== false && strpos($service['factory'], '::') === false) { - $parts = explode(':', $service['factory']); - $definition->setFactory(array($this->resolveServices('@'.$parts[0]), $parts[1])); - } else { - $definition->setFactory($service['factory']); - } - } else { - $definition->setFactory(array($this->resolveServices($service['factory'][0]), $service['factory'][1])); - } - } - - if (isset($service['factory_class'])) { - @trigger_error(sprintf('The "factory_class" key of service "%s" in file "%s" is deprecated since version 2.6 and will be removed in 3.0. Use "factory" instead.', $id, $file), E_USER_DEPRECATED); - $definition->setFactoryClass($service['factory_class']); - } - - if (isset($service['factory_method'])) { - @trigger_error(sprintf('The "factory_method" key of service "%s" in file "%s" is deprecated since version 2.6 and will be removed in 3.0. Use "factory" instead.', $id, $file), E_USER_DEPRECATED); - $definition->setFactoryMethod($service['factory_method']); - } - - if (isset($service['factory_service'])) { - @trigger_error(sprintf('The "factory_service" key of service "%s" in file "%s" is deprecated since version 2.6 and will be removed in 3.0. Use "factory" instead.', $id, $file), E_USER_DEPRECATED); - $definition->setFactoryService($service['factory_service']); + $definition->setFactory($this->parseCallable($service['factory'], 'factory', $id, $file)); } if (isset($service['file'])) { @@ -234,19 +443,15 @@ private function parseDefinition($id, $service, $file) } if (isset($service['arguments'])) { - $definition->setArguments($this->resolveServices($service['arguments'])); + $definition->setArguments($this->resolveServices($service['arguments'], $file)); } if (isset($service['properties'])) { - $definition->setProperties($this->resolveServices($service['properties'])); + $definition->setProperties($this->resolveServices($service['properties'], $file)); } if (isset($service['configurator'])) { - if (is_string($service['configurator'])) { - $definition->setConfigurator($service['configurator']); - } else { - $definition->setConfigurator(array($this->resolveServices($service['configurator'][0]), $service['configurator'][1])); - } + $definition->setConfigurator($this->parseCallable($service['configurator'], 'configurator', $id, $file)); } if (isset($service['calls'])) { @@ -257,45 +462,47 @@ private function parseDefinition($id, $service, $file) foreach ($service['calls'] as $call) { if (isset($call['method'])) { $method = $call['method']; - $args = isset($call['arguments']) ? $this->resolveServices($call['arguments']) : array(); + $args = isset($call['arguments']) ? $this->resolveServices($call['arguments'], $file) : array(); } else { $method = $call[0]; - $args = isset($call[1]) ? $this->resolveServices($call[1]) : array(); + $args = isset($call[1]) ? $this->resolveServices($call[1], $file) : array(); } $definition->addMethodCall($method, $args); } } - if (isset($service['tags'])) { - if (!is_array($service['tags'])) { - throw new InvalidArgumentException(sprintf('Parameter "tags" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); - } + $tags = isset($service['tags']) ? $service['tags'] : array(); + if (!is_array($tags)) { + throw new InvalidArgumentException(sprintf('Parameter "tags" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); + } - foreach ($service['tags'] as $tag) { - if (!is_array($tag)) { - throw new InvalidArgumentException(sprintf('A "tags" entry must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); - } + if (isset($defaults['tags'])) { + $tags = array_merge($tags, $defaults['tags']); + } - if (!isset($tag['name'])) { - throw new InvalidArgumentException(sprintf('A "tags" entry is missing a "name" key for service "%s" in %s.', $id, $file)); - } + foreach ($tags as $tag) { + if (!is_array($tag)) { + $tag = array('name' => $tag); + } - if (!is_string($tag['name']) || '' === $tag['name']) { - throw new InvalidArgumentException(sprintf('The tag name for service "%s" in %s must be a non-empty string.', $id, $file)); - } + if (!isset($tag['name'])) { + throw new InvalidArgumentException(sprintf('A "tags" entry is missing a "name" key for service "%s" in %s.', $id, $file)); + } + $name = $tag['name']; + unset($tag['name']); - $name = $tag['name']; - unset($tag['name']); + if (!is_string($name) || '' === $name) { + throw new InvalidArgumentException(sprintf('The tag name for service "%s" in %s must be a non-empty string.', $id, $file)); + } - foreach ($tag as $attribute => $value) { - if (!is_scalar($value) && null !== $value) { - throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in %s. Check your YAML syntax.', $id, $name, $attribute, $file)); - } + foreach ($tag as $attribute => $value) { + if (!is_scalar($value) && null !== $value) { + throw new InvalidArgumentException(sprintf('A "tags" attribute must be of a scalar-type for service "%s", tag "%s", attribute "%s" in %s. Check your YAML syntax.', $id, $name, $attribute, $file)); } - - $definition->addTag($name, $tag); } + + $definition->addTag($name, $tag); } if (isset($service['decorates'])) { @@ -312,25 +519,81 @@ private function parseDefinition($id, $service, $file) $definition->setAutowired($service['autowire']); } - if (isset($service['autowiring_types'])) { - if (is_string($service['autowiring_types'])) { - $definition->addAutowiringType($service['autowiring_types']); - } else { - if (!is_array($service['autowiring_types'])) { - throw new InvalidArgumentException(sprintf('Parameter "autowiring_types" must be a string or an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); + if (isset($defaults['bind']) || isset($service['bind'])) { + // deep clone, to avoid multiple process of the same instance in the passes + $bindings = isset($defaults['bind']) ? unserialize(serialize($defaults['bind'])) : array(); + + if (isset($service['bind'])) { + if (!is_array($service['bind'])) { + throw new InvalidArgumentException(sprintf('Parameter "bind" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file)); } - foreach ($service['autowiring_types'] as $autowiringType) { - if (!is_string($autowiringType)) { - throw new InvalidArgumentException(sprintf('A "autowiring_types" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file)); - } + $bindings = array_merge($bindings, $this->resolveServices($service['bind'], $file)); + } - $definition->addAutowiringType($autowiringType); - } + $definition->setBindings($bindings); + } + + if (isset($service['autoconfigure'])) { + if (!$definition instanceof ChildDefinition) { + $definition->setAutoconfigured($service['autoconfigure']); + } elseif ($service['autoconfigure']) { + throw new InvalidArgumentException(sprintf('The service "%s" cannot have a "parent" and also have "autoconfigure". Try setting "autoconfigure: false" for the service.', $id)); + } + } + + if (array_key_exists('resource', $service)) { + if (!is_string($service['resource'])) { + throw new InvalidArgumentException(sprintf('A "resource" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file)); + } + $exclude = isset($service['exclude']) ? $service['exclude'] : null; + $this->registerClasses($definition, $id, $service['resource'], $exclude); + } else { + $this->setDefinition($id, $definition); + } + } + + /** + * Parses a callable. + * + * @param string|array $callable A callable + * @param string $parameter A parameter (e.g. 'factory' or 'configurator') + * @param string $id A service identifier + * @param string $file A parsed file + * + * @throws InvalidArgumentException When errors are occuried + * + * @return string|array A parsed callable + */ + private function parseCallable($callable, $parameter, $id, $file) + { + if (is_string($callable)) { + if ('' !== $callable && '@' === $callable[0]) { + throw new InvalidArgumentException(sprintf('The value of the "%s" option for the "%s" service must be the id of the service without the "@" prefix (replace "%s" with "%s").', $parameter, $id, $callable, substr($callable, 1))); + } + + if (false !== strpos($callable, ':') && false === strpos($callable, '::')) { + $parts = explode(':', $callable); + + return array($this->resolveServices('@'.$parts[0], $file), $parts[1]); } + + return $callable; } - $this->container->setDefinition($id, $definition); + if (is_array($callable)) { + if (isset($callable[0]) && isset($callable[1])) { + return array($this->resolveServices($callable[0], $file), $callable[1]); + } + + if ('factory' === $parameter && isset($callable[1]) && null === $callable[0]) { + return $callable; + } + + throw new InvalidArgumentException(sprintf('Parameter "%s" must contain an array with two elements for service "%s" in %s. Check your YAML syntax.', $parameter, $id, $file)); + } + + throw new InvalidArgumentException(sprintf('Parameter "%s" must be a string or an array for service "%s" in %s. Check your YAML syntax.', $parameter, $id, $file)); } /** @@ -361,7 +624,7 @@ protected function loadFile($file) } try { - $configuration = $this->yamlParser->parse(file_get_contents($file)); + $configuration = $this->yamlParser->parse(file_get_contents($file), Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS); } catch (ParseException $e) { throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $file), 0, $e); } @@ -412,14 +675,59 @@ private function validate($content, $file) /** * Resolves services. * - * @param string|array $value + * @param mixed $value + * @param string $file + * @param bool $isParameter * - * @return array|string|Reference + * @return array|string|Reference|ArgumentInterface */ - private function resolveServices($value) + private function resolveServices($value, $file, $isParameter = false) { + if ($value instanceof TaggedValue) { + $argument = $value->getValue(); + if ('iterator' === $value->getTag()) { + if (!is_array($argument)) { + throw new InvalidArgumentException(sprintf('"!iterator" tag only accepts sequences in "%s".', $file)); + } + $argument = $this->resolveServices($argument, $file, $isParameter); + try { + return new IteratorArgument($argument); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException(sprintf('"!iterator" tag only accepts arrays of "@service" references in "%s".', $file)); + } + } + if ('service' === $value->getTag()) { + if ($isParameter) { + throw new InvalidArgumentException(sprintf('Using an anonymous service in a parameter is not allowed in "%s".', $file)); + } + + $isLoadingInstanceof = $this->isLoadingInstanceof; + $this->isLoadingInstanceof = false; + $instanceof = $this->instanceof; + $this->instanceof = array(); + + $id = sprintf('%d_%s', ++$this->anonymousServicesCount, preg_replace('/^.*\\\\/', '', isset($argument['class']) ? $argument['class'] : '').$this->anonymousServicesSuffix); + $this->parseDefinition($id, $argument, $file, array()); + + if (!$this->container->hasDefinition($id)) { + throw new InvalidArgumentException(sprintf('Creating an alias using the tag "!service" is not allowed in "%s".', $file)); + } + + $this->container->getDefinition($id)->setPublic(false); + + $this->isLoadingInstanceof = $isLoadingInstanceof; + $this->instanceof = $instanceof; + + return new Reference($id); + } + + throw new InvalidArgumentException(sprintf('Unsupported tag "!%s".', $value->getTag())); + } + if (is_array($value)) { - $value = array_map(array($this, 'resolveServices'), $value); + foreach ($value as $k => $v) { + $value[$k] = $this->resolveServices($v, $file, $isParameter); + } } elseif (is_string($value) && 0 === strpos($value, '@=')) { return new Expression(substr($value, 2)); } elseif (is_string($value) && 0 === strpos($value, '@')) { @@ -434,15 +742,8 @@ private function resolveServices($value) $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; } - if ('=' === substr($value, -1)) { - $value = substr($value, 0, -1); - $strict = false; - } else { - $strict = true; - } - if (null !== $invalidBehavior) { - $value = new Reference($value, $invalidBehavior, $strict); + $value = new Reference($value, $invalidBehavior); } } @@ -468,4 +769,28 @@ private function loadFromExtensions(array $content) $this->container->loadFromExtension($namespace, $values); } } + + /** + * Checks the keywords used to define a service. + * + * @param string $id The service name + * @param array $definition The service definition to check + * @param string $file The loaded YAML file + */ + private function checkDefinition($id, array $definition, $file) + { + if ($this->isLoadingInstanceof) { + $keywords = self::$instanceofKeywords; + } elseif ($throw = isset($definition['resource'])) { + $keywords = self::$prototypeKeywords; + } else { + $keywords = self::$serviceKeywords; + } + + foreach ($definition as $key => $value) { + if (!isset($keywords[$key])) { + throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported for definition "%s" in "%s". Allowed configuration keys are "%s".', $key, $id, $file, implode('", "', $keywords))); + } + } + } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index a8d7cf88df8f5..c2b3453ee38d9 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -52,8 +52,11 @@ Enclosing element for the definition of all services ]]> - - + + + + + @@ -76,19 +79,34 @@ + - + + + + + + + + + + + + + @@ -99,26 +117,60 @@ - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -154,7 +206,18 @@ - + + + + + + + + + + + + @@ -167,7 +230,6 @@ - @@ -192,6 +254,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php b/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php new file mode 100644 index 0000000000000..34f79477fa034 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/EnvPlaceholderParameterBag.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\ParameterBag; + +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + */ +class EnvPlaceholderParameterBag extends ParameterBag +{ + private $envPlaceholders = array(); + private $resolveEnvReferences = false; + + /** + * {@inheritdoc} + */ + public function get($name) + { + if (0 === strpos($name, 'env(') && ')' === substr($name, -1) && 'env()' !== $name) { + $env = substr($name, 4, -1); + + if (isset($this->envPlaceholders[$env])) { + foreach ($this->envPlaceholders[$env] as $placeholder) { + return $placeholder; // return first result + } + } + if (preg_match('/\W/', $env)) { + throw new InvalidArgumentException(sprintf('Invalid %s name: only "word" characters are allowed.', $name)); + } + + if ($this->has($name)) { + $defaultValue = parent::get($name); + + if (null !== $defaultValue && !is_scalar($defaultValue)) { + throw new RuntimeException(sprintf('The default value of an env() parameter must be scalar or null, but "%s" given to "%s".', gettype($defaultValue), $name)); + } + } + + $uniqueName = md5($name.uniqid(mt_rand(), true)); + $placeholder = sprintf('env_%s_%s', $env, $uniqueName); + $this->envPlaceholders[$env][$placeholder] = $placeholder; + + return $placeholder; + } + + return parent::get($name); + } + + /** + * Returns the map of env vars used in the resolved parameter values to their placeholders. + * + * @return string[][] A map of env var names to their placeholders + */ + public function getEnvPlaceholders() + { + return $this->envPlaceholders; + } + + /** + * Merges the env placeholders of another EnvPlaceholderParameterBag. + */ + public function mergeEnvPlaceholders(self $bag) + { + if ($newPlaceholders = $bag->getEnvPlaceholders()) { + $this->envPlaceholders += $newPlaceholders; + + foreach ($newPlaceholders as $env => $placeholders) { + $this->envPlaceholders[$env] += $placeholders; + } + } + } + + /** + * {@inheritdoc} + */ + public function resolve() + { + if ($this->resolved) { + return; + } + parent::resolve(); + + foreach ($this->envPlaceholders as $env => $placeholders) { + if (!isset($this->parameters[$name = strtolower("env($env)")])) { + continue; + } + if (is_numeric($default = $this->parameters[$name])) { + $this->parameters[$name] = (string) $default; + } elseif (null !== $default && !is_scalar($default)) { + throw new RuntimeException(sprintf('The default value of env parameter "%s" must be scalar or null, %s given.', $env, gettype($default))); + } + } + } + + /** + * Replaces "%env(FOO)%" references by their placeholder, keeping regular "%parameters%" references as is. + */ + public function resolveEnvReferences() + { + $this->resolveEnvReferences = true; + try { + $this->resolve(); + } finally { + $this->resolveEnvReferences = false; + } + } + + /** + * {@inheritdoc} + */ + public function resolveString($value, array $resolving = array()) + { + if ($this->resolveEnvReferences) { + return preg_replace_callback('/%%|%(env\([^%\s]+\))%/', function ($match) { return isset($match[1]) ? $this->get($match[1]) : '%%'; }, $value); + } + + return parent::resolveString($value, $resolving); + } +} diff --git a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php index 0611b1f69e322..83ba7e7076fc1 100644 --- a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php @@ -81,7 +81,23 @@ public function get($name) } } - throw new ParameterNotFoundException($name, null, null, null, $alternatives); + $nonNestedAlternative = null; + if (!count($alternatives) && false !== strpos($name, '.')) { + $namePartsLength = array_map('strlen', explode('.', $name)); + $key = substr($name, 0, -1 * (1 + array_pop($namePartsLength))); + while (count($namePartsLength)) { + if ($this->has($key)) { + if (is_array($this->get($key))) { + $nonNestedAlternative = $key; + } + break; + } + + $key = substr($key, 0, -1 * (1 + array_pop($namePartsLength))); + } + } + + throw new ParameterNotFoundException($name, null, null, null, $alternatives, $nonNestedAlternative); } return $this->parameters[$name]; @@ -189,40 +205,40 @@ public function resolveString($value, array $resolving = array()) // as the preg_replace_callback throw an exception when trying // a non-string in a parameter value if (preg_match('/^%([^%\s]+)%$/', $value, $match)) { - $key = strtolower($match[1]); + $key = $match[1]; + $lcKey = strtolower($key); - if (isset($resolving[$key])) { + if (isset($resolving[$lcKey])) { throw new ParameterCircularReferenceException(array_keys($resolving)); } - $resolving[$key] = true; + $resolving[$lcKey] = true; return $this->resolved ? $this->get($key) : $this->resolveValue($this->get($key), $resolving); } - $self = $this; - - return preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($self, $resolving, $value) { + return preg_replace_callback('/%%|%([^%\s]+)%/', function ($match) use ($resolving, $value) { // skip %% if (!isset($match[1])) { return '%%'; } - $key = strtolower($match[1]); - if (isset($resolving[$key])) { + $key = $match[1]; + $lcKey = strtolower($key); + if (isset($resolving[$lcKey])) { throw new ParameterCircularReferenceException(array_keys($resolving)); } - $resolved = $self->get($key); + $resolved = $this->get($key); if (!is_string($resolved) && !is_numeric($resolved)) { throw new RuntimeException(sprintf('A string value must be composed of strings and/or numbers, but found parameter "%s" of type %s inside string value "%s".', $key, gettype($resolved), $value)); } $resolved = (string) $resolved; - $resolving[$key] = true; + $resolving[$lcKey] = true; - return $self->isResolved() ? $resolved : $self->resolveString($resolved, $resolving); + return $this->isResolved() ? $resolved : $this->resolveString($resolved, $resolving); }, $value); } diff --git a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBagInterface.php b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBagInterface.php index 3291b373deb90..7386df06481a7 100644 --- a/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBagInterface.php +++ b/src/Symfony/Component/DependencyInjection/ParameterBag/ParameterBagInterface.php @@ -55,6 +55,13 @@ public function all(); */ public function get($name); + /** + * Removes a parameter. + * + * @param string $name The parameter name + */ + public function remove($name); + /** * Sets a service container parameter. * diff --git a/src/Symfony/Component/DependencyInjection/Reference.php b/src/Symfony/Component/DependencyInjection/Reference.php index cb2445023da01..82906d2b7524c 100644 --- a/src/Symfony/Component/DependencyInjection/Reference.php +++ b/src/Symfony/Component/DependencyInjection/Reference.php @@ -20,22 +20,17 @@ class Reference { private $id; private $invalidBehavior; - private $strict; /** - * Note: The $strict parameter is deprecated since version 2.8 and will be removed in 3.0. - * * @param string $id The service identifier * @param int $invalidBehavior The behavior when the service does not exist - * @param bool $strict Sets how this reference is validated * * @see Container */ - public function __construct($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $strict = true) + public function __construct($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) { - $this->id = strtolower($id); + $this->id = (string) $id; $this->invalidBehavior = $invalidBehavior; - $this->strict = $strict; } /** @@ -55,20 +50,4 @@ public function getInvalidBehavior() { return $this->invalidBehavior; } - - /** - * Returns true when this Reference is strict. - * - * @return bool - * - * @deprecated since version 2.8, to be removed in 3.0. - */ - public function isStrict($triggerDeprecationError = true) - { - if ($triggerDeprecationError) { - @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } - - return $this->strict; - } } diff --git a/src/Symfony/Component/DependencyInjection/Scope.php b/src/Symfony/Component/DependencyInjection/Scope.php deleted file mode 100644 index b0b8ed6c2e516..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Scope.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection; - -/** - * Scope class. - * - * @author Johannes M. Schmitt - * - * @deprecated since version 2.8, to be removed in 3.0. - */ -class Scope implements ScopeInterface -{ - private $name; - private $parentName; - - public function __construct($name, $parentName = ContainerInterface::SCOPE_CONTAINER) - { - $this->name = $name; - $this->parentName = $parentName; - } - - public function getName() - { - return $this->name; - } - - public function getParentName() - { - return $this->parentName; - } -} diff --git a/src/Symfony/Component/DependencyInjection/ScopeInterface.php b/src/Symfony/Component/DependencyInjection/ScopeInterface.php deleted file mode 100644 index 11b10973994ad..0000000000000 --- a/src/Symfony/Component/DependencyInjection/ScopeInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection; - -/** - * Scope Interface. - * - * @author Johannes M. Schmitt - * - * @deprecated since version 2.8, to be removed in 3.0. - */ -interface ScopeInterface -{ - public function getName(); - - public function getParentName(); -} diff --git a/src/Symfony/Component/DependencyInjection/ServiceLocator.php b/src/Symfony/Component/DependencyInjection/ServiceLocator.php new file mode 100644 index 0000000000000..270de6b935630 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ServiceLocator.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection; + +use Psr\Container\ContainerInterface as PsrContainerInterface; +use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; + +/** + * @author Robin Chalas + * @author Nicolas Grekas + */ +class ServiceLocator implements PsrContainerInterface +{ + private $factories; + + /** + * @param callable[] $factories + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * {@inheritdoc} + */ + public function has($id) + { + return isset($this->factories[$id]); + } + + /** + * {@inheritdoc} + */ + public function get($id) + { + if (!isset($this->factories[$id])) { + throw new ServiceNotFoundException($id, null, null, array_keys($this->factories)); + } + + if (true === $factory = $this->factories[$id]) { + throw new ServiceCircularReferenceException($id, array($id, $id)); + } + + $this->factories[$id] = true; + try { + return $factory(); + } finally { + $this->factories[$id] = $factory; + } + } + + public function __invoke($id) + { + return isset($this->factories[$id]) ? $this->get($id) : null; + } +} diff --git a/src/Symfony/Component/DependencyInjection/ServiceSubscriberInterface.php b/src/Symfony/Component/DependencyInjection/ServiceSubscriberInterface.php new file mode 100644 index 0000000000000..7024484bd8aee --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ServiceSubscriberInterface.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection; + +/** + * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. + * + * The getSubscribedServices method returns an array of service types required by such instances, + * optionally keyed by the service names used internally. Service types that start with an interrogation + * mark "?" are optional, while the other ones are mandatory service dependencies. + * + * The injected service locators SHOULD NOT allow access to any other services not specified by the method. + * + * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. + * This interface does not dictate any injection method for these service locators, although constructor + * injection is recommended. + * + * @author Nicolas Grekas + */ +interface ServiceSubscriberInterface +{ + /** + * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * + * For mandatory dependencies: + * + * * array('logger' => 'Psr\Log\LoggerInterface') means the objects use the "logger" name + * internally to fetch a service which must implement Psr\Log\LoggerInterface. + * * array('Psr\Log\LoggerInterface') is a shortcut for + * * array('Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface') + * + * otherwise: + * + * * array('logger' => '?Psr\Log\LoggerInterface') denotes an optional dependency + * * array('?Psr\Log\LoggerInterface') is a shortcut for + * * array('Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface') + * + * @return array The required service types, optionally keyed by service names + */ + public static function getSubscribedServices(); +} diff --git a/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php b/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php deleted file mode 100644 index 87c67c4d7e7d5..0000000000000 --- a/src/Symfony/Component/DependencyInjection/SimpleXMLElement.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection; - -@trigger_error('The '.__NAMESPACE__.'\SimpleXMLElement class is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED); - -use Symfony\Component\Config\Util\XmlUtils; -use Symfony\Component\ExpressionLanguage\Expression; - -/** - * SimpleXMLElement class. - * - * @author Fabien Potencier - * - * @deprecated since version 2.5, to be removed in 3.0. - */ -class SimpleXMLElement extends \SimpleXMLElement -{ - /** - * Converts an attribute as a PHP type. - * - * @param string $name - * - * @return mixed - */ - public function getAttributeAsPhp($name) - { - return self::phpize($this[$name]); - } - - /** - * Returns arguments as valid PHP types. - * - * @param string $name - * @param bool $lowercase - * - * @return mixed - */ - public function getArgumentsAsPhp($name, $lowercase = true) - { - $arguments = array(); - foreach ($this->$name as $arg) { - if (isset($arg['name'])) { - $arg['key'] = (string) $arg['name']; - } - $key = isset($arg['key']) ? (string) $arg['key'] : (!$arguments ? 0 : max(array_keys($arguments)) + 1); - - // parameter keys are case insensitive - if ('parameter' == $name && $lowercase) { - $key = strtolower($key); - } - - // this is used by DefinitionDecorator to overwrite a specific - // argument of the parent definition - if (isset($arg['index'])) { - $key = 'index_'.$arg['index']; - } - - switch ($arg['type']) { - case 'service': - $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; - if (isset($arg['on-invalid']) && 'ignore' == $arg['on-invalid']) { - $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; - } elseif (isset($arg['on-invalid']) && 'null' == $arg['on-invalid']) { - $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; - } - - if (isset($arg['strict'])) { - $strict = self::phpize($arg['strict']); - } else { - $strict = true; - } - - $arguments[$key] = new Reference((string) $arg['id'], $invalidBehavior, $strict); - break; - case 'expression': - $arguments[$key] = new Expression((string) $arg); - break; - case 'collection': - $arguments[$key] = $arg->getArgumentsAsPhp($name, false); - break; - case 'string': - $arguments[$key] = (string) $arg; - break; - case 'constant': - $arguments[$key] = constant((string) $arg); - break; - default: - $arguments[$key] = self::phpize($arg); - } - } - - return $arguments; - } - - /** - * Converts an xml value to a PHP type. - * - * @param mixed $value - * - * @return mixed - */ - public static function phpize($value) - { - return XmlUtils::phpize($value); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Argument/RewindableGeneratorTest.php b/src/Symfony/Component/DependencyInjection/Tests/Argument/RewindableGeneratorTest.php new file mode 100644 index 0000000000000..1415869a4f1e9 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Argument/RewindableGeneratorTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Argument; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; + +class RewindableGeneratorTest extends TestCase +{ + public function testImplementsCountable() + { + $this->assertInstanceOf(\Countable::class, new RewindableGenerator(function () { + yield 1; + }, 1)); + } + + public function testCountUsesProvidedValue() + { + $generator = new RewindableGenerator(function () { + yield 1; + }, 3); + + $this->assertCount(3, $generator); + } + + public function testCountUsesProvidedValueAsCallback() + { + $called = 0; + $generator = new RewindableGenerator(function () { + yield 1; + }, function () use (&$called) { + ++$called; + + return 3; + }); + + $this->assertSame(0, $called, 'Count callback is called lazily'); + $this->assertCount(3, $generator); + + count($generator); + + $this->assertSame(1, $called, 'Count callback is called only once'); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/ChildDefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/ChildDefinitionTest.php new file mode 100644 index 0000000000000..7f359b94f5311 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/ChildDefinitionTest.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ChildDefinition; + +class ChildDefinitionTest extends TestCase +{ + public function testConstructor() + { + $def = new ChildDefinition('foo'); + + $this->assertSame('foo', $def->getParent()); + $this->assertSame(array(), $def->getChanges()); + } + + /** + * @dataProvider getPropertyTests + */ + public function testSetProperty($property, $changeKey) + { + $def = new ChildDefinition('foo'); + + $getter = 'get'.ucfirst($property); + $setter = 'set'.ucfirst($property); + + $this->assertNull($def->$getter()); + $this->assertSame($def, $def->$setter('foo')); + $this->assertSame('foo', $def->$getter()); + $this->assertSame(array($changeKey => true), $def->getChanges()); + } + + public function getPropertyTests() + { + return array( + array('class', 'class'), + array('factory', 'factory'), + array('configurator', 'configurator'), + array('file', 'file'), + ); + } + + public function testSetPublic() + { + $def = new ChildDefinition('foo'); + + $this->assertTrue($def->isPublic()); + $this->assertSame($def, $def->setPublic(false)); + $this->assertFalse($def->isPublic()); + $this->assertSame(array('public' => true), $def->getChanges()); + } + + public function testSetLazy() + { + $def = new ChildDefinition('foo'); + + $this->assertFalse($def->isLazy()); + $this->assertSame($def, $def->setLazy(false)); + $this->assertFalse($def->isLazy()); + $this->assertSame(array('lazy' => true), $def->getChanges()); + } + + public function testSetAutowired() + { + $def = new ChildDefinition('foo'); + + $this->assertFalse($def->isAutowired()); + $this->assertSame($def, $def->setAutowired(true)); + $this->assertTrue($def->isAutowired()); + $this->assertSame(array('autowired' => true), $def->getChanges()); + } + + public function testSetArgument() + { + $def = new ChildDefinition('foo'); + + $this->assertSame(array(), $def->getArguments()); + $this->assertSame($def, $def->replaceArgument(0, 'foo')); + $this->assertSame(array('index_0' => 'foo'), $def->getArguments()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testReplaceArgumentShouldRequireIntegerIndex() + { + $def = new ChildDefinition('foo'); + + $def->replaceArgument('0', 'foo'); + } + + public function testReplaceArgument() + { + $def = new ChildDefinition('foo'); + + $def->setArguments(array(0 => 'foo', 1 => 'bar')); + $this->assertSame('foo', $def->getArgument(0)); + $this->assertSame('bar', $def->getArgument(1)); + + $this->assertSame($def, $def->replaceArgument(1, 'baz')); + $this->assertSame('foo', $def->getArgument(0)); + $this->assertSame('baz', $def->getArgument(1)); + + $this->assertSame(array(0 => 'foo', 1 => 'bar', 'index_1' => 'baz'), $def->getArguments()); + + $this->assertSame($def, $def->replaceArgument('$bar', 'val')); + $this->assertSame('val', $def->getArgument('$bar')); + $this->assertSame(array(0 => 'foo', 1 => 'bar', 'index_1' => 'baz', '$bar' => 'val'), $def->getArguments()); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testGetArgumentShouldCheckBounds() + { + $def = new ChildDefinition('foo'); + + $def->setArguments(array(0 => 'foo')); + $def->replaceArgument(0, 'foo'); + + $def->getArgument(1); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\BadMethodCallException + */ + public function testCannotCallSetAutoconfigured() + { + $def = new ChildDefinition('foo'); + $def->setAutoconfigured(true); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\BadMethodCallException + */ + public function testCannotCallSetInstanceofConditionals() + { + $def = new ChildDefinition('foo'); + $def->setInstanceofConditionals(array('Foo' => new ChildDefinition(''))); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php index 1c374662ff2e1..7a06e10d9ffad 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; use Symfony\Component\DependencyInjection\Compiler\RepeatedPass; @@ -60,6 +61,54 @@ public function testProcess() $this->assertSame($ref6, $edges[3]->getValue()); } + public function testProcessMarksEdgesLazyWhenReferencedServiceIsLazy() + { + $container = new ContainerBuilder(); + + $container + ->register('a') + ->setLazy(true) + ->addArgument($ref1 = new Reference('b')) + ; + + $container + ->register('b') + ->addArgument($ref2 = new Reference('a')) + ; + + $graph = $this->process($container); + + $this->assertCount(1, $graph->getNode('b')->getInEdges()); + $this->assertCount(1, $edges = $graph->getNode('a')->getInEdges()); + + $this->assertSame($ref2, $edges[0]->getValue()); + $this->assertTrue($edges[0]->isLazy()); + } + + public function testProcessMarksEdgesLazyWhenReferencedFromIteratorArgument() + { + $container = new ContainerBuilder(); + $container->register('a'); + $container->register('b'); + + $container + ->register('c') + ->addArgument($ref1 = new Reference('a')) + ->addArgument(new IteratorArgument(array($ref2 = new Reference('b')))) + ; + + $graph = $this->process($container); + + $this->assertCount(1, $graph->getNode('a')->getInEdges()); + $this->assertCount(1, $graph->getNode('b')->getInEdges()); + $this->assertCount(2, $edges = $graph->getNode('c')->getOutEdges()); + + $this->assertSame($ref1, $edges[0]->getValue()); + $this->assertFalse($edges[0]->isLazy()); + $this->assertSame($ref2, $edges[1]->getValue()); + $this->assertTrue($edges[1]->isLazy()); + } + public function testProcessDetectsReferencesFromInlinedDefinitions() { $container = new ContainerBuilder(); @@ -79,6 +128,25 @@ public function testProcessDetectsReferencesFromInlinedDefinitions() $this->assertSame($ref, $refs[0]->getValue()); } + public function testProcessDetectsReferencesFromIteratorArguments() + { + $container = new ContainerBuilder(); + + $container + ->register('a') + ; + + $container + ->register('b') + ->addArgument(new IteratorArgument(array($ref = new Reference('a')))) + ; + + $graph = $this->process($container); + + $this->assertCount(1, $refs = $graph->getNode('a')->getInEdges()); + $this->assertSame($ref, $refs[0]->getValue()); + } + public function testProcessDetectsReferencesFromInlinedFactoryDefinitions() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutoAliasServicePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutoAliasServicePassTest.php index 281634225acaa..f76001a11abf7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutoAliasServicePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutoAliasServicePassTest.php @@ -58,7 +58,6 @@ public function testProcessWithNonExistingAlias() $pass->process($container); $this->assertEquals('Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassDefault', $container->getDefinition('example')->getClass()); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassDefault', $container->get('example')); } public function testProcessWithExistingAlias() @@ -76,7 +75,7 @@ public function testProcessWithExistingAlias() $this->assertTrue($container->hasAlias('example')); $this->assertEquals('mysql.example', $container->getAlias('example')); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassMysql', $container->get('example')); + $this->assertSame('Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassMysql', $container->getDefinition('mysql.example')->getClass()); } public function testProcessWithManualAlias() @@ -87,7 +86,7 @@ public function testProcessWithManualAlias() ->addTag('auto_alias', array('format' => '%existing%.example')); $container->register('mysql.example', 'Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassMysql'); - $container->register('mariadb.example', 'Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassMariadb'); + $container->register('mariadb.example', 'Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassMariaDb'); $container->setAlias('example', 'mariadb.example'); $container->setParameter('existing', 'mysql'); @@ -96,7 +95,7 @@ public function testProcessWithManualAlias() $this->assertTrue($container->hasAlias('example')); $this->assertEquals('mariadb.example', $container->getAlias('example')); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassMariaDb', $container->get('example')); + $this->assertSame('Symfony\Component\DependencyInjection\Tests\Compiler\ServiceClassMariaDb', $container->getDefinition('mariadb.example')->getClass()); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireExceptionPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireExceptionPassTest.php new file mode 100644 index 0000000000000..4f0b3d6c2566c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowireExceptionPassTest.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\AutowireExceptionPass; +use Symfony\Component\DependencyInjection\Compiler\AutowirePass; +use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; + +class AutowireExceptionPassTest extends TestCase +{ + public function testThrowsException() + { + $autowirePass = $this->getMockBuilder(AutowirePass::class) + ->getMock(); + + $autowireException = new AutowiringFailedException('foo_service_id', 'An autowiring exception message'); + $autowirePass->expects($this->any()) + ->method('getAutowiringExceptions') + ->will($this->returnValue(array($autowireException))); + + $inlinePass = $this->getMockBuilder(InlineServiceDefinitionsPass::class) + ->getMock(); + $inlinePass->expects($this->any()) + ->method('getInlinedServiceIds') + ->will($this->returnValue(array())); + + $container = new ContainerBuilder(); + $container->register('foo_service_id'); + + $pass = new AutowireExceptionPass($autowirePass, $inlinePass); + + try { + $pass->process($container); + $this->fail('->process() should throw the exception if the service id exists'); + } catch (\Exception $e) { + $this->assertSame($autowireException, $e); + } + } + + public function testThrowExceptionIfServiceInlined() + { + $autowirePass = $this->getMockBuilder(AutowirePass::class) + ->getMock(); + + $autowireException = new AutowiringFailedException('a_service', 'An autowiring exception message'); + $autowirePass->expects($this->any()) + ->method('getAutowiringExceptions') + ->will($this->returnValue(array($autowireException))); + + $inlinePass = $this->getMockBuilder(InlineServiceDefinitionsPass::class) + ->getMock(); + $inlinePass->expects($this->any()) + ->method('getInlinedServiceIds') + ->will($this->returnValue(array( + // a_service inlined into b_service + 'a_service' => array('b_service'), + // b_service inlined into c_service + 'b_service' => array('c_service'), + ))); + + $container = new ContainerBuilder(); + // ONLY register c_service in the final container + $container->register('c_service', 'stdClass'); + + $pass = new AutowireExceptionPass($autowirePass, $inlinePass); + + try { + $pass->process($container); + $this->fail('->process() should throw the exception if the service id exists'); + } catch (\Exception $e) { + $this->assertSame($autowireException, $e); + } + } + + public function testDoNotThrowExceptionIfServiceInlinedButRemoved() + { + $autowirePass = $this->getMockBuilder(AutowirePass::class) + ->getMock(); + + $autowireException = new AutowiringFailedException('a_service', 'An autowiring exception message'); + $autowirePass->expects($this->any()) + ->method('getAutowiringExceptions') + ->will($this->returnValue(array($autowireException))); + + $inlinePass = $this->getMockBuilder(InlineServiceDefinitionsPass::class) + ->getMock(); + $inlinePass->expects($this->any()) + ->method('getInlinedServiceIds') + ->will($this->returnValue(array( + // a_service inlined into b_service + 'a_service' => array('b_service'), + // b_service inlined into c_service + 'b_service' => array('c_service'), + ))); + + // do NOT register c_service in the container + $container = new ContainerBuilder(); + + $pass = new AutowireExceptionPass($autowirePass, $inlinePass); + + $pass->process($container); + // mark the test as passed + $this->assertTrue(true); + } + + public function testNoExceptionIfServiceRemoved() + { + $autowirePass = $this->getMockBuilder(AutowirePass::class) + ->getMock(); + + $autowireException = new AutowiringFailedException('non_existent_service'); + $autowirePass->expects($this->any()) + ->method('getAutowiringExceptions') + ->will($this->returnValue(array($autowireException))); + + $inlinePass = $this->getMockBuilder(InlineServiceDefinitionsPass::class) + ->getMock(); + $inlinePass->expects($this->any()) + ->method('getInlinedServiceIds') + ->will($this->returnValue(array())); + + $container = new ContainerBuilder(); + + $pass = new AutowireExceptionPass($autowirePass, $inlinePass); + + $pass->process($container); + // mark the test as passed + $this->assertTrue(true); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index ad89c5b9b1679..3203c32e2d566 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -13,8 +13,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Compiler\AutowirePass; +use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic; +use Symfony\Component\DependencyInjection\TypedReference; /** * @author Kévin Dunglas @@ -25,47 +29,69 @@ public function testProcess() { $container = new ContainerBuilder(); - $container->register('foo', __NAMESPACE__.'\Foo'); + $container->register(Foo::class); $barDefinition = $container->register('bar', __NAMESPACE__.'\Bar'); $barDefinition->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $this->assertCount(1, $container->getDefinition('bar')->getArguments()); - $this->assertEquals('foo', (string) $container->getDefinition('bar')->getArgument(0)); + $this->assertEquals(Foo::class, (string) $container->getDefinition('bar')->getArgument(0)); + } + + public function testProcessVariadic() + { + $container = new ContainerBuilder(); + $container->register(Foo::class); + $definition = $container->register('fooVariadic', FooVariadic::class); + $definition->setAutowired(true); + + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); + + $this->assertCount(1, $container->getDefinition('fooVariadic')->getArguments()); + $this->assertEquals(Foo::class, (string) $container->getDefinition('fooVariadic')->getArgument(0)); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Cannot autowire service "c": argument "$a" of method "Symfony\Component\DependencyInjection\Tests\Compiler\C::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\A" but no such service exists. You should maybe alias this class to the existing "Symfony\Component\DependencyInjection\Tests\Compiler\B" service. + */ public function testProcessAutowireParent() { $container = new ContainerBuilder(); - $container->register('b', __NAMESPACE__.'\B'); + $container->register(B::class); $cDefinition = $container->register('c', __NAMESPACE__.'\C'); $cDefinition->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $this->assertCount(1, $container->getDefinition('c')->getArguments()); - $this->assertEquals('b', (string) $container->getDefinition('c')->getArgument(0)); + $this->assertEquals(B::class, (string) $container->getDefinition('c')->getArgument(0)); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Cannot autowire service "g": argument "$d" of method "Symfony\Component\DependencyInjection\Tests\Compiler\G::__construct()" references interface "Symfony\Component\DependencyInjection\Tests\Compiler\DInterface" but no such service exists. You should maybe alias this interface to the existing "Symfony\Component\DependencyInjection\Tests\Compiler\F" service. + */ public function testProcessAutowireInterface() { $container = new ContainerBuilder(); - $container->register('f', __NAMESPACE__.'\F'); + $container->register(F::class); $gDefinition = $container->register('g', __NAMESPACE__.'\G'); $gDefinition->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $this->assertCount(3, $container->getDefinition('g')->getArguments()); - $this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(0)); - $this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(1)); - $this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(2)); + $this->assertEquals(F::class, (string) $container->getDefinition('g')->getArgument(0)); + $this->assertEquals(F::class, (string) $container->getDefinition('g')->getArgument(1)); + $this->assertEquals(F::class, (string) $container->getDefinition('g')->getArgument(2)); } public function testCompleteExistingDefinition() @@ -73,38 +99,67 @@ public function testCompleteExistingDefinition() $container = new ContainerBuilder(); $container->register('b', __NAMESPACE__.'\B'); - $container->register('f', __NAMESPACE__.'\F'); + $container->register(DInterface::class, F::class); $hDefinition = $container->register('h', __NAMESPACE__.'\H')->addArgument(new Reference('b')); $hDefinition->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $this->assertCount(2, $container->getDefinition('h')->getArguments()); $this->assertEquals('b', (string) $container->getDefinition('h')->getArgument(0)); - $this->assertEquals('f', (string) $container->getDefinition('h')->getArgument(1)); + $this->assertEquals(DInterface::class, (string) $container->getDefinition('h')->getArgument(1)); } public function testCompleteExistingDefinitionWithNotDefinedArguments() { $container = new ContainerBuilder(); - $container->register('b', __NAMESPACE__.'\B'); - $container->register('f', __NAMESPACE__.'\F'); + $container->register(B::class); + $container->register(DInterface::class, F::class); $hDefinition = $container->register('h', __NAMESPACE__.'\H')->addArgument('')->addArgument(''); $hDefinition->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $this->assertCount(2, $container->getDefinition('h')->getArguments()); - $this->assertEquals('b', (string) $container->getDefinition('h')->getArgument(0)); - $this->assertEquals('f', (string) $container->getDefinition('h')->getArgument(1)); + $this->assertEquals(B::class, (string) $container->getDefinition('h')->getArgument(0)); + $this->assertEquals(DInterface::class, (string) $container->getDefinition('h')->getArgument(1)); + } + + public function testExceptionsAreStored() + { + $container = new ContainerBuilder(); + + $container->register('c1', __NAMESPACE__.'\CollisionA'); + $container->register('c2', __NAMESPACE__.'\CollisionB'); + $container->register('c3', __NAMESPACE__.'\CollisionB'); + $aDefinition = $container->register('a', __NAMESPACE__.'\CannotBeAutowired'); + $aDefinition->setAutowired(true); + + $pass = new AutowirePass(false); + $pass->process($container); + $this->assertCount(1, $pass->getAutowiringExceptions()); } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" for the service "a". Multiple services exist for this interface (c1, c2, c3). + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Unable to resolve service "private_service": constructor of class "Symfony\Component\DependencyInjection\Tests\Compiler\PrivateConstructor" must be public. + */ + public function testPrivateConstructorThrowsAutowireException() + { + $container = new ContainerBuilder(); + + $container->autowire('private_service', __NAMESPACE__.'\PrivateConstructor'); + + $pass = new AutowirePass(true); + $pass->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "a": argument "$collision" of method "Symfony\Component\DependencyInjection\Tests\Compiler\CannotBeAutowired::__construct()" references interface "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" but no such service exists. You should maybe alias this interface to one of these existing services: "c1", "c2", "c3". */ public function testTypeCollision() { @@ -121,8 +176,8 @@ public function testTypeCollision() } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\Foo" for the service "a". Multiple services exist for this class (a1, a2). + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "a": argument "$k" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotGuessableArgument::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\Foo" but no such service exists. You should maybe alias this class to one of these existing services: "a1", "a2". */ public function testTypeNotGuessable() { @@ -138,8 +193,8 @@ public function testTypeNotGuessable() } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\A" for the service "a". Multiple services exist for this class (a1, a2). + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "a": argument "$k" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotGuessableArgumentForSubclass::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\A" but no such service exists. You should maybe alias this class to one of these existing services: "a1", "a2". */ public function testTypeNotGuessableWithSubclass() { @@ -155,8 +210,8 @@ public function testTypeNotGuessableWithSubclass() } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" for the service "a". No services were found matching this interface and it cannot be auto-registered. + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "a": argument "$collision" of method "Symfony\Component\DependencyInjection\Tests\Compiler\CannotBeAutowired::__construct()" references interface "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" but no such service exists. */ public function testTypeNotGuessableNoServicesFound() { @@ -175,7 +230,7 @@ public function testTypeNotGuessableWithTypeSet() $container->register('a1', __NAMESPACE__.'\Foo'); $container->register('a2', __NAMESPACE__.'\Foo'); - $container->register('a3', __NAMESPACE__.'\Foo')->addAutowiringType(__NAMESPACE__.'\Foo'); + $container->register(Foo::class, Foo::class); $aDefinition = $container->register('a', __NAMESPACE__.'\NotGuessableArgument'); $aDefinition->setAutowired(true); @@ -183,7 +238,7 @@ public function testTypeNotGuessableWithTypeSet() $pass->process($container); $this->assertCount(1, $container->getDefinition('a')->getArguments()); - $this->assertEquals('a3', (string) $container->getDefinition('a')->getArgument(0)); + $this->assertEquals(Foo::class, (string) $container->getDefinition('a')->getArgument(0)); } public function testWithTypeSet() @@ -191,7 +246,8 @@ public function testWithTypeSet() $container = new ContainerBuilder(); $container->register('c1', __NAMESPACE__.'\CollisionA'); - $container->register('c2', __NAMESPACE__.'\CollisionB')->addAutowiringType(__NAMESPACE__.'\CollisionInterface'); + $container->register('c2', __NAMESPACE__.'\CollisionB'); + $container->setAlias(CollisionInterface::class, 'c2'); $aDefinition = $container->register('a', __NAMESPACE__.'\CannotBeAutowired'); $aDefinition->setAutowired(true); @@ -199,9 +255,14 @@ public function testWithTypeSet() $pass->process($container); $this->assertCount(1, $container->getDefinition('a')->getArguments()); - $this->assertEquals('c2', (string) $container->getDefinition('a')->getArgument(0)); + $this->assertEquals(CollisionInterface::class, (string) $container->getDefinition('a')->getArgument(0)); } + /** + * @group legacy + * @expectedDeprecation Relying on service auto-registration for type "Symfony\Component\DependencyInjection\Tests\Compiler\Lille" is deprecated since version 3.4 and won't be supported in 4.0. Create a service named "Symfony\Component\DependencyInjection\Tests\Compiler\Lille" instead. + * @expectedDeprecation Relying on service auto-registration for type "Symfony\Component\DependencyInjection\Tests\Compiler\Dunglas" is deprecated since version 3.4 and won't be supported in 4.0. Create a service named "Symfony\Component\DependencyInjection\Tests\Compiler\Dunglas" instead. + */ public function testCreateDefinition() { $container = new ContainerBuilder(); @@ -213,14 +274,14 @@ public function testCreateDefinition() $pass->process($container); $this->assertCount(2, $container->getDefinition('coop_tilleuls')->getArguments()); - $this->assertEquals('autowired.symfony\component\dependencyinjection\tests\compiler\dunglas', $container->getDefinition('coop_tilleuls')->getArgument(0)); - $this->assertEquals('autowired.symfony\component\dependencyinjection\tests\compiler\dunglas', $container->getDefinition('coop_tilleuls')->getArgument(1)); + $this->assertEquals('autowired.Symfony\Component\DependencyInjection\Tests\Compiler\Dunglas', $container->getDefinition('coop_tilleuls')->getArgument(0)); + $this->assertEquals('autowired.Symfony\Component\DependencyInjection\Tests\Compiler\Dunglas', $container->getDefinition('coop_tilleuls')->getArgument(1)); $dunglasDefinition = $container->getDefinition('autowired.Symfony\Component\DependencyInjection\Tests\Compiler\Dunglas'); $this->assertEquals(__NAMESPACE__.'\Dunglas', $dunglasDefinition->getClass()); $this->assertFalse($dunglasDefinition->isPublic()); $this->assertCount(1, $dunglasDefinition->getArguments()); - $this->assertEquals('autowired.symfony\component\dependencyinjection\tests\compiler\lille', $dunglasDefinition->getArgument(0)); + $this->assertEquals('autowired.Symfony\Component\DependencyInjection\Tests\Compiler\Lille', $dunglasDefinition->getArgument(0)); $lilleDefinition = $container->getDefinition('autowired.Symfony\Component\DependencyInjection\Tests\Compiler\Lille'); $this->assertEquals(__NAMESPACE__.'\Lille', $lilleDefinition->getClass()); @@ -230,51 +291,51 @@ public function testResolveParameter() { $container = new ContainerBuilder(); - $container->setParameter('class_name', __NAMESPACE__.'\Foo'); - $container->register('foo', '%class_name%'); - $barDefinition = $container->register('bar', __NAMESPACE__.'\Bar'); + $container->setParameter('class_name', Bar::class); + $container->register(Foo::class); + $barDefinition = $container->register('bar', '%class_name%'); $barDefinition->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); - $this->assertEquals('foo', $container->getDefinition('bar')->getArgument(0)); + $this->assertEquals(Foo::class, $container->getDefinition('bar')->getArgument(0)); } public function testOptionalParameter() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A'); - $container->register('foo', __NAMESPACE__.'\Foo'); + $container->register(A::class); + $container->register(Foo::class); $optDefinition = $container->register('opt', __NAMESPACE__.'\OptionalParameter'); $optDefinition->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $definition = $container->getDefinition('opt'); $this->assertNull($definition->getArgument(0)); - $this->assertEquals('a', $definition->getArgument(1)); - $this->assertEquals('foo', $definition->getArgument(2)); + $this->assertEquals(A::class, $definition->getArgument(1)); + $this->assertEquals(Foo::class, $definition->getArgument(2)); } public function testDontTriggerAutowiring() { $container = new ContainerBuilder(); - $container->register('foo', __NAMESPACE__.'\Foo'); + $container->register(Foo::class); $container->register('bar', __NAMESPACE__.'\Bar'); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $this->assertCount(0, $container->getDefinition('bar')->getArguments()); } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Cannot autowire argument 2 for Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument because the type-hinted class does not exist (Class Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass does not exist). + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class cannot be loaded. */ public function testClassNotFoundThrowsException() { @@ -283,13 +344,15 @@ public function testClassNotFoundThrowsException() $aDefinition = $container->register('a', __NAMESPACE__.'\BadTypeHintedArgument'); $aDefinition->setAutowired(true); + $container->register(Dunglas::class, Dunglas::class); + $pass = new AutowirePass(); $pass->process($container); } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Cannot autowire argument 2 for Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument because the type-hinted class does not exist (Class Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass does not exist). + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "a": argument "$r" of method "Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument::__construct()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass" but this class cannot be loaded. */ public function testParentClassNotFoundThrowsException() { @@ -298,32 +361,35 @@ public function testParentClassNotFoundThrowsException() $aDefinition = $container->register('a', __NAMESPACE__.'\BadParentTypeHintedArgument'); $aDefinition->setAutowired(true); + $container->register(Dunglas::class, Dunglas::class); + $pass = new AutowirePass(); $pass->process($container); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "bar": argument "$foo" of method "Symfony\Component\DependencyInjection\Tests\Compiler\Bar::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\Foo" but this service is abstract. You should maybe alias this class to the existing "foo" service. + */ public function testDontUseAbstractServices() { $container = new ContainerBuilder(); - $container->register('abstract_foo', __NAMESPACE__.'\Foo')->setAbstract(true); + $container->register(Foo::class)->setAbstract(true); $container->register('foo', __NAMESPACE__.'\Foo'); $container->register('bar', __NAMESPACE__.'\Bar')->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); - - $arguments = $container->getDefinition('bar')->getArguments(); - $this->assertSame('foo', (string) $arguments[0]); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); } public function testSomeSpecificArgumentsAreSet() { $container = new ContainerBuilder(); - $container->register('foo', __NAMESPACE__.'\Foo'); - $container->register('a', __NAMESPACE__.'\A'); - $container->register('dunglas', __NAMESPACE__.'\Dunglas'); + $container->register('foo', Foo::class); + $container->register(A::class); + $container->register(Dunglas::class); $container->register('multiple', __NAMESPACE__.'\MultipleArguments') ->setAutowired(true) // set the 2nd (index 1) argument only: autowire the first and third @@ -332,75 +398,71 @@ public function testSomeSpecificArgumentsAreSet() 1 => new Reference('foo'), )); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $definition = $container->getDefinition('multiple'); $this->assertEquals( array( - new Reference('a'), + new TypedReference(A::class, A::class, MultipleArguments::class), new Reference('foo'), - new Reference('dunglas'), + new TypedReference(Dunglas::class, Dunglas::class, MultipleArguments::class), ), $definition->getArguments() ); } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Unable to autowire argument index 1 ($foo) for the service "arg_no_type_hint". If this is an object, give it a type-hint. Otherwise, specify this argument's value explicitly. + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "arg_no_type_hint": argument "$foo" of method "Symfony\Component\DependencyInjection\Tests\Compiler\MultipleArguments::__construct()" must have a type-hint or be given a value explicitly. */ public function testScalarArgsCannotBeAutowired() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A'); - $container->register('dunglas', __NAMESPACE__.'\Dunglas'); + $container->register(A::class); + $container->register(Dunglas::class); $container->register('arg_no_type_hint', __NAMESPACE__.'\MultipleArguments') ->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); - - $container->getDefinition('arg_no_type_hint'); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Unable to autowire argument index 1 ($foo) for the service "not_really_optional_scalar". If this is an object, give it a type-hint. Otherwise, specify this argument's value explicitly. - */ - public function testOptionalScalarNotReallyOptionalThrowException() + public function testOptionalScalarNotReallyOptionalUsesDefaultValue() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A'); - $container->register('lille', __NAMESPACE__.'\Lille'); - $container->register('not_really_optional_scalar', __NAMESPACE__.'\MultipleArgumentsOptionalScalarNotReallyOptional') + $container->register(A::class); + $container->register(Lille::class); + $definition = $container->register('not_really_optional_scalar', __NAMESPACE__.'\MultipleArgumentsOptionalScalarNotReallyOptional') ->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); + + $this->assertSame('default_val', $definition->getArgument(1)); } public function testOptionalScalarArgsDontMessUpOrder() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A'); - $container->register('lille', __NAMESPACE__.'\Lille'); + $container->register(A::class); + $container->register(Lille::class); $container->register('with_optional_scalar', __NAMESPACE__.'\MultipleArgumentsOptionalScalar') ->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $definition = $container->getDefinition('with_optional_scalar'); $this->assertEquals( array( - new Reference('a'), + new TypedReference(A::class, A::class, MultipleArgumentsOptionalScalar::class), // use the default value 'default_val', - new Reference('lille'), + new TypedReference(Lille::class, Lille::class), ), $definition->getArguments() ); @@ -410,24 +472,134 @@ public function testOptionalScalarArgsNotPassedIfLast() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A'); - $container->register('lille', __NAMESPACE__.'\Lille'); + $container->register(A::class); + $container->register(Lille::class); $container->register('with_optional_scalar_last', __NAMESPACE__.'\MultipleArgumentsOptionalScalarLast') ->setAutowired(true); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); $definition = $container->getDefinition('with_optional_scalar_last'); $this->assertEquals( array( - new Reference('a'), - new Reference('lille'), + new TypedReference(A::class, A::class, MultipleArgumentsOptionalScalarLast::class), + new TypedReference(Lille::class, Lille::class, MultipleArgumentsOptionalScalarLast::class), ), $definition->getArguments() ); } + public function testOptionalArgsNoRequiredForCoreClasses() + { + $container = new ContainerBuilder(); + + $container->register('foo', \SplFileObject::class) + ->addArgument('foo.txt') + ->setAutowired(true); + + (new AutowirePass())->process($container); + + $definition = $container->getDefinition('foo'); + $this->assertEquals( + array('foo.txt'), + $definition->getArguments() + ); + } + + public function testSetterInjection() + { + $container = new ContainerBuilder(); + $container->register(Foo::class); + $container->register(A::class); + $container->register(CollisionA::class); + $container->register(CollisionB::class); + + // manually configure *one* call, to override autowiring + $container + ->register('setter_injection', SetterInjection::class) + ->setAutowired(true) + ->addMethodCall('setWithCallsConfigured', array('manual_arg1', 'manual_arg2')) + ; + + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); + + $methodCalls = $container->getDefinition('setter_injection')->getMethodCalls(); + + $this->assertEquals( + array('setWithCallsConfigured', 'setFoo', 'setDependencies', 'setChildMethodWithoutDocBlock'), + array_column($methodCalls, 0) + ); + + // test setWithCallsConfigured args + $this->assertEquals( + array('manual_arg1', 'manual_arg2'), + $methodCalls[0][1] + ); + // test setFoo args + $this->assertEquals( + array(new TypedReference(Foo::class, Foo::class, SetterInjection::class)), + $methodCalls[1][1] + ); + } + + public function testExplicitMethodInjection() + { + $container = new ContainerBuilder(); + $container->register(Foo::class); + $container->register(A::class); + $container->register(CollisionA::class); + $container->register(CollisionB::class); + + $container + ->register('setter_injection', SetterInjection::class) + ->setAutowired(true) + ->addMethodCall('notASetter', array()) + ; + + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); + + $methodCalls = $container->getDefinition('setter_injection')->getMethodCalls(); + + $this->assertEquals( + array('notASetter', 'setFoo', 'setDependencies', 'setWithCallsConfigured', 'setChildMethodWithoutDocBlock'), + array_column($methodCalls, 0) + ); + $this->assertEquals( + array(new TypedReference(A::class, A::class, SetterInjection::class)), + $methodCalls[0][1] + ); + } + + /** + * @group legacy + * @expectedDeprecation Relying on service auto-registration for type "Symfony\Component\DependencyInjection\Tests\Compiler\A" is deprecated since version 3.4 and won't be supported in 4.0. Create a service named "Symfony\Component\DependencyInjection\Tests\Compiler\A" instead. + */ + public function testTypedReference() + { + $container = new ContainerBuilder(); + + $container + ->register('bar', Bar::class) + ->setProperty('a', array(new TypedReference(A::class, A::class, Bar::class))) + ; + + $pass = new AutowirePass(); + $pass->process($container); + + $this->assertSame(A::class, $container->getDefinition('autowired.'.A::class)->getClass()); + } + + public function getCreateResourceTests() + { + return array( + array('IdenticalClassResource', true), + array('ClassChangedConstructorArgs', false), + ); + } + public function testIgnoreServiceWithClassNotExisting() { $container = new ContainerBuilder(); @@ -437,12 +609,35 @@ public function testIgnoreServiceWithClassNotExisting() $barDefinition = $container->register('bar', __NAMESPACE__.'\Bar'); $barDefinition->setAutowired(true); + $container->register(Foo::class, Foo::class); + $pass = new AutowirePass(); $pass->process($container); $this->assertTrue($container->hasDefinition('bar')); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "setter_injection_collision": argument "$collision" of method "Symfony\Component\DependencyInjection\Tests\Compiler\SetterInjectionCollision::setMultipleInstancesForOneArg()" references interface "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" but no such service exists. You should maybe alias this interface to one of these existing services: "c1", "c2". + */ + public function testSetterInjectionCollisionThrowsException() + { + $container = new ContainerBuilder(); + + $container->register('c1', CollisionA::class); + $container->register('c2', CollisionB::class); + $aDefinition = $container->register('setter_injection_collision', SetterInjectionCollision::class); + $aDefinition->setAutowired(true); + + $pass = new AutowirePass(); + $pass->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "bar": argument "$foo" of method "Symfony\Component\DependencyInjection\Tests\Compiler\Bar::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\Foo" but no such service exists. You should maybe alias this class to the existing "foo" service. + */ public function testProcessDoesNotTriggerDeprecations() { $container = new ContainerBuilder(); @@ -462,36 +657,122 @@ public function testEmptyStringIsKept() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A'); - $container->register('lille', __NAMESPACE__.'\Lille'); + $container->register(A::class); + $container->register(Lille::class); $container->register('foo', __NAMESPACE__.'\MultipleArgumentsOptionalScalar') ->setAutowired(true) ->setArguments(array('', '')); - $pass = new AutowirePass(); - $pass->process($container); + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); + + $this->assertEquals(array(new TypedReference(A::class, A::class, MultipleArgumentsOptionalScalar::class), '', new TypedReference(Lille::class, Lille::class)), $container->getDefinition('foo')->getArguments()); + } + + public function testWithFactory() + { + $container = new ContainerBuilder(); + + $container->register(Foo::class); + $definition = $container->register('a', A::class) + ->setFactory(array(A::class, 'create')) + ->setAutowired(true); + + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); - $this->assertEquals(array(new Reference('a'), '', new Reference('lille')), $container->getDefinition('foo')->getArguments()); + $this->assertEquals(array(new TypedReference(Foo::class, Foo::class, A::class)), $definition->getArguments()); } - public function provideAutodiscoveredAutowiringOrder() + /** + * @dataProvider provideNotWireableCalls + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + */ + public function testNotWireableCalls($method, $expectedMsg) + { + $container = new ContainerBuilder(); + + $foo = $container->register('foo', NotWireable::class)->setAutowired(true) + ->addMethodCall('setBar', array()) + ->addMethodCall('setOptionalNotAutowireable', array()) + ->addMethodCall('setOptionalNoTypeHint', array()) + ->addMethodCall('setOptionalArgNoAutowireable', array()) + ; + + if ($method) { + $foo->addMethodCall($method, array()); + } + + if (method_exists($this, 'expectException')) { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage($expectedMsg); + } else { + $this->setExpectedException(RuntimeException::class, $expectedMsg); + } + + (new ResolveClassPass())->process($container); + (new AutowirePass())->process($container); + } + + public function provideNotWireableCalls() { return array( - array('CannotBeAutowiredForwardOrder'), - array('CannotBeAutowiredReverseOrder'), + array('setNotAutowireable', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setNotAutowireable()" has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class cannot be loaded.'), + array('setDifferentNamespace', 'Cannot autowire service "foo": argument "$n" of method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setDifferentNamespace()" references class "stdClass" but no such service exists. It cannot be auto-registered because it is from a different root namespace.'), + array(null, 'Cannot autowire service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'), ); } /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Service "a" can use either autowiring or a factory, not both. + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "j": argument "$i" of method "Symfony\Component\DependencyInjection\Tests\Compiler\J::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\I" but no such service exists. Try changing the type-hint to "Symfony\Component\DependencyInjection\Tests\Compiler\IInterface" instead. */ - public function testWithFactory() + public function testByIdAlternative() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A') - ->setFactory('foo') + $container->setAlias(IInterface::class, 'i'); + $container->register('i', I::class); + $container->register('j', J::class) + ->setAutowired(true); + + $pass = new AutowirePass(); + $pass->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "j": argument "$i" of method "Symfony\Component\DependencyInjection\Tests\Compiler\J::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\I" but no such service exists. Try changing the type-hint to "Symfony\Component\DependencyInjection\Tests\Compiler\IInterface" instead. + */ + public function testExceptionWhenAliasExists() + { + $container = new ContainerBuilder(); + + // multiple I services... but there *is* IInterface available + $container->setAlias(IInterface::class, 'i'); + $container->register('i', I::class); + $container->register('i2', I::class); + // J type-hints against I concretely + $container->register('j', J::class) + ->setAutowired(true); + + $pass = new AutowirePass(); + $pass->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException + * @expectedExceptionMessage Cannot autowire service "j": argument "$i" of method "Symfony\Component\DependencyInjection\Tests\Compiler\J::__construct()" references class "Symfony\Component\DependencyInjection\Tests\Compiler\I" but no such service exists. You should maybe alias this class to one of these existing services: "i", "i2". + */ + public function testExceptionWhenAliasDoesNotExist() + { + $container = new ContainerBuilder(); + + // multiple I instances... but no IInterface alias + $container->register('i', I::class); + $container->register('i2', I::class); + // J type-hints against I concretely + $container->register('j', J::class) ->setAutowired(true); $pass = new AutowirePass(); @@ -510,10 +791,17 @@ public function __construct(Foo $foo) } } -class A +interface AInterface { } +class A implements AInterface +{ + public static function create(Foo $foo) + { + } +} + class B extends A { } @@ -559,6 +847,27 @@ public function __construct(B $b, DInterface $d) } } +class D +{ + public function __construct(A $a, DInterface $d) + { + } +} + +class E +{ + public function __construct(D $d = null) + { + } +} + +class J +{ + public function __construct(I $i) + { + } +} + interface CollisionInterface { } @@ -666,3 +975,137 @@ public function __construct(A $a, $foo = 'default_val', Lille $lille) { } } + +/* + * Classes used for testing createResourceForClass + */ +class ClassForResource +{ + public function __construct($foo, Bar $bar = null) + { + } + + public function setBar(Bar $bar) + { + } +} +class IdenticalClassResource extends ClassForResource +{ +} + +class ClassChangedConstructorArgs extends ClassForResource +{ + public function __construct($foo, Bar $bar, $baz) + { + } +} + +class SetterInjection extends SetterInjectionParent +{ + /** + * @required + */ + public function setFoo(Foo $foo) + { + // should be called + } + + /** @inheritdoc*/ + public function setDependencies(Foo $foo, A $a) + { + // should be called + } + + /** {@inheritdoc} */ + public function setWithCallsConfigured(A $a) + { + // this method has a calls configured on it + } + + public function notASetter(A $a) + { + // should be called only when explicitly specified + } + + /** + * @required*/ + public function setChildMethodWithoutDocBlock(A $a) + { + } +} + +class SetterInjectionParent +{ + /** @required*/ + public function setDependencies(Foo $foo, A $a) + { + // should be called + } + + public function notASetter(A $a) + { + // @required should be ignored when the child does not add @inheritdoc + } + + /** @required prefix is on purpose */ + public function setWithCallsConfigured(A $a) + { + } + + /** @required */ + public function setChildMethodWithoutDocBlock(A $a) + { + } +} + +class SetterInjectionCollision +{ + /** + * @required + */ + public function setMultipleInstancesForOneArg(CollisionInterface $collision) + { + // The CollisionInterface cannot be autowired - there are multiple + + // should throw an exception + } +} + +class NotWireable +{ + public function setNotAutowireable(NotARealClass $n) + { + } + + public function setBar() + { + } + + public function setOptionalNotAutowireable(NotARealClass $n = null) + { + } + + public function setDifferentNamespace(\stdClass $n) + { + } + + public function setOptionalNoTypeHint($foo = null) + { + } + + public function setOptionalArgNoAutowireable($other = 'default_val') + { + } + + /** @required */ + protected function setProtectedMethod(A $a) + { + } +} + +class PrivateConstructor +{ + private function __construct() + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php new file mode 100644 index 0000000000000..891acbeee0d89 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckArgumentsValidityPassTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\CheckArgumentsValidityPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Kévin Dunglas + */ +class CheckArgumentsValidityPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + $definition = $container->register('foo'); + $definition->setArguments(array(null, 1, 'a')); + $definition->setMethodCalls(array( + array('bar', array('a', 'b')), + array('baz', array('c', 'd')), + )); + + $pass = new CheckArgumentsValidityPass(); + $pass->process($container); + + $this->assertEquals(array(null, 1, 'a'), $container->getDefinition('foo')->getArguments()); + $this->assertEquals(array( + array('bar', array('a', 'b')), + array('baz', array('c', 'd')), + ), $container->getDefinition('foo')->getMethodCalls()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @dataProvider definitionProvider + */ + public function testException(array $arguments, array $methodCalls) + { + $container = new ContainerBuilder(); + $definition = $container->register('foo'); + $definition->setArguments($arguments); + $definition->setMethodCalls($methodCalls); + + $pass = new CheckArgumentsValidityPass(); + $pass->process($container); + } + + public function definitionProvider() + { + return array( + array(array(null, 'a' => 'a'), array()), + array(array(1 => 1), array()), + array(array(), array(array('baz', array(null, 'a' => 'a')))), + array(array(), array(array('baz', array(1 => 1)))), + ); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php index 220348f4fecf2..e8a526e722ae8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckCircularReferencesPassTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Compiler\CheckCircularReferencesPass; use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; @@ -118,6 +119,30 @@ public function testProcessIgnoresMethodCalls() $this->addToAssertionCount(1); } + public function testProcessIgnoresLazyServices() + { + $container = new ContainerBuilder(); + $container->register('a')->setLazy(true)->addArgument(new Reference('b')); + $container->register('b')->addArgument(new Reference('a')); + + $this->process($container); + + // just make sure that a lazily loaded service does not trigger a CircularReferenceException + $this->addToAssertionCount(1); + } + + public function testProcessIgnoresIteratorArguments() + { + $container = new ContainerBuilder(); + $container->register('a')->addArgument(new Reference('b')); + $container->register('b')->addArgument(new IteratorArgument(array(new Reference('a')))); + + $this->process($container); + + // just make sure that an IteratorArgument does not trigger a CircularReferenceException + $this->addToAssertionCount(1); + } + protected function process(ContainerBuilder $container) { $compiler = new Compiler(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php index 7e4d4fa9a8fed..585bb357669b4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Compiler\CheckDefinitionValidityPass; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; class CheckDefinitionValidityPassTest extends TestCase @@ -29,30 +28,6 @@ public function testProcessDetectsSyntheticNonPublicDefinitions() $this->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @group legacy - */ - public function testProcessDetectsSyntheticPrototypeDefinitions() - { - $container = new ContainerBuilder(); - $container->register('a')->setSynthetic(true)->setScope(ContainerInterface::SCOPE_PROTOTYPE); - - $this->process($container); - } - - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @group legacy - */ - public function testProcessDetectsSharedPrototypeDefinitions() - { - $container = new ContainerBuilder(); - $container->register('a')->setShared(true)->setScope(ContainerInterface::SCOPE_PROTOTYPE); - - $this->process($container); - } - /** * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException */ @@ -64,18 +39,6 @@ public function testProcessDetectsNonSyntheticNonAbstractDefinitionWithoutClass( $this->process($container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @group legacy - */ - public function testLegacyProcessDetectsBothFactorySyntaxesUsed() - { - $container = new ContainerBuilder(); - $container->register('a')->setFactory(array('a', 'b'))->setFactoryClass('a'); - - $this->process($container); - } - public function testProcess() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckReferenceValidityPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckReferenceValidityPassTest.php index 5604680e73072..231520cd70c07 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckReferenceValidityPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckReferenceValidityPassTest.php @@ -12,76 +12,12 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\Scope; use Symfony\Component\DependencyInjection\Compiler\CheckReferenceValidityPass; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; class CheckReferenceValidityPassTest extends TestCase { - /** - * @group legacy - */ - public function testProcessIgnoresScopeWideningIfNonStrictReference() - { - $container = new ContainerBuilder(); - $container->register('a')->addArgument(new Reference('b', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false)); - $container->register('b')->setScope('prototype'); - - $this->process($container); - - $this->addToAssertionCount(1); - } - - /** - * @expectedException \RuntimeException - * @group legacy - */ - public function testProcessDetectsScopeWidening() - { - $container = new ContainerBuilder(); - $container->register('a')->addArgument(new Reference('b')); - $container->register('b')->setScope('prototype'); - - $this->process($container); - - $this->addToAssertionCount(1); - } - - /** - * @group legacy - */ - public function testProcessIgnoresCrossScopeHierarchyReferenceIfNotStrict() - { - $container = new ContainerBuilder(); - $container->addScope(new Scope('a')); - $container->addScope(new Scope('b')); - - $container->register('a')->setScope('a')->addArgument(new Reference('b', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false)); - $container->register('b')->setScope('b'); - - $this->process($container); - - $this->addToAssertionCount(1); - } - - /** - * @expectedException \RuntimeException - * @group legacy - */ - public function testProcessDetectsCrossScopeHierarchyReference() - { - $container = new ContainerBuilder(); - $container->addScope(new Scope('a')); - $container->addScope(new Scope('b')); - - $container->register('a')->setScope('a')->addArgument(new Reference('b')); - $container->register('b')->setScope('b'); - - $this->process($container); - } - /** * @expectedException \RuntimeException */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php index d8a2d64da9a21..8c51df86f6811 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php @@ -144,27 +144,6 @@ public function testProcessMovesTagsFromDecoratedDefinitionToDecoratingDefinitio $this->assertEquals(array('bar' => array('attr' => 'baz'), 'foobar' => array('attr' => 'bar')), $container->getDefinition('baz')->getTags()); } - public function testProcessMergesAutowiringTypesInDecoratingDefinitionAndRemoveThemFromDecoratedDefinition() - { - $container = new ContainerBuilder(); - - $container - ->register('parent') - ->addAutowiringType('Bar') - ; - - $container - ->register('child') - ->setDecoratedService('parent') - ->addAutowiringType('Foo') - ; - - $this->process($container); - - $this->assertEquals(array('Bar', 'Foo'), $container->getDefinition('child')->getAutowiringTypes()); - $this->assertEmpty($container->getDefinition('child.inner')->getAutowiringTypes()); - } - protected function process(ContainerBuilder $container) { $repeatedPass = new DecoratorServicePass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php index 335af14d65f12..26b24fa713242 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php @@ -12,13 +12,14 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\Scope; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; use Symfony\Component\DependencyInjection\Compiler\RepeatedPass; use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; class InlineServiceDefinitionsPassTest extends TestCase { @@ -62,29 +63,6 @@ public function testProcessDoesNotInlinesWhenAliasedServiceIsShared() $this->assertSame($ref, $arguments[0]); } - /** - * @group legacy - */ - public function testProcessDoesNotInlineWhenAliasedServiceIsNotOfPrototypeScope() - { - $container = new ContainerBuilder(); - $container - ->register('foo') - ->setPublic(false) - ; - $container->setAlias('moo', 'foo'); - - $container - ->register('service') - ->setArguments(array($ref = new Reference('foo'))) - ; - - $this->process($container); - - $arguments = $container->getDefinition('service')->getArguments(); - $this->assertSame($ref, $arguments[0]); - } - public function testProcessDoesInlineNonSharedService() { $container = new ContainerBuilder(); @@ -114,38 +92,6 @@ public function testProcessDoesInlineNonSharedService() $this->assertNotSame($container->getDefinition('bar'), $arguments[2]); } - /** - * @group legacy - */ - public function testProcessDoesInlineServiceOfPrototypeScope() - { - $container = new ContainerBuilder(); - $container - ->register('foo') - ->setScope('prototype') - ; - $container - ->register('bar') - ->setPublic(false) - ->setScope('prototype') - ; - $container->setAlias('moo', 'bar'); - - $container - ->register('service') - ->setArguments(array(new Reference('foo'), $ref = new Reference('moo'), new Reference('bar'))) - ; - - $this->process($container); - - $arguments = $container->getDefinition('service')->getArguments(); - $this->assertEquals($container->getDefinition('foo'), $arguments[0]); - $this->assertNotSame($container->getDefinition('foo'), $arguments[0]); - $this->assertSame($ref, $arguments[1]); - $this->assertEquals($container->getDefinition('bar'), $arguments[2]); - $this->assertNotSame($container->getDefinition('bar'), $arguments[2]); - } - public function testProcessInlinesIfMultipleReferencesButAllFromTheSameDefinition() { $container = new ContainerBuilder(); @@ -244,23 +190,6 @@ public function testProcessDoesNotInlineReferenceWhenUsedByInlineFactory() $this->assertSame($ref, $args[0]); } - /** - * @group legacy - */ - public function testProcessInlinesOnlyIfSameScope() - { - $container = new ContainerBuilder(); - - $container->addScope(new Scope('foo')); - $a = $container->register('a')->setPublic(false)->setScope('foo'); - $b = $container->register('b')->addArgument(new Reference('a')); - - $this->process($container); - $arguments = $b->getArguments(); - $this->assertEquals(new Reference('a'), $arguments[0]); - $this->assertTrue($container->hasDefinition('a')); - } - public function testProcessDoesNotInlineWhenServiceIsPrivateButLazy() { $container = new ContainerBuilder(); @@ -296,6 +225,57 @@ public function testProcessDoesNotInlineWhenServiceReferencesItself() $this->assertSame($ref, $calls[0][1][0]); } + public function testProcessDoesNotSetLazyArgumentValuesAfterInlining() + { + $container = new ContainerBuilder(); + $container + ->register('inline') + ->setShared(false) + ; + $container + ->register('service-closure') + ->setArguments(array(new ServiceClosureArgument(new Reference('inline')))) + ; + $container + ->register('iterator') + ->setArguments(array(new IteratorArgument(array(new Reference('inline'))))) + ; + + $this->process($container); + + $values = $container->getDefinition('service-closure')->getArgument(0)->getValues(); + $this->assertInstanceOf(Reference::class, $values[0]); + $this->assertSame('inline', (string) $values[0]); + + $values = $container->getDefinition('iterator')->getArgument(0)->getValues(); + $this->assertInstanceOf(Reference::class, $values[0]); + $this->assertSame('inline', (string) $values[0]); + } + + public function testGetInlinedServiceIdData() + { + $container = new ContainerBuilder(); + $container + ->register('inlinable.service') + ->setPublic(false) + ; + $container + ->register('non_inlinable.service') + ->setPublic(true) + ; + + $container + ->register('other_service') + ->setArguments(array(new Reference('inlinable.service'))) + ; + + $inlinePass = new InlineServiceDefinitionsPass(); + $repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), $inlinePass)); + $repeatedPass->process($container); + + $this->assertEquals(array('inlinable.service' => array('other_service')), $inlinePass->getInlinedServiceIds()); + } + protected function process(ContainerBuilder $container) { $repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new InlineServiceDefinitionsPass())); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php index db33d4b4a14dc..15c827d8270df 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php @@ -12,7 +12,9 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -114,4 +116,109 @@ public function testProcessInlinesWhenThereAreMultipleReferencesButFromTheSameDe $this->assertFalse($container->hasDefinition('b')); $this->assertFalse($container->hasDefinition('c'), 'Service C was not inlined.'); } + + /** + * @dataProvider getYamlCompileTests + */ + public function testYamlContainerCompiles($directory, $actualServiceId, $expectedServiceId, ContainerBuilder $mainContainer = null) + { + // allow a container to be passed in, which might have autoconfigure settings + $container = $mainContainer ? $mainContainer : new ContainerBuilder(); + $container->setResourceTracking(false); + $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Fixtures/yaml/integration/'.$directory)); + $loader->load('main.yml'); + $container->compile(); + $actualService = $container->getDefinition($actualServiceId); + + // create a fresh ContainerBuilder, to avoid autoconfigure stuff + $container = new ContainerBuilder(); + $container->setResourceTracking(false); + $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Fixtures/yaml/integration/'.$directory)); + $loader->load('expected.yml'); + $container->compile(); + $expectedService = $container->getDefinition($expectedServiceId); + + // reset changes, we don't care if these differ + $actualService->setChanges(array()); + $expectedService->setChanges(array()); + + $this->assertEquals($expectedService, $actualService); + } + + public function getYamlCompileTests() + { + $container = new ContainerBuilder(); + $container->registerForAutoconfiguration(IntegrationTestStub::class); + yield array( + 'autoconfigure_child_not_applied', + 'child_service', + 'child_service_expected', + $container, + ); + + $container = new ContainerBuilder(); + $container->registerForAutoconfiguration(IntegrationTestStub::class); + yield array( + 'autoconfigure_parent_child', + 'child_service', + 'child_service_expected', + $container, + ); + + $container = new ContainerBuilder(); + $container->registerForAutoconfiguration(IntegrationTestStub::class) + ->addTag('from_autoconfigure'); + yield array( + 'autoconfigure_parent_child_tags', + 'child_service', + 'child_service_expected', + $container, + ); + + yield array( + 'child_parent', + 'child_service', + 'child_service_expected', + ); + + yield array( + 'defaults_child_tags', + 'child_service', + 'child_service_expected', + ); + + yield array( + 'defaults_instanceof_importance', + 'main_service', + 'main_service_expected', + ); + + yield array( + 'defaults_parent_child', + 'child_service', + 'child_service_expected', + ); + + yield array( + 'instanceof_parent_child', + 'child_service', + 'child_service_expected', + ); + } +} + +class IntegrationTestStub extends IntegrationTestStubParent +{ +} + +class IntegrationTestStubParent +{ + public function enableSummer($enable) + { + // methods used in calls - added here to prevent errors for not existing + } + + public function setSunshine($type) + { + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/LegacyResolveParameterPlaceHoldersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/LegacyResolveParameterPlaceHoldersPassTest.php deleted file mode 100644 index 8450ee98988ab..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/LegacyResolveParameterPlaceHoldersPassTest.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Tests\Compiler; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -/** - * @group legacy - */ -class LegacyResolveParameterPlaceHoldersPassTest extends TestCase -{ - public function testFactoryClassParametersShouldBeResolved() - { - $compilerPass = new ResolveParameterPlaceHoldersPass(); - - $container = new ContainerBuilder(); - $container->setParameter('foo.factory.class', 'FooFactory'); - $fooDefinition = $container->register('foo', '%foo.factory.class%'); - $fooDefinition->setFactoryClass('%foo.factory.class%'); - $compilerPass->process($container); - $fooDefinition = $container->getDefinition('foo'); - - $this->assertSame('FooFactory', $fooDefinition->getFactoryClass()); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PassConfigTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PassConfigTest.php new file mode 100644 index 0000000000000..d9444ca89cf3d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PassConfigTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * @author Guilhem N + */ +class PassConfigTest extends TestCase +{ + public function testPassOrdering() + { + $config = new PassConfig(); + $config->setBeforeOptimizationPasses(array()); + + $pass1 = $this->getMockBuilder(CompilerPassInterface::class)->getMock(); + $config->addPass($pass1, PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); + + $pass2 = $this->getMockBuilder(CompilerPassInterface::class)->getMock(); + $config->addPass($pass2, PassConfig::TYPE_BEFORE_OPTIMIZATION, 30); + + $passes = $config->getBeforeOptimizationPasses(); + $this->assertSame($pass2, $passes[0]); + $this->assertSame($pass1, $passes[1]); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php new file mode 100644 index 0000000000000..61e3fa947a47d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/PriorityTaggedServiceTraitTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class PriorityTaggedServiceTraitTest extends TestCase +{ + public function testThatCacheWarmersAreProcessedInPriorityOrder() + { + $services = array( + 'my_service1' => array('my_custom_tag' => array('priority' => 100)), + 'my_service2' => array('my_custom_tag' => array('priority' => 200)), + 'my_service3' => array('my_custom_tag' => array('priority' => -501)), + 'my_service4' => array('my_custom_tag' => array()), + 'my_service5' => array('my_custom_tag' => array('priority' => -1)), + 'my_service6' => array('my_custom_tag' => array('priority' => -500)), + 'my_service7' => array('my_custom_tag' => array('priority' => -499)), + 'my_service8' => array('my_custom_tag' => array('priority' => 1)), + 'my_service9' => array('my_custom_tag' => array('priority' => -2)), + 'my_service10' => array('my_custom_tag' => array('priority' => -1000)), + 'my_service11' => array('my_custom_tag' => array('priority' => -1001)), + 'my_service12' => array('my_custom_tag' => array('priority' => -1002)), + 'my_service13' => array('my_custom_tag' => array('priority' => -1003)), + 'my_service14' => array('my_custom_tag' => array('priority' => -1000)), + 'my_service15' => array('my_custom_tag' => array('priority' => 1)), + 'my_service16' => array('my_custom_tag' => array('priority' => -1)), + 'my_service17' => array('my_custom_tag' => array('priority' => 200)), + 'my_service18' => array('my_custom_tag' => array('priority' => 100)), + 'my_service19' => array('my_custom_tag' => array()), + ); + + $container = new ContainerBuilder(); + + foreach ($services as $id => $tags) { + $definition = $container->register($id); + + foreach ($tags as $name => $attributes) { + $definition->addTag($name, $attributes); + } + } + + $expected = array( + new Reference('my_service2'), + new Reference('my_service17'), + new Reference('my_service1'), + new Reference('my_service18'), + new Reference('my_service8'), + new Reference('my_service15'), + new Reference('my_service4'), + new Reference('my_service19'), + new Reference('my_service5'), + new Reference('my_service16'), + new Reference('my_service9'), + new Reference('my_service7'), + new Reference('my_service6'), + new Reference('my_service3'), + new Reference('my_service10'), + new Reference('my_service14'), + new Reference('my_service11'), + new Reference('my_service12'), + new Reference('my_service13'), + ); + + $priorityTaggedServiceTraitImplementation = new PriorityTaggedServiceTraitImplementation(); + + $this->assertEquals($expected, $priorityTaggedServiceTraitImplementation->test('my_custom_tag', $container)); + } +} + +class PriorityTaggedServiceTraitImplementation +{ + use PriorityTaggedServiceTrait; + + public function test($tagName, ContainerBuilder $container) + { + return $this->findAndSortTaggedServices($tagName, $container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php new file mode 100644 index 0000000000000..957a04c6c48c7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface as PsrContainerInterface; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveServiceSubscribersPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber; +use Symfony\Component\DependencyInjection\TypedReference; + +require_once __DIR__.'/../Fixtures/includes/classes.php'; + +class RegisterServiceSubscribersPassTest extends TestCase +{ + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Service "foo" must implement interface "Symfony\Component\DependencyInjection\ServiceSubscriberInterface". + */ + public function testInvalidClass() + { + $container = new ContainerBuilder(); + + $container->register('foo', CustomDefinition::class) + ->addTag('container.service_subscriber') + ; + + (new RegisterServiceSubscribersPass())->process($container); + (new ResolveServiceSubscribersPass())->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage The "container.service_subscriber" tag accepts only the "key" and "id" attributes, "bar" given for service "foo". + */ + public function testInvalidAttributes() + { + $container = new ContainerBuilder(); + + $container->register('foo', TestServiceSubscriber::class) + ->addTag('container.service_subscriber', array('bar' => '123')) + ; + + (new RegisterServiceSubscribersPass())->process($container); + (new ResolveServiceSubscribersPass())->process($container); + } + + public function testNoAttributes() + { + $container = new ContainerBuilder(); + + $container->register('foo', TestServiceSubscriber::class) + ->addArgument(new Reference(PsrContainerInterface::class)) + ->addTag('container.service_subscriber') + ; + + (new RegisterServiceSubscribersPass())->process($container); + (new ResolveServiceSubscribersPass())->process($container); + + $foo = $container->getDefinition('foo'); + $locator = $container->getDefinition((string) $foo->getArgument(0)); + + $this->assertFalse($locator->isPublic()); + $this->assertSame(ServiceLocator::class, $locator->getClass()); + + $expected = array( + TestServiceSubscriber::class => new ServiceClosureArgument(new TypedReference(TestServiceSubscriber::class, TestServiceSubscriber::class, TestServiceSubscriber::class)), + CustomDefinition::class => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, TestServiceSubscriber::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + 'bar' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, TestServiceSubscriber::class)), + 'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, TestServiceSubscriber::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + ); + + $this->assertEquals($expected, $locator->getArgument(0)); + } + + public function testWithAttributes() + { + $container = new ContainerBuilder(); + + $container->register('foo', TestServiceSubscriber::class) + ->setAutowired(true) + ->addArgument(new Reference(PsrContainerInterface::class)) + ->addTag('container.service_subscriber', array('key' => 'bar', 'id' => 'bar')) + ->addTag('container.service_subscriber', array('key' => 'bar', 'id' => 'baz')) // should be ignored: the first wins + ; + + (new RegisterServiceSubscribersPass())->process($container); + (new ResolveServiceSubscribersPass())->process($container); + + $foo = $container->getDefinition('foo'); + $locator = $container->getDefinition((string) $foo->getArgument(0)); + + $this->assertFalse($locator->isPublic()); + $this->assertSame(ServiceLocator::class, $locator->getClass()); + + $expected = array( + TestServiceSubscriber::class => new ServiceClosureArgument(new TypedReference(TestServiceSubscriber::class, TestServiceSubscriber::class, TestServiceSubscriber::class)), + CustomDefinition::class => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, TestServiceSubscriber::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + 'bar' => new ServiceClosureArgument(new TypedReference('bar', CustomDefinition::class, TestServiceSubscriber::class)), + 'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, TestServiceSubscriber::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + ); + + $this->assertEquals($expected, $locator->getArgument(0)); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Service key "test" does not exist in the map returned by "Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber::getSubscribedServices()" for service "foo_service". + */ + public function testExtraServiceSubscriber() + { + $container = new ContainerBuilder(); + $container->register('foo_service', TestServiceSubscriber::class) + ->setAutowired(true) + ->addArgument(new Reference(PsrContainerInterface::class)) + ->addTag('container.service_subscriber', array( + 'key' => 'test', + 'id' => TestServiceSubscriber::class, + )) + ; + $container->register(TestServiceSubscriber::class, TestServiceSubscriber::class); + $container->compile(); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php index 57dd42b487ab4..c6f4e5e79d64d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; use Symfony\Component\DependencyInjection\Compiler\RepeatedPass; use Symfony\Component\DependencyInjection\Compiler\RemoveUnusedDefinitionsPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -106,6 +107,28 @@ public function testProcessWontRemovePrivateFactory() $this->assertTrue($container->hasDefinition('foobar')); } + public function testProcessConsiderEnvVariablesAsUsedEvenInPrivateServices() + { + $container = new ContainerBuilder(); + $container->setParameter('env(FOOBAR)', 'test'); + $container + ->register('foo') + ->setArguments(array('%env(FOOBAR)%')) + ->setPublic(false) + ; + + $resolvePass = new ResolveParameterPlaceHoldersPass(); + $resolvePass->process($container); + + $this->process($container); + + $this->assertFalse($container->hasDefinition('foo')); + + $envCounters = $container->getEnvCounters(); + $this->assertArrayHasKey('FOOBAR', $envCounters); + $this->assertSame(1, $envCounters['FOOBAR']); + } + protected function process(ContainerBuilder $container) { $repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new RemoveUnusedDefinitionsPass())); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ReplaceAliasByActualDefinitionPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ReplaceAliasByActualDefinitionPassTest.php index 7e7c3e41e2a8e..7574e7943b4cb 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ReplaceAliasByActualDefinitionPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ReplaceAliasByActualDefinitionPassTest.php @@ -26,8 +26,6 @@ public function testProcess() $container = new ContainerBuilder(); $aDefinition = $container->register('a', '\stdClass'); - $aDefinition->setFactoryService('b', false); - $aDefinition->setFactory(array(new Reference('b'), 'createA')); $bDefinition = new Definition('\stdClass'); @@ -49,33 +47,12 @@ public function testProcess() '->process() replaces alias to actual.' ); - $this->assertSame('b_alias', $aDefinition->getFactoryService(false)); $this->assertTrue($container->has('container')); $resolvedFactory = $aDefinition->getFactory(); $this->assertSame('b_alias', (string) $resolvedFactory[0]); } - /** - * @group legacy - */ - public function testPrivateAliasesInFactory() - { - $container = new ContainerBuilder(); - - $container->register('a', 'Bar\FooClass'); - $container->register('b', 'Bar\FooClass') - ->setFactoryService('a') - ->setFactoryMethod('getInstance'); - - $container->register('c', 'stdClass')->setPublic(false); - $container->setAlias('c_alias', 'c'); - - $this->process($container); - - $this->assertInstanceOf('Bar\FooClass', $container->get('b')); - } - /** * @expectedException \InvalidArgumentException */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php new file mode 100644 index 0000000000000..f7e29d2110ff1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\TypedReference; + +class ResolveBindingsPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + + $bindings = array(CaseSensitiveClass::class => new BoundArgument(new Reference('foo'))); + + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); + $definition->setArguments(array(1 => '123')); + $definition->addMethodCall('setSensitiveClass'); + $definition->setBindings($bindings); + + $container->register('foo', CaseSensitiveClass::class) + ->setBindings($bindings); + + $pass = new ResolveBindingsPass(); + $pass->process($container); + + $this->assertEquals(array(new Reference('foo'), '123'), $definition->getArguments()); + $this->assertEquals(array(array('setSensitiveClass', array(new Reference('foo')))), $definition->getMethodCalls()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Unused binding "$quz" in service "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy". + */ + public function testUnusedBinding() + { + $container = new ContainerBuilder(); + + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); + $definition->setBindings(array('$quz' => '123')); + + $pass = new ResolveBindingsPass(); + $pass->process($container); + } + + public function testTypedReferenceSupport() + { + $container = new ContainerBuilder(); + + $bindings = array(CaseSensitiveClass::class => new BoundArgument(new Reference('foo'))); + + // Explicit service id + $definition1 = $container->register('def1', NamedArgumentsDummy::class); + $definition1->addArgument($typedRef = new TypedReference('bar', CaseSensitiveClass::class)); + $definition1->setBindings($bindings); + + $definition2 = $container->register('def2', NamedArgumentsDummy::class); + $definition2->addArgument(new TypedReference(CaseSensitiveClass::class, CaseSensitiveClass::class)); + $definition2->setBindings($bindings); + + $pass = new ResolveBindingsPass(); + $pass->process($container); + + $this->assertEquals(array($typedRef), $container->getDefinition('def1')->getArguments()); + $this->assertEquals(array(new Reference('foo')), $container->getDefinition('def2')->getArguments()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveClassPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveClassPassTest.php new file mode 100644 index 0000000000000..acbcf1dda2d8c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveClassPassTest.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; + +class ResolveClassPassTest extends TestCase +{ + /** + * @dataProvider provideValidClassId + */ + public function testResolveClassFromId($serviceId) + { + $container = new ContainerBuilder(); + $def = $container->register($serviceId); + + (new ResolveClassPass())->process($container); + + $this->assertSame($serviceId, $def->getClass()); + } + + public function provideValidClassId() + { + yield array('Acme\UnknownClass'); + yield array(CaseSensitiveClass::class); + } + + /** + * @dataProvider provideInvalidClassId + */ + public function testWontResolveClassFromId($serviceId) + { + $container = new ContainerBuilder(); + $def = $container->register($serviceId); + + (new ResolveClassPass())->process($container); + + $this->assertNull($def->getClass()); + } + + public function provideInvalidClassId() + { + yield array(\stdClass::class); + yield array('bar'); + yield array('\DateTime'); + } + + public function testNonFqcnChildDefinition() + { + $container = new ContainerBuilder(); + $parent = $container->register('App\Foo', null); + $child = $container->setDefinition('App\Foo.child', new ChildDefinition('App\Foo')); + + (new ResolveClassPass())->process($container); + + $this->assertSame('App\Foo', $parent->getClass()); + $this->assertNull($child->getClass()); + } + + public function testClassFoundChildDefinition() + { + $container = new ContainerBuilder(); + $parent = $container->register('App\Foo', null); + $child = $container->setDefinition(self::class, new ChildDefinition('App\Foo')); + + (new ResolveClassPass())->process($container); + + $this->assertSame('App\Foo', $parent->getClass()); + $this->assertSame(self::class, $child->getClass()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Service definition "App\Foo\Child" has a parent but no class, and its name looks like a FQCN. Either the class is missing or you want to inherit it from the parent service. To resolve this ambiguity, please rename this service to a non-FQCN (e.g. using dots), or create the missing class. + */ + public function testAmbiguousChildDefinition() + { + $container = new ContainerBuilder(); + $parent = $container->register('App\Foo', null); + $child = $container->setDefinition('App\Foo\Child', new ChildDefinition('App\Foo')); + + (new ResolveClassPass())->process($container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveDefinitionTemplatesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveDefinitionTemplatesPassTest.php index ec7633feeaf49..323aa003ef1c7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveDefinitionTemplatesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveDefinitionTemplatesPassTest.php @@ -12,8 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\ResolveDefinitionTemplatesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -23,7 +22,7 @@ public function testProcess() { $container = new ContainerBuilder(); $container->register('parent', 'foo')->setArguments(array('moo', 'b'))->setProperty('foo', 'moo'); - $container->setDefinition('child', new DefinitionDecorator('parent')) + $container->setDefinition('child', new ChildDefinition('parent')) ->replaceArgument(0, 'a') ->setProperty('foo', 'bar') ->setClass('bar') @@ -32,7 +31,7 @@ public function testProcess() $this->process($container); $def = $container->getDefinition('child'); - $this->assertNotInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $def); + $this->assertNotInstanceOf(ChildDefinition::class, $def); $this->assertEquals('bar', $def->getClass()); $this->assertEquals(array('a', 'b'), $def->getArguments()); $this->assertEquals(array('foo' => 'bar'), $def->getProperties()); @@ -48,7 +47,7 @@ public function testProcessAppendsMethodCallsAlways() ; $container - ->setDefinition('child', new DefinitionDecorator('parent')) + ->setDefinition('child', new ChildDefinition('parent')) ->addMethodCall('bar', array('foo')) ; @@ -71,7 +70,7 @@ public function testProcessDoesNotCopyAbstract() ; $container - ->setDefinition('child', new DefinitionDecorator('parent')) + ->setDefinition('child', new ChildDefinition('parent')) ; $this->process($container); @@ -80,28 +79,6 @@ public function testProcessDoesNotCopyAbstract() $this->assertFalse($def->isAbstract()); } - /** - * @group legacy - */ - public function testProcessDoesNotCopyScope() - { - $container = new ContainerBuilder(); - - $container - ->register('parent') - ->setScope('foo') - ; - - $container - ->setDefinition('child', new DefinitionDecorator('parent')) - ; - - $this->process($container); - - $def = $container->getDefinition('child'); - $this->assertEquals(ContainerInterface::SCOPE_CONTAINER, $def->getScope()); - } - public function testProcessDoesNotCopyShared() { $container = new ContainerBuilder(); @@ -112,7 +89,7 @@ public function testProcessDoesNotCopyShared() ; $container - ->setDefinition('child', new DefinitionDecorator('parent')) + ->setDefinition('child', new ChildDefinition('parent')) ; $this->process($container); @@ -131,7 +108,7 @@ public function testProcessDoesNotCopyTags() ; $container - ->setDefinition('child', new DefinitionDecorator('parent')) + ->setDefinition('child', new ChildDefinition('parent')) ; $this->process($container); @@ -150,7 +127,7 @@ public function testProcessDoesNotCopyDecoratedService() ; $container - ->setDefinition('child', new DefinitionDecorator('parent')) + ->setDefinition('child', new ChildDefinition('parent')) ; $this->process($container); @@ -168,7 +145,7 @@ public function testProcessDoesNotDropShared() ; $container - ->setDefinition('child', new DefinitionDecorator('parent')) + ->setDefinition('child', new ChildDefinition('parent')) ->setShared(false) ; @@ -188,12 +165,12 @@ public function testProcessHandlesMultipleInheritance() ; $container - ->setDefinition('child2', new DefinitionDecorator('child1')) + ->setDefinition('child2', new ChildDefinition('child1')) ->replaceArgument(1, 'b') ; $container - ->setDefinition('child1', new DefinitionDecorator('parent')) + ->setDefinition('child1', new ChildDefinition('parent')) ->replaceArgument(0, 'a') ; @@ -210,7 +187,7 @@ public function testSetLazyOnServiceHasParent() $container->register('parent', 'stdClass'); - $container->setDefinition('child1', new DefinitionDecorator('parent')) + $container->setDefinition('child1', new ChildDefinition('parent')) ->setLazy(true) ; @@ -227,7 +204,7 @@ public function testSetLazyOnServiceIsParent() ->setLazy(true) ; - $container->setDefinition('child1', new DefinitionDecorator('parent')); + $container->setDefinition('child1', new ChildDefinition('parent')); $this->process($container); @@ -238,15 +215,17 @@ public function testSetAutowiredOnServiceHasParent() { $container = new ContainerBuilder(); - $container->register('parent', 'stdClass'); - - $container->setDefinition('child1', new DefinitionDecorator('parent')) + $container->register('parent', 'stdClass') ->setAutowired(true) ; + $container->setDefinition('child1', new ChildDefinition('parent')) + ->setAutowired(false) + ; + $this->process($container); - $this->assertTrue($container->getDefinition('child1')->isAutowired()); + $this->assertFalse($container->getDefinition('child1')->isAutowired()); } public function testSetAutowiredOnServiceIsParent() @@ -257,7 +236,7 @@ public function testSetAutowiredOnServiceIsParent() ->setAutowired(true) ; - $container->setDefinition('child1', new DefinitionDecorator('parent')); + $container->setDefinition('child1', new ChildDefinition('parent')); $this->process($container); @@ -270,11 +249,11 @@ public function testDeepDefinitionsResolving() $container->register('parent', 'parentClass'); $container->register('sibling', 'siblingClass') - ->setConfigurator(new DefinitionDecorator('parent'), 'foo') - ->setFactory(array(new DefinitionDecorator('parent'), 'foo')) - ->addArgument(new DefinitionDecorator('parent')) - ->setProperty('prop', new DefinitionDecorator('parent')) - ->addMethodCall('meth', array(new DefinitionDecorator('parent'))) + ->setConfigurator(new ChildDefinition('parent'), 'foo') + ->setFactory(array(new ChildDefinition('parent'), 'foo')) + ->addArgument(new ChildDefinition('parent')) + ->setProperty('prop', new ChildDefinition('parent')) + ->addMethodCall('meth', array(new ChildDefinition('parent'))) ; $this->process($container); @@ -306,7 +285,7 @@ public function testSetDecoratedServiceOnServiceHasParent() $container->register('parent', 'stdClass'); - $container->setDefinition('child1', new DefinitionDecorator('parent')) + $container->setDefinition('child1', new ChildDefinition('parent')) ->setDecoratedService('foo', 'foo_inner', 5) ; @@ -322,7 +301,7 @@ public function testDecoratedServiceCopiesDeprecatedStatusFromParent() ->setDeprecated(true) ; - $container->setDefinition('decorated_deprecated_parent', new DefinitionDecorator('deprecated_parent')); + $container->setDefinition('decorated_deprecated_parent', new ChildDefinition('deprecated_parent')); $this->process($container); @@ -336,7 +315,7 @@ public function testDecoratedServiceCanOverwriteDeprecatedParentStatus() ->setDeprecated(true) ; - $container->setDefinition('decorated_deprecated_parent', new DefinitionDecorator('deprecated_parent')) + $container->setDefinition('decorated_deprecated_parent', new ChildDefinition('deprecated_parent')) ->setDeprecated(false) ; @@ -345,27 +324,50 @@ public function testDecoratedServiceCanOverwriteDeprecatedParentStatus() $this->assertFalse($container->getDefinition('decorated_deprecated_parent')->isDeprecated()); } - public function testProcessMergeAutowiringTypes() + public function testProcessResolvesAliases() { $container = new ContainerBuilder(); - $container - ->register('parent') - ->addAutowiringType('Foo') - ; + $container->register('parent', 'ParentClass'); + $container->setAlias('parent_alias', 'parent'); + $container->setDefinition('child', new ChildDefinition('parent_alias')); - $container - ->setDefinition('child', new DefinitionDecorator('parent')) - ->addAutowiringType('Bar') - ; + $this->process($container); + + $def = $container->getDefinition('child'); + $this->assertSame('ParentClass', $def->getClass()); + } + + public function testProcessSetsArguments() + { + $container = new ContainerBuilder(); + + $container->register('parent', 'ParentClass')->setArguments(array(0)); + $container->setDefinition('child', (new ChildDefinition('parent'))->setArguments(array( + 1, + 'index_0' => 2, + 'foo' => 3, + ))); $this->process($container); - $childDef = $container->getDefinition('child'); - $this->assertEquals(array('Foo', 'Bar'), $childDef->getAutowiringTypes()); + $def = $container->getDefinition('child'); + $this->assertSame(array(2, 1, 'foo' => 3), $def->getArguments()); + } + + public function testSetAutoconfiguredOnServiceIsParent() + { + $container = new ContainerBuilder(); + + $container->register('parent', 'stdClass') + ->setAutoconfigured(true) + ; + + $container->setDefinition('child1', new ChildDefinition('parent')); + + $this->process($container); - $parentDef = $container->getDefinition('parent'); - $this->assertSame(array('Foo'), $parentDef->getAutowiringTypes()); + $this->assertFalse($container->getDefinition('child1')->isAutoconfigured()); } protected function process(ContainerBuilder $container) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveFactoryClassPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveFactoryClassPassTest.php new file mode 100644 index 0000000000000..96453c30381e2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveFactoryClassPassTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\ResolveFactoryClassPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class ResolveFactoryClassPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + + $factory = $container->register('factory', 'Foo\Bar'); + $factory->setFactory(array(null, 'create')); + + $pass = new ResolveFactoryClassPass(); + $pass->process($container); + + $this->assertSame(array('Foo\Bar', 'create'), $factory->getFactory()); + } + + public function testInlinedDefinitionFactoryIsProcessed() + { + $container = new ContainerBuilder(); + + $factory = $container->register('factory'); + $factory->setFactory(array((new Definition('Baz\Qux'))->setFactory(array(null, 'getInstance')), 'create')); + + $pass = new ResolveFactoryClassPass(); + $pass->process($container); + + $this->assertSame(array('Baz\Qux', 'getInstance'), $factory->getFactory()[0]->getFactory()); + } + + public function provideFulfilledFactories() + { + return array( + array(array('Foo\Bar', 'create')), + array(array(new Reference('foo'), 'create')), + array(array(new Definition('Baz'), 'create')), + ); + } + + /** + * @dataProvider provideFulfilledFactories + */ + public function testIgnoresFulfilledFactories($factory) + { + $container = new ContainerBuilder(); + $definition = new Definition(); + $definition->setFactory($factory); + + $container->setDefinition('factory', $definition); + + $pass = new ResolveFactoryClassPass(); + $pass->process($container); + + $this->assertSame($factory, $container->getDefinition('factory')->getFactory()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage The "factory" service is defined to be created by a factory, but is missing the factory class. Did you forget to define the factory or service class? + */ + public function testNotAnyClassThrowsException() + { + $container = new ContainerBuilder(); + + $factory = $container->register('factory'); + $factory->setFactory(array(null, 'create')); + + $pass = new ResolveFactoryClassPass(); + $pass->process($container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInstanceofConditionalsPassTest.php new file mode 100644 index 0000000000000..164ab25941d64 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInstanceofConditionalsPassTest.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\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveDefinitionTemplatesPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class ResolveInstanceofConditionalsPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + $def = $container->register('foo', self::class)->addTag('tag')->setAutowired(true)->setChanges(array()); + $def->setInstanceofConditionals(array( + parent::class => (new ChildDefinition(''))->setProperty('foo', 'bar')->addTag('baz', array('attr' => 123)), + )); + + (new ResolveInstanceofConditionalsPass())->process($container); + + $parent = 'instanceof.'.parent::class.'.0.foo'; + $def = $container->getDefinition('foo'); + $this->assertEmpty($def->getInstanceofConditionals()); + $this->assertInstanceof(ChildDefinition::class, $def); + $this->assertTrue($def->isAutowired()); + $this->assertSame($parent, $def->getParent()); + $this->assertSame(array('tag' => array(array()), 'baz' => array(array('attr' => 123))), $def->getTags()); + + $parent = $container->getDefinition($parent); + $this->assertSame(array('foo' => 'bar'), $parent->getProperties()); + $this->assertSame(array(), $parent->getTags()); + } + + public function testProcessInheritance() + { + $container = new ContainerBuilder(); + + $def = $container + ->register('parent', parent::class) + ->addMethodCall('foo', array('foo')); + $def->setInstanceofConditionals(array( + parent::class => (new ChildDefinition(''))->addMethodCall('foo', array('bar')), + )); + + $def = (new ChildDefinition('parent'))->setClass(self::class); + $container->setDefinition('child', $def); + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ResolveDefinitionTemplatesPass())->process($container); + + $expected = array( + array('foo', array('bar')), + array('foo', array('foo')), + ); + + $this->assertSame($expected, $container->getDefinition('parent')->getMethodCalls()); + $this->assertSame($expected, $container->getDefinition('child')->getMethodCalls()); + } + + public function testProcessDoesReplaceShared() + { + $container = new ContainerBuilder(); + + $def = $container->register('foo', 'stdClass'); + $def->setInstanceofConditionals(array( + 'stdClass' => (new ChildDefinition(''))->setShared(false), + )); + + (new ResolveInstanceofConditionalsPass())->process($container); + + $def = $container->getDefinition('foo'); + $this->assertFalse($def->isShared()); + } + + public function testProcessHandlesMultipleInheritance() + { + $container = new ContainerBuilder(); + + $def = $container->register('foo', self::class)->setShared(true); + + $def->setInstanceofConditionals(array( + parent::class => (new ChildDefinition(''))->setLazy(true)->setShared(false), + self::class => (new ChildDefinition(''))->setAutowired(true), + )); + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ResolveDefinitionTemplatesPass())->process($container); + + $def = $container->getDefinition('foo'); + $this->assertTrue($def->isAutowired()); + $this->assertTrue($def->isLazy()); + $this->assertTrue($def->isShared()); + } + + public function testProcessUsesAutoconfiguredInstanceof() + { + $container = new ContainerBuilder(); + $def = $container->register('normal_service', self::class); + $def->setInstanceofConditionals(array( + parent::class => (new ChildDefinition('')) + ->addTag('local_instanceof_tag') + ->setFactory('locally_set_factory'), + )); + $def->setAutoconfigured(true); + $container->registerForAutoconfiguration(parent::class) + ->addTag('autoconfigured_tag') + ->setAutowired(true) + ->setFactory('autoconfigured_factory'); + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ResolveDefinitionTemplatesPass())->process($container); + + $def = $container->getDefinition('normal_service'); + // autowired thanks to the autoconfigured instanceof + $this->assertTrue($def->isAutowired()); + // factory from the specific instanceof overrides global one + $this->assertEquals('locally_set_factory', $def->getFactory()); + // tags are merged, the locally set one is first + $this->assertSame(array('local_instanceof_tag' => array(array()), 'autoconfigured_tag' => array(array())), $def->getTags()); + } + + public function testAutoconfigureInstanceofDoesNotDuplicateTags() + { + $container = new ContainerBuilder(); + $def = $container->register('normal_service', self::class); + $def + ->addTag('duplicated_tag') + ->addTag('duplicated_tag', array('and_attributes' => 1)) + ; + $def->setInstanceofConditionals(array( + parent::class => (new ChildDefinition(''))->addTag('duplicated_tag'), + )); + $def->setAutoconfigured(true); + $container->registerForAutoconfiguration(parent::class) + ->addTag('duplicated_tag', array('and_attributes' => 1)) + ; + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ResolveDefinitionTemplatesPass())->process($container); + + $def = $container->getDefinition('normal_service'); + $this->assertSame(array('duplicated_tag' => array(array(), array('and_attributes' => 1))), $def->getTags()); + } + + public function testProcessDoesNotUseAutoconfiguredInstanceofIfNotEnabled() + { + $container = new ContainerBuilder(); + $def = $container->register('normal_service', self::class); + $def->setInstanceofConditionals(array( + parent::class => (new ChildDefinition('')) + ->addTag('foo_tag'), + )); + $container->registerForAutoconfiguration(parent::class) + ->setAutowired(true); + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ResolveDefinitionTemplatesPass())->process($container); + + $def = $container->getDefinition('normal_service'); + $this->assertFalse($def->isAutowired()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage "App\FakeInterface" is set as an "instanceof" conditional, but it does not exist. + */ + public function testBadInterfaceThrowsException() + { + $container = new ContainerBuilder(); + $def = $container->register('normal_service', self::class); + $def->setInstanceofConditionals(array( + 'App\\FakeInterface' => (new ChildDefinition('')) + ->addTag('foo_tag'), + )); + + (new ResolveInstanceofConditionalsPass())->process($container); + } + + public function testBadInterfaceForAutomaticInstanceofIsOk() + { + $container = new ContainerBuilder(); + $container->register('normal_service', self::class) + ->setAutoconfigured(true); + $container->registerForAutoconfiguration('App\\FakeInterface') + ->setAutowired(true); + + (new ResolveInstanceofConditionalsPass())->process($container); + $this->assertTrue($container->hasDefinition('normal_service')); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Autoconfigured instanceof for type "PHPUnit\Framework\TestCase" defines method calls but these are not supported and should be removed. + */ + public function testProcessThrowsExceptionForAutoconfiguredCalls() + { + $container = new ContainerBuilder(); + $container->registerForAutoconfiguration(parent::class) + ->addMethodCall('setFoo'); + + (new ResolveInstanceofConditionalsPass())->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Autoconfigured instanceof for type "PHPUnit\Framework\TestCase" defines arguments but these are not supported and should be removed. + */ + public function testProcessThrowsExceptionForArguments() + { + $container = new ContainerBuilder(); + $container->registerForAutoconfiguration(parent::class) + ->addArgument('bar'); + + (new ResolveInstanceofConditionalsPass())->process($container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php index ad698d2bff483..00613ba5c1c6c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Compiler\ResolveInvalidReferencesPass; @@ -24,17 +25,63 @@ public function testProcess() $container = new ContainerBuilder(); $def = $container ->register('foo') - ->setArguments(array(new Reference('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE))) + ->setArguments(array( + new Reference('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference('baz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + )) ->addMethodCall('foo', array(new Reference('moo', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))) ; $this->process($container); $arguments = $def->getArguments(); - $this->assertNull($arguments[0]); + $this->assertSame(array(null, null), $arguments); $this->assertCount(0, $def->getMethodCalls()); } + public function testProcessIgnoreInvalidArgumentInCollectionArgument() + { + $container = new ContainerBuilder(); + $container->register('baz'); + $def = $container + ->register('foo') + ->setArguments(array( + array( + new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + $baz = new Reference('baz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + new Reference('moo', ContainerInterface::NULL_ON_INVALID_REFERENCE), + ), + )) + ; + + $this->process($container); + + $arguments = $def->getArguments(); + $this->assertSame(array($baz, null), $arguments[0]); + } + + public function testProcessKeepMethodCallOnInvalidArgumentInCollectionArgument() + { + $container = new ContainerBuilder(); + $container->register('baz'); + $def = $container + ->register('foo') + ->addMethodCall('foo', array( + array( + new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + $baz = new Reference('baz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + new Reference('moo', ContainerInterface::NULL_ON_INVALID_REFERENCE), + ), + )) + ; + + $this->process($container); + + $calls = $def->getMethodCalls(); + $this->assertCount(1, $def->getMethodCalls()); + $this->assertSame(array($baz, null), $calls[0][1][0]); + } + public function testProcessIgnoreNonExistentServices() { $container = new ContainerBuilder(); @@ -62,21 +109,22 @@ public function testProcessRemovesPropertiesOnInvalid() $this->assertEquals(array(), $def->getProperties()); } - /** - * @group legacy - */ - public function testStrictFlagIsPreserved() + public function testProcessRemovesArgumentsOnInvalid() { $container = new ContainerBuilder(); - $container->register('bar'); $def = $container ->register('foo') - ->addArgument(new Reference('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE, false)) + ->addArgument(array( + array( + new Reference('bar', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + new ServiceClosureArgument(new Reference('baz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + ), + )) ; $this->process($container); - $this->assertFalse($def->getArgument(0)->isStrict()); + $this->assertSame(array(array(array())), $def->getArguments()); } protected function process(ContainerBuilder $container) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php new file mode 100644 index 0000000000000..fac05f070b05c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\ResolveNamedArgumentsPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; + +/** + * @author Kévin Dunglas + */ +class ResolveNamedArgumentsPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); + $definition->setArguments(array( + 2 => 'http://api.example.com', + '$apiKey' => '123', + 0 => new Reference('foo'), + )); + $definition->addMethodCall('setApiKey', array('$apiKey' => '123')); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + + $this->assertEquals(array(0 => new Reference('foo'), 1 => '123', 2 => 'http://api.example.com'), $definition->getArguments()); + $this->assertEquals(array(array('setApiKey', array('123'))), $definition->getMethodCalls()); + } + + public function testWithFactory() + { + $container = new ContainerBuilder(); + + $container->register('factory', NoConstructor::class); + $definition = $container->register('foo', NoConstructor::class) + ->setFactory(array(new Reference('factory'), 'create')) + ->setArguments(array('$apiKey' => '123')); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + + $this->assertSame(array(0 => '123'), $definition->getArguments()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testClassNull() + { + $container = new ContainerBuilder(); + + $definition = $container->register(NamedArgumentsDummy::class); + $definition->setArguments(array('$apiKey' => '123')); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testClassNotExist() + { + $container = new ContainerBuilder(); + + $definition = $container->register(NotExist::class, NotExist::class); + $definition->setArguments(array('$apiKey' => '123')); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + */ + public function testClassNoConstructor() + { + $container = new ContainerBuilder(); + + $definition = $container->register(NoConstructor::class, NoConstructor::class); + $definition->setArguments(array('$apiKey' => '123')); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function testArgumentNotFound() + { + $container = new ContainerBuilder(); + + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); + $definition->setArguments(array('$notFound' => '123')); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + } + + public function testTypedArgument() + { + $container = new ContainerBuilder(); + + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); + $definition->setArguments(array('$apiKey' => '123', CaseSensitiveClass::class => new Reference('foo'))); + + $pass = new ResolveNamedArgumentsPass(); + $pass->process($container); + + $this->assertEquals(array(new Reference('foo'), '123'), $definition->getArguments()); + } +} + +class NoConstructor +{ + public static function create($apiKey) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php index 50be82d741119..b9459729e2e80 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php @@ -41,12 +41,12 @@ public function testFactoryParametersShouldBeResolved() public function testArgumentParametersShouldBeResolved() { - $this->assertSame(array('bar', 'baz'), $this->fooDefinition->getArguments()); + $this->assertSame(array('bar', array('bar' => 'baz')), $this->fooDefinition->getArguments()); } public function testMethodCallParametersShouldBeResolved() { - $this->assertSame(array(array('foobar', array('bar', 'baz'))), $this->fooDefinition->getMethodCalls()); + $this->assertSame(array(array('foobar', array('bar', array('bar' => 'baz')))), $this->fooDefinition->getMethodCalls()); } public function testPropertyParametersShouldBeResolved() @@ -71,7 +71,7 @@ private function createContainerBuilder() $containerBuilder->setParameter('foo.class', 'Foo'); $containerBuilder->setParameter('foo.factory.class', 'FooFactory'); $containerBuilder->setParameter('foo.arg1', 'bar'); - $containerBuilder->setParameter('foo.arg2', 'baz'); + $containerBuilder->setParameter('foo.arg2', array('%foo.arg1%' => 'baz')); $containerBuilder->setParameter('foo.method', 'foobar'); $containerBuilder->setParameter('foo.property.name', 'bar'); $containerBuilder->setParameter('foo.property.value', 'baz'); @@ -80,7 +80,7 @@ private function createContainerBuilder() $fooDefinition = $containerBuilder->register('foo', '%foo.class%'); $fooDefinition->setFactory(array('%foo.factory.class%', 'getFoo')); - $fooDefinition->setArguments(array('%foo.arg1%', '%foo.arg2%')); + $fooDefinition->setArguments(array('%foo.arg1%', array('%foo.arg1%' => 'baz'))); $fooDefinition->addMethodCall('%foo.method%', array('%foo.arg1%', '%foo.arg2%')); $fooDefinition->setProperty('%foo.property.name%', '%foo.property.value%'); $fooDefinition->setFile('%foo.file%'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php index aaf2f5593a969..c22ab59ea9b98 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php @@ -83,24 +83,6 @@ public function testResolveFactory() $this->assertSame('Factory', (string) $resolvedBarFactory[0]); } - /** - * @group legacy - */ - public function testResolveFactoryService() - { - $container = new ContainerBuilder(); - $container->register('factory', 'Factory'); - $container->setAlias('factory_alias', new Alias('factory')); - $foo = new Definition(); - $foo->setFactoryService('factory_alias'); - $foo->setFactoryMethod('createFoo'); - $container->setDefinition('foo', $foo); - - $this->process($container); - - $this->assertSame('factory', $foo->getFactoryService()); - } - protected function process(ContainerBuilder $container) { $pass = new ResolveReferencesToAliasesPass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceCheckerTest.php b/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceCheckerTest.php new file mode 100644 index 0000000000000..a91934b3ef8f0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceCheckerTest.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\Component\DependencyInjection\Tests\Config; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\ResourceCheckerInterface; +use Symfony\Component\DependencyInjection\Config\ContainerParametersResource; +use Symfony\Component\DependencyInjection\Config\ContainerParametersResourceChecker; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class ContainerParametersResourceCheckerTest extends TestCase +{ + /** @var ContainerParametersResource */ + private $resource; + + /** @var ResourceCheckerInterface */ + private $resourceChecker; + + /** @var ContainerInterface */ + private $container; + + protected function setUp() + { + $this->resource = new ContainerParametersResource(array('locales' => array('fr', 'en'), 'default_locale' => 'fr')); + $this->container = $this->getMockBuilder(ContainerInterface::class)->getMock(); + $this->resourceChecker = new ContainerParametersResourceChecker($this->container); + } + + public function testSupports() + { + $this->assertTrue($this->resourceChecker->supports($this->resource)); + } + + /** + * @dataProvider isFreshProvider + */ + public function testIsFresh(callable $mockContainer, $expected) + { + $mockContainer($this->container); + + $this->assertSame($expected, $this->resourceChecker->isFresh($this->resource, time())); + } + + public function isFreshProvider() + { + yield 'not fresh on missing parameter' => array(function (\PHPUnit_Framework_MockObject_MockObject $container) { + $container->method('hasParameter')->with('locales')->willReturn(false); + }, false); + + yield 'not fresh on different value' => array(function (\PHPUnit_Framework_MockObject_MockObject $container) { + $container->method('getParameter')->with('locales')->willReturn(array('nl', 'es')); + }, false); + + yield 'fresh on every identical parameters' => array(function (\PHPUnit_Framework_MockObject_MockObject $container) { + $container->expects($this->exactly(2))->method('hasParameter')->willReturn(true); + $container->expects($this->exactly(2))->method('getParameter') + ->withConsecutive( + array($this->equalTo('locales')), + array($this->equalTo('default_locale')) + ) + ->will($this->returnValueMap(array( + array('locales', array('fr', 'en')), + array('default_locale', 'fr'), + ))) + ; + }, true); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceTest.php b/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceTest.php new file mode 100644 index 0000000000000..4da4766f27253 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Config/ContainerParametersResourceTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Config; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Config\ContainerParametersResource; + +class ContainerParametersResourceTest extends TestCase +{ + /** @var ContainerParametersResource */ + private $resource; + + protected function setUp() + { + $this->resource = new ContainerParametersResource(array('locales' => array('fr', 'en'), 'default_locale' => 'fr')); + } + + public function testToString() + { + $this->assertSame('container_parameters_9893d3133814ab03cac3490f36dece77', (string) $this->resource); + } + + public function testSerializeUnserialize() + { + $unserialized = unserialize(serialize($this->resource)); + + $this->assertEquals($this->resource, $unserialized); + } + + public function testGetParameters() + { + $this->assertSame(array('locales' => array('fr', 'en'), 'default_locale' => 'fr'), $this->resource->getParameters()); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 3c14bfc19895e..500ef58f74f04 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -15,26 +15,49 @@ require_once __DIR__.'/Fixtures/includes/ProjectExtension.php'; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface as PsrContainerInterface; +use Symfony\Component\Config\Resource\ComposerResource; use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; 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\RuntimeException; -use Symfony\Component\DependencyInjection\Exception\InactiveScopeException; -use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; -use Symfony\Component\DependencyInjection\Scope; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\ExpressionLanguage\Expression; class ContainerBuilderTest extends TestCase { + public function testDefaultRegisteredDefinitions() + { + $builder = new ContainerBuilder(); + + $this->assertCount(1, $builder->getDefinitions()); + $this->assertTrue($builder->hasDefinition('service_container')); + + $definition = $builder->getDefinition('service_container'); + $this->assertInstanceOf(Definition::class, $definition); + $this->assertTrue($definition->isSynthetic()); + $this->assertSame(ContainerInterface::class, $definition->getClass()); + $this->assertTrue($builder->hasAlias(PsrContainerInterface::class)); + $this->assertTrue($builder->hasAlias(ContainerInterface::class)); + } + public function testDefinitions() { $builder = new ContainerBuilder(); @@ -72,7 +95,8 @@ public function testCreateDeprecatedService() $definition->setDeprecated(true); $builder = new ContainerBuilder(); - $builder->createService($definition, 'deprecated_foo'); + $builder->setDefinition('deprecated_foo', $definition); + $builder->get('deprecated_foo'); } public function testRegister() @@ -83,6 +107,15 @@ public function testRegister() $this->assertInstanceOf('Symfony\Component\DependencyInjection\Definition', $builder->getDefinition('foo'), '->register() returns the newly created Definition instance'); } + public function testAutowire() + { + $builder = new ContainerBuilder(); + $builder->autowire('foo', 'Bar\FooClass'); + + $this->assertTrue($builder->hasDefinition('foo'), '->autowire() registers a new service definition'); + $this->assertTrue($builder->getDefinition('foo')->isAutowired(), '->autowire() creates autowired definitions'); + } + public function testHas() { $builder = new ContainerBuilder(); @@ -93,34 +126,64 @@ public function testHas() $this->assertTrue($builder->has('bar'), '->has() returns true if a service exists'); } - public function testGet() + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * @expectedExceptionMessage You have requested a non-existent service "foo". + */ + public function testGetThrowsExceptionIfServiceDoesNotExist() + { + $builder = new ContainerBuilder(); + $builder->get('foo'); + } + + public function testGetReturnsNullIfServiceDoesNotExistAndInvalidReferenceIsUsed() { $builder = new ContainerBuilder(); - try { - $builder->get('foo'); - $this->fail('->get() throws a ServiceNotFoundException if the service does not exist'); - } catch (ServiceNotFoundException $e) { - $this->assertEquals('You have requested a non-existent service "foo".', $e->getMessage(), '->get() throws a ServiceNotFoundException if the service does not exist'); - } $this->assertNull($builder->get('foo', ContainerInterface::NULL_ON_INVALID_REFERENCE), '->get() returns null if the service does not exist and NULL_ON_INVALID_REFERENCE is passed as a second argument'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException + */ + public function testGetThrowsCircularReferenceExceptionIfServiceHasReferenceToItself() + { + $builder = new ContainerBuilder(); + $builder->register('baz', 'stdClass')->setArguments(array(new Reference('baz'))); + $builder->get('baz'); + } + public function testGetReturnsSameInstanceWhenServiceIsShared() + { + $builder = new ContainerBuilder(); + $builder->register('bar', 'stdClass'); + + $this->assertTrue($builder->get('bar') === $builder->get('bar'), '->get() always returns the same instance if the service is shared'); + } + + public function testGetCreatesServiceBasedOnDefinition() + { + $builder = new ContainerBuilder(); $builder->register('foo', 'stdClass'); + $this->assertInternalType('object', $builder->get('foo'), '->get() returns the service definition associated with the id'); + } + + public function testGetReturnsRegisteredService() + { + $builder = new ContainerBuilder(); $builder->set('bar', $bar = new \stdClass()); - $this->assertEquals($bar, $builder->get('bar'), '->get() returns the service associated with the id'); - $builder->register('bar', 'stdClass'); - $this->assertEquals($bar, $builder->get('bar'), '->get() returns the service associated with the id even if a definition has been defined'); - $builder->register('baz', 'stdClass')->setArguments(array(new Reference('baz'))); - try { - @$builder->get('baz'); - $this->fail('->get() throws a ServiceCircularReferenceException if the service has a circular reference to itself'); - } catch (ServiceCircularReferenceException $e) { - $this->assertEquals('Circular reference detected for service "baz", path: "baz".', $e->getMessage(), '->get() throws a LogicException if the service has a circular reference to itself'); - } + $this->assertSame($bar, $builder->get('bar'), '->get() returns the service associated with the id'); + } - $this->assertSame($builder->get('bar'), $builder->get('bar'), '->get() always returns the same instance if the service is shared'); + public function testRegisterDoesNotOverrideExistingService() + { + $builder = new ContainerBuilder(); + $builder->set('bar', $bar = new \stdClass()); + $builder->register('bar', 'stdClass'); + + $this->assertSame($bar, $builder->get('bar'), '->get() returns the service associated with the id even if a definition has been defined'); } public function testNonSharedServicesReturnsDifferentInstances() @@ -150,34 +213,23 @@ public function testGetUnsetLoadingServiceWhenCreateServiceThrowsAnException() $builder->get('foo'); } - /** - * @group legacy - */ - public function testGetReturnsNullOnInactiveScope() - { - $builder = new ContainerBuilder(); - $builder->register('foo', 'stdClass')->setScope('request'); - - $this->assertNull($builder->get('foo', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - } - - /** - * @group legacy - */ - public function testGetReturnsNullOnInactiveScopeWhenServiceIsCreatedByAMethod() - { - $builder = new ProjectContainer(); - - $this->assertNull($builder->get('foobaz', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - } - public function testGetServiceIds() { $builder = new ContainerBuilder(); $builder->register('foo', 'stdClass'); $builder->bar = $bar = new \stdClass(); $builder->register('bar', 'stdClass'); - $this->assertEquals(array('foo', 'bar', 'service_container'), $builder->getServiceIds(), '->getServiceIds() returns all defined service ids'); + $this->assertEquals( + array( + 'service_container', + 'foo', + 'bar', + 'Psr\Container\ContainerInterface', + 'Symfony\Component\DependencyInjection\ContainerInterface', + ), + $builder->getServiceIds(), + '->getServiceIds() returns all defined service ids' + ); } public function testAliases() @@ -225,7 +277,7 @@ public function testGetAliases() $builder->set('foobar', 'stdClass'); $builder->set('moo', 'stdClass'); - $this->assertCount(0, $builder->getAliases(), '->getAliases() does not return aliased services that have been overridden'); + $this->assertCount(2, $builder->getAliases(), '->getAliases() does not return aliased services that have been overridden'); } public function testSetAliases() @@ -275,19 +327,23 @@ public function testAddGetCompilerPass() { $builder = new ContainerBuilder(); $builder->setResourceTracking(false); - $builderCompilerPasses = $builder->getCompiler()->getPassConfig()->getPasses(); - $builder->addCompilerPass($this->getMockBuilder('Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface')->getMock()); - - $this->assertCount(count($builder->getCompiler()->getPassConfig()->getPasses()) - 1, $builderCompilerPasses); + $defaultPasses = $builder->getCompiler()->getPassConfig()->getPasses(); + $builder->addCompilerPass($pass1 = $this->getMockBuilder('Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface')->getMock(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -5); + $builder->addCompilerPass($pass2 = $this->getMockBuilder('Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface')->getMock(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); + + $passes = $builder->getCompiler()->getPassConfig()->getPasses(); + $this->assertCount(count($passes) - 2, $defaultPasses); + // Pass 1 is executed later + $this->assertTrue(array_search($pass1, $passes, true) > array_search($pass2, $passes, true)); } public function testCreateService() { $builder = new ContainerBuilder(); $builder->register('foo1', 'Bar\FooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php'); - $this->assertInstanceOf('\Bar\FooClass', $builder->get('foo1'), '->createService() requires the file defined by the service definition'); $builder->register('foo2', 'Bar\FooClass')->setFile(__DIR__.'/Fixtures/includes/%file%.php'); $builder->setParameter('file', 'foo'); + $this->assertInstanceOf('\Bar\FooClass', $builder->get('foo1'), '->createService() requires the file defined by the service definition'); $this->assertInstanceOf('\Bar\FooClass', $builder->get('foo2'), '->createService() replaces parameters in the file provided by the service definition'); } @@ -335,41 +391,6 @@ public function testCreateServiceFactory() $this->assertTrue($builder->get('baz')->called, '->createService() uses another service as factory'); } - /** - * @group legacy - */ - public function testLegacyCreateServiceFactory() - { - $builder = new ContainerBuilder(); - $builder->register('bar', 'Bar\FooClass'); - $builder - ->register('foo1', 'Bar\FooClass') - ->setFactoryClass('%foo_class%') - ->setFactoryMethod('getInstance') - ->addArgument(array('foo' => '%value%', '%value%' => 'foo', new Reference('bar'))) - ; - $builder->setParameter('value', 'bar'); - $builder->setParameter('foo_class', 'Bar\FooClass'); - $this->assertTrue($builder->get('foo1')->called, '->createService() calls the factory method to create the service instance'); - $this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', $builder->get('bar')), $builder->get('foo1')->arguments, '->createService() passes the arguments to the factory method'); - } - - /** - * @group legacy - */ - public function testLegacyCreateServiceFactoryService() - { - $builder = new ContainerBuilder(); - $builder->register('foo_service', 'Bar\FooClass'); - $builder - ->register('foo', 'Bar\FooClass') - ->setFactoryService('%foo_service%') - ->setFactoryMethod('getInstance') - ; - $builder->setParameter('foo_service', 'foo_service'); - $this->assertTrue($builder->get('foo')->called, '->createService() calls the factory method to create the service instance'); - } - public function testCreateServiceMethodCalls() { $builder = new ContainerBuilder(); @@ -401,20 +422,18 @@ public function testCreateServiceConfigurator() { $builder = new ContainerBuilder(); $builder->register('foo1', 'Bar\FooClass')->setConfigurator('sc_configure'); - $this->assertTrue($builder->get('foo1')->configured, '->createService() calls the configurator'); - $builder->register('foo2', 'Bar\FooClass')->setConfigurator(array('%class%', 'configureStatic')); $builder->setParameter('class', 'BazClass'); - $this->assertTrue($builder->get('foo2')->configured, '->createService() calls the configurator'); - $builder->register('baz', 'BazClass'); $builder->register('foo3', 'Bar\FooClass')->setConfigurator(array(new Reference('baz'), 'configure')); - $this->assertTrue($builder->get('foo3')->configured, '->createService() calls the configurator'); - $builder->register('foo4', 'Bar\FooClass')->setConfigurator(array($builder->getDefinition('baz'), 'configure')); + $builder->register('foo5', 'Bar\FooClass')->setConfigurator('foo'); + + $this->assertTrue($builder->get('foo1')->configured, '->createService() calls the configurator'); + $this->assertTrue($builder->get('foo2')->configured, '->createService() calls the configurator'); + $this->assertTrue($builder->get('foo3')->configured, '->createService() calls the configurator'); $this->assertTrue($builder->get('foo4')->configured, '->createService() calls the configurator'); - $builder->register('foo5', 'Bar\FooClass')->setConfigurator('foo'); try { $builder->get('foo5'); $this->fail('->createService() throws an InvalidArgumentException if the configure callable is not a valid callable'); @@ -423,6 +442,42 @@ public function testCreateServiceConfigurator() } } + public function testCreateServiceWithIteratorArgument() + { + $builder = new ContainerBuilder(); + $builder->register('bar', 'stdClass'); + $builder + ->register('lazy_context', 'LazyContext') + ->setArguments(array( + new IteratorArgument(array('k1' => new Reference('bar'), new Reference('invalid', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))), + new IteratorArgument(array()), + )) + ; + + $lazyContext = $builder->get('lazy_context'); + $this->assertInstanceOf(RewindableGenerator::class, $lazyContext->lazyValues); + $this->assertInstanceOf(RewindableGenerator::class, $lazyContext->lazyEmptyValues); + $this->assertCount(1, $lazyContext->lazyValues); + $this->assertCount(0, $lazyContext->lazyEmptyValues); + + $i = 0; + foreach ($lazyContext->lazyValues as $k => $v) { + ++$i; + $this->assertEquals('k1', $k); + $this->assertInstanceOf('\stdClass', $v); + } + + // The second argument should have been ignored. + $this->assertEquals(1, $i); + + $i = 0; + foreach ($lazyContext->lazyEmptyValues as $k => $v) { + ++$i; + } + + $this->assertEquals(0, $i); + } + /** * @expectedException \RuntimeException */ @@ -459,8 +514,8 @@ public function testResolveServicesWithDecoratedDefinition() { $builder = new ContainerBuilder(); $builder->setDefinition('grandpa', new Definition('stdClass')); - $builder->setDefinition('parent', new DefinitionDecorator('grandpa')); - $builder->setDefinition('foo', new DefinitionDecorator('parent')); + $builder->setDefinition('parent', new ChildDefinition('grandpa')); + $builder->setDefinition('foo', new ChildDefinition('parent')); $builder->get('foo'); } @@ -503,7 +558,7 @@ public function testMerge() $config->setDefinition('baz', new Definition('BazClass')); $config->setAlias('alias_for_foo', 'foo'); $container->merge($config); - $this->assertEquals(array('foo', 'bar', 'baz'), array_keys($container->getDefinitions()), '->merge() merges definitions already defined ones'); + $this->assertEquals(array('service_container', 'foo', 'bar', 'baz'), array_keys($container->getDefinitions()), '->merge() merges definitions already defined ones'); $aliases = $container->getAliases(); $this->assertTrue(isset($aliases['alias_for_foo'])); @@ -515,6 +570,81 @@ public function testMerge() $config->setDefinition('foo', new Definition('BazClass')); $container->merge($config); $this->assertEquals('BazClass', $container->getDefinition('foo')->getClass(), '->merge() overrides already defined services'); + + $container = new ContainerBuilder(); + $bag = new EnvPlaceholderParameterBag(); + $bag->get('env(Foo)'); + $config = new ContainerBuilder($bag); + $this->assertSame(array('%env(Bar)%'), $config->resolveEnvPlaceholders(array($bag->get('env(Bar)')))); + $container->merge($config); + $this->assertEquals(array('Foo' => 0, 'Bar' => 1), $container->getEnvCounters()); + + $container = new ContainerBuilder(); + $config = new ContainerBuilder(); + $childDefA = $container->registerForAutoconfiguration('AInterface'); + $childDefB = $config->registerForAutoconfiguration('BInterface'); + $container->merge($config); + $this->assertSame(array('AInterface' => $childDefA, 'BInterface' => $childDefB), $container->getAutoconfiguredInstanceof()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage "AInterface" has already been autoconfigured and merge() does not support merging autoconfiguration for the same class/interface. + */ + public function testMergeThrowsExceptionForDuplicateAutomaticInstanceofDefinitions() + { + $container = new ContainerBuilder(); + $config = new ContainerBuilder(); + $container->registerForAutoconfiguration('AInterface'); + $config->registerForAutoconfiguration('AInterface'); + $container->merge($config); + } + + public function testResolveEnvValues() + { + $_ENV['DUMMY_ENV_VAR'] = 'du%%y'; + $_SERVER['DUMMY_SERVER_VAR'] = 'ABC'; + $_SERVER['HTTP_DUMMY_VAR'] = 'DEF'; + + $container = new ContainerBuilder(); + $container->setParameter('bar', '%% %env(DUMMY_ENV_VAR)% %env(DUMMY_SERVER_VAR)% %env(HTTP_DUMMY_VAR)%'); + $container->setParameter('env(HTTP_DUMMY_VAR)', '123'); + + $this->assertSame('%% du%%%%y ABC 123', $container->resolveEnvPlaceholders('%bar%', true)); + + unset($_ENV['DUMMY_ENV_VAR'], $_SERVER['DUMMY_SERVER_VAR'], $_SERVER['HTTP_DUMMY_VAR']); + } + + public function testCompileWithResolveEnv() + { + putenv('DUMMY_ENV_VAR=du%%y'); + $_SERVER['DUMMY_SERVER_VAR'] = 'ABC'; + $_SERVER['HTTP_DUMMY_VAR'] = 'DEF'; + + $container = new ContainerBuilder(); + $container->setParameter('env(FOO)', 'Foo'); + $container->setParameter('bar', '%% %env(DUMMY_ENV_VAR)% %env(DUMMY_SERVER_VAR)% %env(HTTP_DUMMY_VAR)%'); + $container->setParameter('foo', '%env(FOO)%'); + $container->setParameter('baz', '%foo%'); + $container->setParameter('env(HTTP_DUMMY_VAR)', '123'); + $container->compile(true); + + $this->assertSame('% du%%y ABC 123', $container->getParameter('bar')); + $this->assertSame('Foo', $container->getParameter('baz')); + + unset($_SERVER['DUMMY_SERVER_VAR'], $_SERVER['HTTP_DUMMY_VAR']); + putenv('DUMMY_ENV_VAR'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\EnvNotFoundException + * @expectedExceptionMessage Environment variable not found: "FOO". + */ + public function testCompileWithResolveMissingEnv() + { + $container = new ContainerBuilder(); + $container->setParameter('foo', '%env(FOO)%'); + $container->compile(true); } /** @@ -581,7 +711,7 @@ public function testAddObjectResource() $resources = $container->getResources(); - $this->assertCount(1, $resources, '1 resource was registered'); + $this->assertCount(2, $resources, '2 resources were registered'); /* @var $resource \Symfony\Component\Config\Resource\FileResource */ $resource = end($resources); @@ -590,27 +720,30 @@ public function testAddObjectResource() $this->assertSame(realpath(__DIR__.'/Fixtures/includes/classes.php'), realpath($resource->getResource())); } - public function testAddClassResource() + public function testGetReflectionClass() { $container = new ContainerBuilder(); $container->setResourceTracking(false); - $container->addClassResource(new \ReflectionClass('BarClass')); + $r1 = $container->getReflectionClass('BarClass'); $this->assertEmpty($container->getResources(), 'No resources get registered without resource tracking'); $container->setResourceTracking(true); - $container->addClassResource(new \ReflectionClass('BarClass')); + $r2 = $container->getReflectionClass('BarClass'); + $r3 = $container->getReflectionClass('BarClass'); - $resources = $container->getResources(); + $this->assertNull($container->getReflectionClass('BarMissingClass')); - $this->assertCount(1, $resources, '1 resource was registered'); + $this->assertEquals($r1, $r2); + $this->assertSame($r2, $r3); - /* @var $resource \Symfony\Component\Config\Resource\FileResource */ - $resource = end($resources); + $resources = $container->getResources(); - $this->assertInstanceOf('Symfony\Component\Config\Resource\FileResource', $resource); - $this->assertSame(realpath(__DIR__.'/Fixtures/includes/classes.php'), realpath($resource->getResource())); + $this->assertCount(3, $resources, '3 resources were registered'); + + $this->assertSame('reflection.BarClass', (string) $resources[1]); + $this->assertSame('BarMissingClass', (string) end($resources)); } public function testCompilesClassDefinitionsOfLazyServices() @@ -624,11 +757,10 @@ public function testCompilesClassDefinitionsOfLazyServices() $container->compile(); - $classesPath = realpath(__DIR__.'/Fixtures/includes/classes.php'); $matchingResources = array_filter( $container->getResources(), - function (ResourceInterface $resource) use ($classesPath) { - return $resource instanceof FileResource && $classesPath === realpath($resource->getResource()); + function (ResourceInterface $resource) { + return 'reflection.BarClass' === (string) $resource; } ); @@ -651,6 +783,26 @@ public function testResources() $this->assertEquals(array(), $container->getResources()); } + public function testFileExists() + { + $container = new ContainerBuilder(); + $A = new ComposerResource(); + $a = new FileResource(__DIR__.'/Fixtures/xml/services1.xml'); + $b = new FileResource(__DIR__.'/Fixtures/xml/services2.xml'); + $c = new DirectoryResource($dir = dirname($b)); + + $this->assertTrue($container->fileExists((string) $a) && $container->fileExists((string) $b) && $container->fileExists($dir)); + + $resources = array(); + foreach ($container->getResources() as $resource) { + if (false === strpos($resource, '.php')) { + $resources[] = $resource; + } + } + + $this->assertEquals(array($A, $a, $b, $c), $resources, '->getResources() returns an array of resources read for the current configuration'); + } + public function testExtension() { $container = new ContainerBuilder(); @@ -709,7 +861,7 @@ public function testPrivateServiceUser() /** * @expectedException \BadMethodCallException */ - public function testThrowsExceptionWhenSetServiceOnAFrozenContainer() + public function testThrowsExceptionWhenSetServiceOnACompiledContainer() { $container = new ContainerBuilder(); $container->setResourceTracking(false); @@ -718,7 +870,7 @@ public function testThrowsExceptionWhenSetServiceOnAFrozenContainer() $container->set('a', new \stdClass()); } - public function testThrowsExceptionWhenAddServiceOnAFrozenContainer() + public function testThrowsExceptionWhenAddServiceOnACompiledContainer() { $container = new ContainerBuilder(); $container->compile(); @@ -726,7 +878,7 @@ public function testThrowsExceptionWhenAddServiceOnAFrozenContainer() $this->assertSame($foo, $container->get('a')); } - public function testNoExceptionWhenSetSyntheticServiceOnAFrozenContainer() + public function testNoExceptionWhenSetSyntheticServiceOnACompiledContainer() { $container = new ContainerBuilder(); $def = new Definition('stdClass'); @@ -737,62 +889,10 @@ public function testNoExceptionWhenSetSyntheticServiceOnAFrozenContainer() $this->assertEquals($a, $container->get('a')); } - /** - * @group legacy - */ - public function testLegacySetOnSynchronizedService() - { - $container = new ContainerBuilder(); - $container->register('baz', 'BazClass') - ->setSynchronized(true) - ; - $container->register('bar', 'BarClass') - ->addMethodCall('setBaz', array(new Reference('baz'))) - ; - - $container->set('baz', $baz = new \BazClass()); - $this->assertSame($baz, $container->get('bar')->getBaz()); - - $container->set('baz', $baz = new \BazClass()); - $this->assertSame($baz, $container->get('bar')->getBaz()); - } - - /** - * @group legacy - */ - public function testLegacySynchronizedServiceWithScopes() - { - $container = new ContainerBuilder(); - $container->addScope(new Scope('foo')); - $container->register('baz', 'BazClass') - ->setSynthetic(true) - ->setSynchronized(true) - ->setScope('foo') - ; - $container->register('bar', 'BarClass') - ->addMethodCall('setBaz', array(new Reference('baz', ContainerInterface::NULL_ON_INVALID_REFERENCE, false))) - ; - $container->compile(); - - $container->enterScope('foo'); - $container->set('baz', $outerBaz = new \BazClass(), 'foo'); - $this->assertSame($outerBaz, $container->get('bar')->getBaz()); - - $container->enterScope('foo'); - $container->set('baz', $innerBaz = new \BazClass(), 'foo'); - $this->assertSame($innerBaz, $container->get('bar')->getBaz()); - $container->leaveScope('foo'); - - $this->assertNotSame($innerBaz, $container->get('bar')->getBaz()); - $this->assertSame($outerBaz, $container->get('bar')->getBaz()); - - $container->leaveScope('foo'); - } - /** * @expectedException \BadMethodCallException */ - public function testThrowsExceptionWhenSetDefinitionOnAFrozenContainer() + public function testThrowsExceptionWhenSetDefinitionOnACompiledContainer() { $container = new ContainerBuilder(); $container->setResourceTracking(false); @@ -847,16 +947,13 @@ public function testLazyLoadedService() $container->compile(); - $class = new \BazClass(); - $reflectionClass = new \ReflectionClass($class); - $r = new \ReflectionProperty($container, 'resources'); $r->setAccessible(true); $resources = $r->getValue($container); $classInList = false; foreach ($resources as $resource) { - if ($resource->getResource() === $reflectionClass->getFileName()) { + if ('reflection.BazClass' === (string) $resource) { $classInList = true; break; } @@ -883,47 +980,109 @@ public function testAutowiring() { $container = new ContainerBuilder(); - $container->register('a', __NAMESPACE__.'\A'); + $container->register(A::class); $bDefinition = $container->register('b', __NAMESPACE__.'\B'); $bDefinition->setAutowired(true); $container->compile(); - $this->assertEquals('a', (string) $container->getDefinition('b')->getArgument(0)); + $this->assertEquals(A::class, (string) $container->getDefinition('b')->getArgument(0)); + } + + public function testClassFromId() + { + $container = new ContainerBuilder(); + + $unknown = $container->register('Acme\UnknownClass'); + $autoloadClass = $container->register(CaseSensitiveClass::class); + $container->compile(); + + $this->assertSame('Acme\UnknownClass', $unknown->getClass()); + $this->assertEquals(CaseSensitiveClass::class, $autoloadClass->getClass()); } /** - * This test checks the trigger of a deprecation note and should not be removed in major releases. - * - * @group legacy - * @expectedDeprecation The "foo" service is deprecated. You should stop using it, as it will soon be removed. + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage The definition for "DateTime" has no class attribute, and appears to reference a class or interface in the global namespace. */ - public function testPrivateServiceTriggersDeprecation() + public function testNoClassFromGlobalNamespaceClassId() { $container = new ContainerBuilder(); - $container->register('foo', 'stdClass') - ->setPublic(false) - ->setDeprecated(true); - $container->register('bar', 'stdClass') - ->setPublic(true) - ->setProperty('foo', new Reference('foo')); + $definition = $container->register(\DateTime::class); $container->compile(); + } - $container->get('bar'); + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage The definition for "123_abc" has no class. + */ + public function testNoClassFromNonClassId() + { + $container = new ContainerBuilder(); + + $definition = $container->register('123_abc'); + $container->compile(); } -} -class FooClass -{ -} + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage The definition for "\foo" has no class. + */ + public function testNoClassFromNsSeparatorId() + { + $container = new ContainerBuilder(); -class ProjectContainer extends ContainerBuilder -{ - public function getFoobazService() + $definition = $container->register('\\foo'); + $container->compile(); + } + + public function testServiceLocator() { - throw new InactiveScopeException('foo', 'request'); + $container = new ContainerBuilder(); + $container->register('foo_service', ServiceLocator::class) + ->addArgument(array( + 'bar' => new ServiceClosureArgument(new Reference('bar_service')), + 'baz' => new ServiceClosureArgument(new TypedReference('baz_service', 'stdClass')), + )) + ; + $container->register('bar_service', 'stdClass')->setArguments(array(new Reference('baz_service'))); + $container->register('baz_service', 'stdClass')->setPublic(false); + $container->compile(); + + $this->assertInstanceOf(ServiceLocator::class, $foo = $container->get('foo_service')); + $this->assertSame($container->get('bar_service'), $foo->get('bar')); } + + public function testRegisterForAutoconfiguration() + { + $container = new ContainerBuilder(); + $childDefA = $container->registerForAutoconfiguration('AInterface'); + $childDefB = $container->registerForAutoconfiguration('BInterface'); + $this->assertSame(array('AInterface' => $childDefA, 'BInterface' => $childDefB), $container->getAutoconfiguredInstanceof()); + + // when called multiple times, the same instance is returned + $this->assertSame($childDefA, $container->registerForAutoconfiguration('AInterface')); + } + + public function testCaseSensitivity() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass'); + $container->register('Foo', 'stdClass')->setProperty('foo', new Reference('foo'))->setPublic(false); + $container->register('fOO', 'stdClass')->setProperty('Foo', new Reference('Foo')); + + $this->assertSame(array('service_container', 'foo', 'Foo', 'fOO', 'Psr\Container\ContainerInterface', 'Symfony\Component\DependencyInjection\ContainerInterface'), $container->getServiceIds()); + + $container->compile(); + + $this->assertNotSame($container->get('foo'), $container->get('fOO'), '->get() returns the service for the given id, case sensitively'); + $this->assertSame($container->get('fOO')->Foo->foo, $container->get('foo'), '->get() returns the service for the given id, case sensitively'); + } +} + +class FooClass +{ } class A diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php index 721932591f177..46780ed3f01ac 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php @@ -12,11 +12,11 @@ namespace Symfony\Component\DependencyInjection\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\Scope; +use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; -use Symfony\Component\DependencyInjection\Exception\InactiveScopeException; +use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; class ContainerTest extends TestCase { @@ -83,12 +83,18 @@ public function testCompile() $this->assertEquals(array('foo' => 'bar'), $sc->getParameterBag()->all(), '->compile() copies the current parameters to the new parameter bag'); } - public function testIsFrozen() + public function testIsCompiled() { $sc = new Container(new ParameterBag(array('foo' => 'bar'))); - $this->assertFalse($sc->isFrozen(), '->isFrozen() returns false if the parameters are not frozen'); + $this->assertFalse($sc->isCompiled(), '->isCompiled() returns false if the container is not compiled'); $sc->compile(); - $this->assertTrue($sc->isFrozen(), '->isFrozen() returns true if the parameters are frozen'); + $this->assertTrue($sc->isCompiled(), '->isCompiled() returns true if the container is compiled'); + } + + public function testIsCompiledWithFrozenParameters() + { + $sc = new Container(new FrozenParameterBag(array('foo' => 'bar'))); + $this->assertFalse($sc->isCompiled(), '->isCompiled() returns false if the container is not compiled but the parameter bag is already frozen'); } public function testGetParameterBag() @@ -128,7 +134,7 @@ public function testGetServiceIds() $sc = new ProjectServiceContainer(); $sc->set('foo', $obj = new \stdClass()); - $this->assertEquals(array('scoped', 'scoped_foo', 'scoped_synchronized_foo', 'inactive', 'bar', 'foo_bar', 'foo.baz', 'circular', 'throw_exception', 'throws_exception_on_service_configuration', 'service_container', 'foo'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by getXXXService() methods, followed by service ids defined by set()'); + $this->assertEquals(array('service_container', 'bar', 'foo_bar', 'foo.baz', 'circular', 'throw_exception', 'throws_exception_on_service_configuration', 'internal_dependency', 'foo'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by factory methods in the method map, followed by service ids defined by set()'); } public function testSet() @@ -141,58 +147,11 @@ public function testSet() public function testSetWithNullResetTheService() { $sc = new Container(); + $sc->set('foo', new \stdClass()); $sc->set('foo', null); $this->assertFalse($sc->has('foo'), '->set() with null service resets the service'); } - /** - * @expectedException \InvalidArgumentException - * @group legacy - */ - public function testSetDoesNotAllowPrototypeScope() - { - $c = new Container(); - $c->set('foo', new \stdClass(), Container::SCOPE_PROTOTYPE); - } - - /** - * @expectedException \RuntimeException - * @group legacy - */ - public function testSetDoesNotAllowInactiveScope() - { - $c = new Container(); - $c->addScope(new Scope('foo')); - $c->set('foo', new \stdClass(), 'foo'); - } - - /** - * @group legacy - */ - public function testSetAlsoSetsScopedService() - { - $c = new Container(); - $c->addScope(new Scope('foo')); - $c->enterScope('foo'); - $c->set('foo', $foo = new \stdClass(), 'foo'); - - $scoped = $this->getField($c, 'scopedServices'); - $this->assertTrue(isset($scoped['foo']['foo']), '->set() sets a scoped service'); - $this->assertSame($foo, $scoped['foo']['foo'], '->set() sets a scoped service'); - } - - /** - * @group legacy - */ - public function testSetAlsoCallsSynchronizeService() - { - $c = new ProjectServiceContainer(); - $c->addScope(new Scope('foo')); - $c->enterScope('foo'); - $c->set('scoped_synchronized_foo', $bar = new \stdClass(), 'foo'); - $this->assertTrue($c->synchronized, '->set() calls synchronize*Service() if it is defined for the service'); - } - public function testSetReplacesAlias() { $c = new ProjectServiceContainer(); @@ -206,14 +165,9 @@ public function testGet() $sc = new ProjectServiceContainer(); $sc->set('foo', $foo = new \stdClass()); $this->assertSame($foo, $sc->get('foo'), '->get() returns the service for the given id'); - $this->assertSame($foo, $sc->get('Foo'), '->get() returns the service for the given id, and converts id to lowercase'); $this->assertSame($sc->__bar, $sc->get('bar'), '->get() returns the service for the given id'); $this->assertSame($sc->__foo_bar, $sc->get('foo_bar'), '->get() returns the service if a get*Method() is defined'); $this->assertSame($sc->__foo_baz, $sc->get('foo.baz'), '->get() returns the service if a get*Method() is defined'); - $this->assertSame($sc->__foo_baz, $sc->get('foo\\baz'), '->get() returns the service if a get*Method() is defined'); - - $sc->set('bar', $bar = new \stdClass()); - $this->assertSame($bar, $sc->get('bar'), '->get() prefers to return a service defined with set() than one defined with a getXXXMethod()'); try { $sc->get(''); @@ -224,6 +178,17 @@ public function testGet() $this->assertNull($sc->get('', ContainerInterface::NULL_ON_INVALID_REFERENCE), '->get() returns null if the service is empty'); } + public function testCaseSensitivity() + { + $sc = new Container(); + $sc->set('foo', $foo1 = new \stdClass()); + $sc->set('Foo', $foo2 = new \stdClass()); + + $this->assertSame(array('service_container', 'foo', 'Foo'), $sc->getServiceIds()); + $this->assertSame($foo1, $sc->get('foo'), '->get() returns the service for the given id, case sensitively'); + $this->assertSame($foo2, $sc->get('Foo'), '->get() returns the service for the given id, case sensitively'); + } + public function testGetThrowServiceNotFoundException() { $sc = new ProjectServiceContainer(); @@ -260,24 +225,15 @@ public function testGetCircularReference() } /** - * @group legacy + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * @expectedExceptionMessage You have requested a non-existent service "request". */ - public function testGetReturnsNullOnInactiveScope() + public function testGetSyntheticServiceThrows() { - $sc = new ProjectServiceContainer(); - $this->assertNull($sc->get('inactive', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - } - - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage You have requested a synthetic service ("request"). The DIC does not know how to construct this service. - */ - public function testGetSyntheticServiceAlwaysThrows() - { - require_once __DIR__.'/Fixtures/php/services9.php'; + require_once __DIR__.'/Fixtures/php/services9_compiled.php'; $container = new \ProjectServiceContainer(); - $container->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE); + $container->get('request'); } public function testHas() @@ -289,7 +245,6 @@ public function testHas() $this->assertTrue($sc->has('bar'), '->has() returns true if a get*Method() is defined'); $this->assertTrue($sc->has('foo_bar'), '->has() returns true if a get*Method() is defined'); $this->assertTrue($sc->has('foo.baz'), '->has() returns true if a get*Method() is defined'); - $this->assertTrue($sc->has('foo\\baz'), '->has() returns true if a get*Method() is defined'); } public function testInitialized() @@ -301,309 +256,27 @@ public function testInitialized() $this->assertFalse($sc->initialized('bar'), '->initialized() returns false if a service is defined, but not currently loaded'); $this->assertFalse($sc->initialized('alias'), '->initialized() returns false if an aliased service is not initialized'); - $sc->set('bar', new \stdClass()); + $sc->get('bar'); $this->assertTrue($sc->initialized('alias'), '->initialized() returns true for alias if aliased service is initialized'); } - public function testReset() + public function testInitializedWithPrivateService() { - $c = new Container(); - $c->set('bar', new \stdClass()); - - $c->reset(); - - $this->assertNull($c->get('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - } - - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - * @expectedExceptionMessage Resetting the container is not allowed when a scope is active. - * @group legacy - */ - public function testCannotResetInActiveScope() - { - $c = new Container(); - $c->addScope(new Scope('foo')); - $c->set('bar', new \stdClass()); - - $c->enterScope('foo'); - - $c->reset(); + $sc = new ProjectServiceContainer(); + $sc->get('internal_dependency'); + $this->assertFalse($sc->initialized('internal')); } - /** - * @group legacy - */ - public function testResetAfterLeavingScope() + public function testReset() { $c = new Container(); - $c->addScope(new Scope('foo')); $c->set('bar', new \stdClass()); - $c->enterScope('foo'); - $c->leaveScope('foo'); - $c->reset(); $this->assertNull($c->get('bar', ContainerInterface::NULL_ON_INVALID_REFERENCE)); } - /** - * @group legacy - */ - public function testEnterLeaveCurrentScope() - { - $container = new ProjectServiceContainer(); - $container->addScope(new Scope('foo')); - - $container->enterScope('foo'); - $container->set('foo', new \stdClass(), 'foo'); - $scoped1 = $container->get('scoped'); - $scopedFoo1 = $container->get('scoped_foo'); - - $container->enterScope('foo'); - $container->set('foo', new \stdClass(), 'foo'); - $scoped2 = $container->get('scoped'); - $scoped3 = $container->get('SCOPED'); - $scopedFoo2 = $container->get('scoped_foo'); - - $container->set('foo', null, 'foo'); - $container->leaveScope('foo'); - $scoped4 = $container->get('scoped'); - $scopedFoo3 = $container->get('scoped_foo'); - - $this->assertNotSame($scoped1, $scoped2); - $this->assertSame($scoped2, $scoped3); - $this->assertSame($scoped1, $scoped4); - $this->assertNotSame($scopedFoo1, $scopedFoo2); - $this->assertSame($scopedFoo1, $scopedFoo3); - } - - /** - * @group legacy - */ - public function testEnterLeaveScopeWithChildScopes() - { - $container = new Container(); - $container->addScope(new Scope('foo')); - $container->addScope(new Scope('bar', 'foo')); - - $this->assertFalse($container->isScopeActive('foo')); - - $container->enterScope('foo'); - $container->enterScope('bar'); - - $this->assertTrue($container->isScopeActive('foo')); - $this->assertFalse($container->has('a')); - - $a = new \stdClass(); - $container->set('a', $a, 'bar'); - - $scoped = $this->getField($container, 'scopedServices'); - $this->assertTrue(isset($scoped['bar']['a'])); - $this->assertSame($a, $scoped['bar']['a']); - $this->assertTrue($container->has('a')); - - $container->leaveScope('foo'); - - $scoped = $this->getField($container, 'scopedServices'); - $this->assertFalse(isset($scoped['bar'])); - $this->assertFalse($container->isScopeActive('foo')); - $this->assertFalse($container->has('a')); - } - - /** - * @group legacy - */ - public function testEnterScopeRecursivelyWithInactiveChildScopes() - { - $container = new Container(); - $container->addScope(new Scope('foo')); - $container->addScope(new Scope('bar', 'foo')); - - $this->assertFalse($container->isScopeActive('foo')); - - $container->enterScope('foo'); - - $this->assertTrue($container->isScopeActive('foo')); - $this->assertFalse($container->isScopeActive('bar')); - $this->assertFalse($container->has('a')); - - $a = new \stdClass(); - $container->set('a', $a, 'foo'); - - $scoped = $this->getField($container, 'scopedServices'); - $this->assertTrue(isset($scoped['foo']['a'])); - $this->assertSame($a, $scoped['foo']['a']); - $this->assertTrue($container->has('a')); - - $container->enterScope('foo'); - - $scoped = $this->getField($container, 'scopedServices'); - $this->assertFalse(isset($scoped['a'])); - $this->assertTrue($container->isScopeActive('foo')); - $this->assertFalse($container->isScopeActive('bar')); - $this->assertFalse($container->has('a')); - - $container->enterScope('bar'); - - $this->assertTrue($container->isScopeActive('bar')); - - $container->leaveScope('foo'); - - $this->assertTrue($container->isScopeActive('foo')); - $this->assertFalse($container->isScopeActive('bar')); - $this->assertTrue($container->has('a')); - } - - /** - * @group legacy - */ - public function testEnterChildScopeRecursively() - { - $container = new Container(); - $container->addScope(new Scope('foo')); - $container->addScope(new Scope('bar', 'foo')); - - $container->enterScope('foo'); - $container->enterScope('bar'); - - $this->assertTrue($container->isScopeActive('bar')); - $this->assertFalse($container->has('a')); - - $a = new \stdClass(); - $container->set('a', $a, 'bar'); - - $scoped = $this->getField($container, 'scopedServices'); - $this->assertTrue(isset($scoped['bar']['a'])); - $this->assertSame($a, $scoped['bar']['a']); - $this->assertTrue($container->has('a')); - - $container->enterScope('bar'); - - $scoped = $this->getField($container, 'scopedServices'); - $this->assertFalse(isset($scoped['a'])); - $this->assertTrue($container->isScopeActive('foo')); - $this->assertTrue($container->isScopeActive('bar')); - $this->assertFalse($container->has('a')); - - $container->leaveScope('bar'); - - $this->assertTrue($container->isScopeActive('foo')); - $this->assertTrue($container->isScopeActive('bar')); - $this->assertTrue($container->has('a')); - } - - /** - * @expectedException \InvalidArgumentException - * @group legacy - */ - public function testEnterScopeNotAdded() - { - $container = new Container(); - $container->enterScope('foo'); - } - - /** - * @expectedException \RuntimeException - * @group legacy - */ - public function testEnterScopeDoesNotAllowInactiveParentScope() - { - $container = new Container(); - $container->addScope(new Scope('foo')); - $container->addScope(new Scope('bar', 'foo')); - $container->enterScope('bar'); - } - - /** - * @group legacy - */ - public function testLeaveScopeNotActive() - { - $container = new Container(); - $container->addScope(new Scope('foo')); - - try { - $container->leaveScope('foo'); - $this->fail('->leaveScope() throws a \LogicException if the scope is not active yet'); - } catch (\Exception $e) { - $this->assertInstanceOf('\LogicException', $e, '->leaveScope() throws a \LogicException if the scope is not active yet'); - $this->assertEquals('The scope "foo" is not active.', $e->getMessage(), '->leaveScope() throws a \LogicException if the scope is not active yet'); - } - - try { - $container->leaveScope('bar'); - $this->fail('->leaveScope() throws a \LogicException if the scope does not exist'); - } catch (\Exception $e) { - $this->assertInstanceOf('\LogicException', $e, '->leaveScope() throws a \LogicException if the scope does not exist'); - $this->assertEquals('The scope "bar" is not active.', $e->getMessage(), '->leaveScope() throws a \LogicException if the scope does not exist'); - } - } - - /** - * @expectedException \InvalidArgumentException - * @dataProvider getLegacyBuiltInScopes - * @group legacy - */ - public function testAddScopeDoesNotAllowBuiltInScopes($scope) - { - $container = new Container(); - $container->addScope(new Scope($scope)); - } - - /** - * @expectedException \InvalidArgumentException - * @group legacy - */ - public function testAddScopeDoesNotAllowExistingScope() - { - $container = new Container(); - $container->addScope(new Scope('foo')); - $container->addScope(new Scope('foo')); - } - - /** - * @expectedException \InvalidArgumentException - * @dataProvider getLegacyInvalidParentScopes - * @group legacy - */ - public function testAddScopeDoesNotAllowInvalidParentScope($scope) - { - $c = new Container(); - $c->addScope(new Scope('foo', $scope)); - } - - /** - * @group legacy - */ - public function testAddScope() - { - $c = new Container(); - $c->addScope(new Scope('foo')); - $c->addScope(new Scope('bar', 'foo')); - - $this->assertSame(array('foo' => 'container', 'bar' => 'foo'), $this->getField($c, 'scopes')); - $this->assertSame(array('foo' => array('bar'), 'bar' => array()), $this->getField($c, 'scopeChildren')); - - $c->addScope(new Scope('baz', 'bar')); - - $this->assertSame(array('foo' => 'container', 'bar' => 'foo', 'baz' => 'bar'), $this->getField($c, 'scopes')); - $this->assertSame(array('foo' => array('bar', 'baz'), 'bar' => array('baz'), 'baz' => array()), $this->getField($c, 'scopeChildren')); - } - - /** - * @group legacy - */ - public function testHasScope() - { - $c = new Container(); - - $this->assertFalse($c->hasScope('foo')); - $c->addScope(new Scope('foo')); - $this->assertTrue($c->hasScope('foo')); - } - /** * @expectedException \Exception * @expectedExceptionMessage Something went terribly wrong! @@ -643,41 +316,6 @@ public function testGetThrowsExceptionOnServiceConfiguration() $this->assertFalse($c->initialized('throws_exception_on_service_configuration')); } - /** - * @group legacy - */ - public function testIsScopeActive() - { - $c = new Container(); - - $this->assertFalse($c->isScopeActive('foo')); - $c->addScope(new Scope('foo')); - - $this->assertFalse($c->isScopeActive('foo')); - $c->enterScope('foo'); - - $this->assertTrue($c->isScopeActive('foo')); - $c->leaveScope('foo'); - - $this->assertFalse($c->isScopeActive('foo')); - } - - public function getLegacyInvalidParentScopes() - { - return array( - array(ContainerInterface::SCOPE_PROTOTYPE), - array('bar'), - ); - } - - public function getLegacyBuiltInScopes() - { - return array( - array(ContainerInterface::SCOPE_CONTAINER), - array(ContainerInterface::SCOPE_PROTOTYPE), - ); - } - protected function getField($obj, $field) { $reflection = new \ReflectionProperty($obj, $field); @@ -698,11 +336,37 @@ public function testThatCloningIsNotSupported() { $class = new \ReflectionClass('Symfony\Component\DependencyInjection\Container'); $clone = $class->getMethod('__clone'); - if (\PHP_VERSION_ID >= 50400) { - $this->assertFalse($class->isCloneable()); - } + $this->assertFalse($class->isCloneable()); $this->assertTrue($clone->isPrivate()); } + + public function testCheckExistenceOfAnInternalPrivateService() + { + $c = new ProjectServiceContainer(); + $c->get('internal_dependency'); + $this->assertFalse($c->has('internal')); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * @expectedExceptionMessage You have requested a non-existent service "internal". + */ + public function testRequestAnInternalSharedPrivateService() + { + $c = new ProjectServiceContainer(); + $c->get('internal_dependency'); + $c->get('internal'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage You cannot set the pre-defined service "bar". + */ + public function testReplacingAPreDefinedService() + { + $c = new ProjectServiceContainer(); + $c->set('bar', new \stdClass()); + } } class ProjectServiceContainer extends Container @@ -710,7 +374,16 @@ class ProjectServiceContainer extends Container public $__bar; public $__foo_bar; public $__foo_baz; - public $synchronized; + public $__internal; + protected $methodMap = array( + 'bar' => 'getBarService', + 'foo_bar' => 'getFooBarService', + 'foo.baz' => 'getFoo_BazService', + 'circular' => 'getCircularService', + 'throw_exception' => 'getThrowExceptionService', + 'throws_exception_on_service_configuration' => 'getThrowsExceptionOnServiceConfigurationService', + 'internal_dependency' => 'getInternalDependencyService', + ); public function __construct() { @@ -719,56 +392,19 @@ public function __construct() $this->__bar = new \stdClass(); $this->__foo_bar = new \stdClass(); $this->__foo_baz = new \stdClass(); - $this->synchronized = false; + $this->__internal = new \stdClass(); + $this->privates = array(); $this->aliases = array('alias' => 'bar'); } - protected function getScopedService() - { - if (!$this->isScopeActive('foo')) { - throw new \RuntimeException('Invalid call'); - } - - return $this->services['scoped'] = $this->scopedServices['foo']['scoped'] = new \stdClass(); - } - - protected function getScopedFooService() + protected function getInternalService() { - if (!$this->isScopeActive('foo')) { - throw new \RuntimeException('invalid call'); - } - - return $this->services['scoped_foo'] = $this->scopedServices['foo']['scoped_foo'] = new \stdClass(); - } - - protected function getScopedSynchronizedFooService() - { - if (!$this->isScopeActive('foo')) { - throw new \RuntimeException('invalid call'); - } - - return $this->services['scoped_bar'] = $this->scopedServices['foo']['scoped_bar'] = new \stdClass(); - } - - protected function synchronizeFooService() - { - // Typically get the service to pass it to a setter - $this->get('foo'); - } - - protected function synchronizeScopedSynchronizedFooService() - { - $this->synchronized = true; - } - - protected function getInactiveService() - { - throw new InactiveScopeException('request', 'request'); + return $this->privates['internal'] = $this->__internal; } protected function getBarService() { - return $this->__bar; + return $this->services['bar'] = $this->__bar; } protected function getFooBarService() @@ -797,4 +433,13 @@ protected function getThrowsExceptionOnServiceConfigurationService() throw new \Exception('Something was terribly wrong while trying to configure the service!'); } + + protected function getInternalDependencyService() + { + $this->services['internal_dependency'] = $instance = new \stdClass(); + + $instance->internal = $this->privates['internal'] ?? $this->getInternalService(); + + return $instance; + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php b/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php index 6bdc8f4f49501..dbdbb79542316 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/CrossCheckTest.php @@ -55,7 +55,6 @@ public function testCrossCheck($fixture, $type) $this->assertEquals($container2->getAliases(), $container1->getAliases(), 'loading a dump from a previously loaded container returns the same container'); $this->assertEquals($container2->getDefinitions(), $container1->getDefinitions(), 'loading a dump from a previously loaded container returns the same container'); $this->assertEquals($container2->getParameterBag()->all(), $container1->getParameterBag()->all(), '->getParameterBag() returns the same value for both containers'); - $this->assertEquals(serialize($container2), serialize($container1), 'loading a dump from a previously loaded container returns the same container'); $services1 = array(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionDecoratorTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionDecoratorTest.php deleted file mode 100644 index 6be4ad17fd840..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionDecoratorTest.php +++ /dev/null @@ -1,155 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\DefinitionDecorator; - -class DefinitionDecoratorTest extends TestCase -{ - public function testConstructor() - { - $def = new DefinitionDecorator('foo'); - - $this->assertEquals('foo', $def->getParent()); - $this->assertEquals(array(), $def->getChanges()); - } - - /** - * @dataProvider getPropertyTests - */ - public function testSetProperty($property, $changeKey) - { - $def = new DefinitionDecorator('foo'); - - $getter = 'get'.ucfirst($property); - $setter = 'set'.ucfirst($property); - - $this->assertNull($def->$getter()); - $this->assertSame($def, $def->$setter('foo')); - $this->assertEquals('foo', $def->$getter()); - $this->assertEquals(array($changeKey => true), $def->getChanges()); - } - - public function getPropertyTests() - { - return array( - array('class', 'class'), - array('factory', 'factory'), - array('configurator', 'configurator'), - array('file', 'file'), - ); - } - - /** - * @dataProvider provideLegacyPropertyTests - * @group legacy - */ - public function testLegacySetProperty($property, $changeKey) - { - $def = new DefinitionDecorator('foo'); - - $getter = 'get'.ucfirst($property); - $setter = 'set'.ucfirst($property); - - $this->assertNull($def->$getter()); - $this->assertSame($def, $def->$setter('foo')); - $this->assertEquals('foo', $def->$getter()); - $this->assertEquals(array($changeKey => true), $def->getChanges()); - } - - public function provideLegacyPropertyTests() - { - return array( - array('factoryClass', 'factory_class'), - array('factoryMethod', 'factory_method'), - array('factoryService', 'factory_service'), - ); - } - - public function testSetPublic() - { - $def = new DefinitionDecorator('foo'); - - $this->assertTrue($def->isPublic()); - $this->assertSame($def, $def->setPublic(false)); - $this->assertFalse($def->isPublic()); - $this->assertEquals(array('public' => true), $def->getChanges()); - } - - public function testSetLazy() - { - $def = new DefinitionDecorator('foo'); - - $this->assertFalse($def->isLazy()); - $this->assertSame($def, $def->setLazy(false)); - $this->assertFalse($def->isLazy()); - $this->assertEquals(array('lazy' => true), $def->getChanges()); - } - - public function testSetAutowired() - { - $def = new DefinitionDecorator('foo'); - - $this->assertFalse($def->isAutowired()); - $this->assertSame($def, $def->setAutowired(false)); - $this->assertFalse($def->isAutowired()); - $this->assertEquals(array('autowire' => true), $def->getChanges()); - } - - public function testSetArgument() - { - $def = new DefinitionDecorator('foo'); - - $this->assertEquals(array(), $def->getArguments()); - $this->assertSame($def, $def->replaceArgument(0, 'foo')); - $this->assertEquals(array('index_0' => 'foo'), $def->getArguments()); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testReplaceArgumentShouldRequireIntegerIndex() - { - $def = new DefinitionDecorator('foo'); - - $def->replaceArgument('0', 'foo'); - } - - public function testReplaceArgument() - { - $def = new DefinitionDecorator('foo'); - - $def->setArguments(array(0 => 'foo', 1 => 'bar')); - $this->assertEquals('foo', $def->getArgument(0)); - $this->assertEquals('bar', $def->getArgument(1)); - - $this->assertSame($def, $def->replaceArgument(1, 'baz')); - $this->assertEquals('foo', $def->getArgument(0)); - $this->assertEquals('baz', $def->getArgument(1)); - - $this->assertEquals(array(0 => 'foo', 1 => 'bar', 'index_1' => 'baz'), $def->getArguments()); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testGetArgumentShouldCheckBounds() - { - $def = new DefinitionDecorator('foo'); - - $def->setArguments(array(0 => 'foo')); - $def->replaceArgument(0, 'foo'); - - $def->getArgument(1); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php index 5dd0020208704..283bb5cc75127 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/DefinitionTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\ContainerInterface; class DefinitionTest extends TestCase { @@ -21,6 +20,7 @@ public function testConstructor() { $def = new Definition('stdClass'); $this->assertEquals('stdClass', $def->getClass(), '__construct() takes the class name as its first argument'); + $this->assertSame(array('class' => true), $def->getChanges()); $def = new Definition('stdClass', array('foo')); $this->assertEquals(array('foo'), $def->getArguments(), '__construct() takes an optional array of arguments as its second argument'); @@ -28,13 +28,14 @@ public function testConstructor() public function testSetGetFactory() { - $def = new Definition('stdClass'); + $def = new Definition(); $this->assertSame($def, $def->setFactory('foo'), '->setFactory() implements a fluent interface'); $this->assertEquals('foo', $def->getFactory(), '->getFactory() returns the factory'); $def->setFactory('Foo::bar'); $this->assertEquals(array('Foo', 'bar'), $def->getFactory(), '->setFactory() converts string static method call to the array'); + $this->assertSame(array('factory' => true), $def->getChanges()); } public function testSetGetClass() @@ -125,29 +126,6 @@ public function testSetIsShared() $this->assertFalse($def->isShared(), '->isShared() returns false if the instance must not be shared'); } - /** - * @group legacy - */ - public function testPrototypeScopedDefinitionAreNotShared() - { - $def = new Definition('stdClass'); - $def->setScope(ContainerInterface::SCOPE_PROTOTYPE); - - $this->assertFalse($def->isShared()); - $this->assertEquals(ContainerInterface::SCOPE_PROTOTYPE, $def->getScope()); - } - - /** - * @group legacy - */ - public function testSetGetScope() - { - $def = new Definition('stdClass'); - $this->assertEquals('container', $def->getScope()); - $this->assertSame($def, $def->setScope('foo')); - $this->assertEquals('foo', $def->getScope()); - } - public function testSetIsPublic() { $def = new Definition('stdClass'); @@ -164,17 +142,6 @@ public function testSetIsSynthetic() $this->assertTrue($def->isSynthetic(), '->isSynthetic() returns true if the service is synthetic.'); } - /** - * @group legacy - */ - public function testLegacySetIsSynchronized() - { - $def = new Definition('stdClass'); - $this->assertFalse($def->isSynchronized(), '->isSynchronized() returns false by default'); - $this->assertSame($def, $def->setSynchronized(true), '->setSynchronized() implements a fluent interface'); - $this->assertTrue($def->isSynchronized(), '->isSynchronized() returns true if the service is synchronized.'); - } - public function testSetIsLazy() { $def = new Definition('stdClass'); @@ -342,20 +309,66 @@ public function testAutowired() { $def = new Definition('stdClass'); $this->assertFalse($def->isAutowired()); + $def->setAutowired(true); $this->assertTrue($def->isAutowired()); + + $def->setAutowired(false); + $this->assertFalse($def->isAutowired()); } - public function testTypes() + public function testChangesNoChanges() { - $def = new Definition('stdClass'); + $def = new Definition(); + + $this->assertSame(array(), $def->getChanges()); + } - $this->assertEquals(array(), $def->getAutowiringTypes()); - $this->assertSame($def, $def->setAutowiringTypes(array('Foo'))); - $this->assertEquals(array('Foo'), $def->getAutowiringTypes()); - $this->assertSame($def, $def->addAutowiringType('Bar')); - $this->assertTrue($def->hasAutowiringType('Bar')); - $this->assertSame($def, $def->removeAutowiringType('Foo')); - $this->assertEquals(array('Bar'), $def->getAutowiringTypes()); + public function testGetChangesWithChanges() + { + $def = new Definition('stdClass', array('fooarg')); + + $def->setAbstract(true); + $def->setAutowired(true); + $def->setConfigurator('configuration_func'); + $def->setDecoratedService(null); + $def->setDeprecated(true); + $def->setFactory('factory_func'); + $def->setFile('foo.php'); + $def->setLazy(true); + $def->setPublic(true); + $def->setShared(true); + $def->setSynthetic(true); + // changes aren't tracked for these, class or arguments + $def->setInstanceofConditionals(array()); + $def->addTag('foo_tag'); + $def->addMethodCall('methodCall'); + $def->setProperty('fooprop', true); + $def->setAutoconfigured(true); + + $this->assertSame(array( + 'class' => true, + 'autowired' => true, + 'configurator' => true, + 'decorated_service' => true, + 'deprecated' => true, + 'factory' => true, + 'file' => true, + 'lazy' => true, + 'public' => true, + 'shared' => true, + 'autoconfigured' => true, + ), $def->getChanges()); + + $def->setChanges(array()); + $this->assertSame(array(), $def->getChanges()); + } + + public function testShouldAutoconfigure() + { + $def = new Definition('stdClass'); + $this->assertFalse($def->isAutoconfigured()); + $def->setAutoconfigured(true); + $this->assertTrue($def->isAutoconfigured()); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php index 991d2cd72218a..ffdd0730c7781 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/GraphvizDumperTest.php @@ -24,16 +24,6 @@ public static function setUpBeforeClass() self::$fixturesPath = __DIR__.'/../Fixtures/'; } - /** - * @group legacy - */ - public function testLegacyDump() - { - $container = include self::$fixturesPath.'/containers/legacy-container9.php'; - $dumper = new GraphvizDumper($container); - $this->assertEquals(str_replace('%path%', __DIR__, file_get_contents(self::$fixturesPath.'/graphviz/legacy-services9.dot')), $dumper->dump(), '->dump() dumps services'); - } - public function testDump() { $dumper = new GraphvizDumper($container = new ContainerBuilder()); @@ -81,14 +71,4 @@ public function testDumpWithUnresolvedParameter() $this->assertEquals(str_replace('%path%', __DIR__, file_get_contents(self::$fixturesPath.'/graphviz/services17.dot')), $dumper->dump(), '->dump() dumps services'); } - - /** - * @group legacy - */ - public function testDumpWithScopes() - { - $container = include self::$fixturesPath.'/containers/legacy-container18.php'; - $dumper = new GraphvizDumper($container); - $this->assertEquals(str_replace('%path%', __DIR__, file_get_contents(self::$fixturesPath.'/graphviz/services18.dot')), $dumper->dump(), '->dump() dumps services'); - } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 6a6fb015bfdc0..0f7c1d52ac782 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -13,11 +13,23 @@ use DummyProxyDumper; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator; +use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber; use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\ExpressionLanguage\Expression; @@ -34,7 +46,9 @@ public static function setUpBeforeClass() public function testDump() { - $dumper = new PhpDumper(new ContainerBuilder()); + $container = new ContainerBuilder(); + $container->compile(); + $dumper = new PhpDumper($container); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services1.php', $dumper->dump(), '->dump() dumps an empty container as an empty PHP class'); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services1-1.php', $dumper->dump(array('class' => 'Container', 'base_class' => 'AbstractContainer', 'namespace' => 'Symfony\Component\DependencyInjection\Dump')), '->dump() takes a class and a base_class options'); @@ -93,7 +107,9 @@ public function testDumpRelativeDir() */ public function testExportParameters($parameters) { - $dumper = new PhpDumper(new ContainerBuilder(new ParameterBag($parameters))); + $container = new ContainerBuilder(new ParameterBag($parameters)); + $container->compile(); + $dumper = new PhpDumper($container); $dumper->dump(); } @@ -110,25 +126,32 @@ public function provideInvalidParameters() public function testAddParameters() { $container = include self::$fixturesPath.'/containers/container8.php'; + $container->compile(); $dumper = new PhpDumper($container); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services8.php', $dumper->dump(), '->dump() dumps parameters'); } - public function testAddService() + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException + * @expectedExceptionMessage Cannot dump an uncompiled container. + */ + public function testAddServiceWithoutCompilation() { - // without compilation $container = include self::$fixturesPath.'/containers/container9.php'; - $dumper = new PhpDumper($container); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/services9.php', str_replace(str_replace('\\', '\\\\', self::$fixturesPath.DIRECTORY_SEPARATOR.'includes'.DIRECTORY_SEPARATOR), '%path%', $dumper->dump()), '->dump() dumps services'); + new PhpDumper($container); + } - // with compilation + public function testAddService() + { $container = include self::$fixturesPath.'/containers/container9.php'; $container->compile(); $dumper = new PhpDumper($container); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services9_compiled.php', str_replace(str_replace('\\', '\\\\', self::$fixturesPath.DIRECTORY_SEPARATOR.'includes'.DIRECTORY_SEPARATOR), '%path%', $dumper->dump()), '->dump() dumps services'); - $dumper = new PhpDumper($container = new ContainerBuilder()); + $container = new ContainerBuilder(); $container->register('foo', 'FooClass')->addArgument(new \stdClass()); + $container->compile(); + $dumper = new PhpDumper($container); try { $dumper->dump(); $this->fail('->dump() throws a RuntimeException if the container to be dumped has reference to objects or resources'); @@ -138,34 +161,70 @@ public function testAddService() } } - /** - * @group legacy - */ - public function testLegacySynchronizedServices() + public function testDumpAsFiles() { - $container = include self::$fixturesPath.'/containers/container20.php'; + $container = include self::$fixturesPath.'/containers/container9.php'; + $container->compile(); $dumper = new PhpDumper($container); - $this->assertStringEqualsFile(self::$fixturesPath.'/php/services20.php', str_replace(str_replace('\\', '\\\\', self::$fixturesPath.DIRECTORY_SEPARATOR.'includes'.DIRECTORY_SEPARATOR), '%path%', $dumper->dump()), '->dump() dumps services'); + $dump = print_r($dumper->dump(array('as_files' => true, 'file' => __DIR__)), true); + if ('\\' === DIRECTORY_SEPARATOR) { + $dump = str_replace('\\\\Fixtures\\\\includes\\\\foo.php', '/Fixtures/includes/foo.php', $dump); + } + $this->assertStringMatchesFormatFile(self::$fixturesPath.'/php/services9_as_files.txt', $dump); } public function testServicesWithAnonymousFactories() { $container = include self::$fixturesPath.'/containers/container19.php'; + $container->compile(); $dumper = new PhpDumper($container); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services19.php', $dumper->dump(), '->dump() dumps services with anonymous factories'); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Service id "bar$" cannot be converted to a valid PHP method name. - */ - public function testAddServiceInvalidServiceId() + public function testAddServiceIdWithUnsupportedCharacters() { + $class = 'Symfony_DI_PhpDumper_Test_Unsupported_Characters'; $container = new ContainerBuilder(); $container->register('bar$', 'FooClass'); + $container->register('bar$!', 'FooClass'); + $container->compile(); $dumper = new PhpDumper($container); - $dumper->dump(); + eval('?>'.$dumper->dump(array('class' => $class))); + + $this->assertTrue(method_exists($class, 'getBarService')); + $this->assertTrue(method_exists($class, 'getBar2Service')); + } + + public function testConflictingServiceIds() + { + $class = 'Symfony_DI_PhpDumper_Test_Conflicting_Service_Ids'; + $container = new ContainerBuilder(); + $container->register('foo_bar', 'FooClass'); + $container->register('foobar', 'FooClass'); + $container->compile(); + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump(array('class' => $class))); + + $this->assertTrue(method_exists($class, 'getFooBarService')); + $this->assertTrue(method_exists($class, 'getFoobar2Service')); + } + + public function testConflictingMethodsWithParent() + { + $class = 'Symfony_DI_PhpDumper_Test_Conflicting_Method_With_Parent'; + $container = new ContainerBuilder(); + $container->register('bar', 'FooClass'); + $container->register('foo_bar', 'FooClass'); + $container->compile(); + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump(array( + 'class' => $class, + 'base_class' => 'Symfony\Component\DependencyInjection\Tests\Fixtures\containers\CustomContainer', + ))); + + $this->assertTrue(method_exists($class, 'getBar2Service')); + $this->assertTrue(method_exists($class, 'getFoobar2Service')); } /** @@ -179,6 +238,7 @@ public function testInvalidFactories($factory) $def = new Definition('stdClass'); $def->setFactory($factory); $container->setDefinition('bar', $def); + $container->compile(); $dumper = new PhpDumper($container); $dumper->dump(); } @@ -196,13 +256,13 @@ public function provideInvalidFactories() public function testAliases() { $container = include self::$fixturesPath.'/containers/container9.php'; + $container->setParameter('foo_bar', 'foo_bar'); $container->compile(); $dumper = new PhpDumper($container); eval('?>'.$dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Aliases'))); $container = new \Symfony_DI_PhpDumper_Test_Aliases(); - $container->set('foo', $foo = new \stdClass()); - $this->assertSame($foo, $container->get('foo')); + $foo = $container->get('foo'); $this->assertSame($foo, $container->get('alias_for_foo')); $this->assertSame($foo, $container->get('alias_for_alias')); } @@ -219,28 +279,17 @@ public function testFrozenContainerWithoutAliases() $this->assertFalse($container->has('foo')); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage You cannot set the pre-defined service "bar". + */ public function testOverrideServiceWhenUsingADumpedContainer() { - require_once self::$fixturesPath.'/php/services9.php'; - require_once self::$fixturesPath.'/includes/foo.php'; - - $container = new \ProjectServiceContainer(); - $container->set('bar', $bar = new \stdClass()); - $container->setParameter('foo_bar', 'foo_bar'); - - $this->assertSame($bar, $container->get('bar'), '->set() overrides an already defined service'); - } - - public function testOverrideServiceWhenUsingADumpedContainerAndServiceIsUsedFromAnotherOne() - { - require_once self::$fixturesPath.'/php/services9.php'; + require_once self::$fixturesPath.'/php/services9_compiled.php'; require_once self::$fixturesPath.'/includes/foo.php'; - require_once self::$fixturesPath.'/includes/classes.php'; $container = new \ProjectServiceContainer(); $container->set('bar', $bar = new \stdClass()); - - $this->assertSame($bar, $container->get('foo')->bar, '->set() overrides an already defined service'); } /** @@ -261,11 +310,36 @@ public function testCircularReference() public function testDumpAutowireData() { $container = include self::$fixturesPath.'/containers/container24.php'; + $container->compile(); $dumper = new PhpDumper($container); $this->assertStringEqualsFile(self::$fixturesPath.'/php/services24.php', $dumper->dump()); } + public function testEnvParameter() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services26.yml'); + $container->compile(); + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services26.php', $dumper->dump(), '->dump() dumps inline definitions which reference service_container'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\EnvParameterException + * @expectedExceptionMessage Environment variables "FOO" are never used. Please, check your container's configuration. + */ + public function testUnusedEnvParameter() + { + $container = new ContainerBuilder(); + $container->getParameter('env(FOO)'); + $container->compile(); + $dumper = new PhpDumper($container); + $dumper->dump(); + } + public function testInlinedDefinitionReferencingServiceContainer() { $container = new ContainerBuilder(); @@ -348,10 +422,200 @@ public function testCircularReferenceAllowanceForInlinedDefinitionsForLazyServic $this->addToAssertionCount(1); } + public function testLazyArgumentProvideGenerator() + { + require_once self::$fixturesPath.'/includes/classes.php'; + + $container = new ContainerBuilder(); + $container->register('lazy_referenced', 'stdClass'); + $container + ->register('lazy_context', 'LazyContext') + ->setArguments(array( + new IteratorArgument(array('k1' => new Reference('lazy_referenced'), 'k2' => new Reference('service_container'))), + new IteratorArgument(array()), + )) + ; + $container->compile(); + + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Lazy_Argument_Provide_Generator'))); + + $container = new \Symfony_DI_PhpDumper_Test_Lazy_Argument_Provide_Generator(); + $lazyContext = $container->get('lazy_context'); + + $this->assertInstanceOf(RewindableGenerator::class, $lazyContext->lazyValues); + $this->assertInstanceOf(RewindableGenerator::class, $lazyContext->lazyEmptyValues); + $this->assertCount(2, $lazyContext->lazyValues); + $this->assertCount(0, $lazyContext->lazyEmptyValues); + + $i = -1; + foreach ($lazyContext->lazyValues as $k => $v) { + switch (++$i) { + case 0: + $this->assertEquals('k1', $k); + $this->assertInstanceOf('stdCLass', $v); + break; + case 1: + $this->assertEquals('k2', $k); + $this->assertInstanceOf('Symfony_DI_PhpDumper_Test_Lazy_Argument_Provide_Generator', $v); + break; + } + } + + $this->assertEmpty(iterator_to_array($lazyContext->lazyEmptyValues)); + } + + public function testNormalizedId() + { + $container = include self::$fixturesPath.'/containers/container33.php'; + $container->compile(); + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services33.php', $dumper->dump()); + } + + public function testDumpContainerBuilderWithFrozenConstructorIncludingPrivateServices() + { + $container = new ContainerBuilder(); + $container->register('foo_service', 'stdClass')->setArguments(array(new Reference('baz_service'))); + $container->register('bar_service', 'stdClass')->setArguments(array(new Reference('baz_service'))); + $container->register('baz_service', 'stdClass')->setPublic(false); + $container->compile(); + + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_private_frozen.php', $dumper->dump()); + } + + public function testServiceLocator() + { + $container = new ContainerBuilder(); + $container->register('foo_service', ServiceLocator::class) + ->addArgument(array( + 'bar' => new ServiceClosureArgument(new Reference('bar_service')), + 'baz' => new ServiceClosureArgument(new TypedReference('baz_service', 'stdClass')), + 'nil' => $nil = new ServiceClosureArgument(new Reference('nil')), + )) + ; + + // no method calls + $container->register('translator.loader_1', 'stdClass'); + $container->register('translator.loader_1_locator', ServiceLocator::class) + ->setPublic(false) + ->addArgument(array( + 'translator.loader_1' => new ServiceClosureArgument(new Reference('translator.loader_1')), + )); + $container->register('translator_1', StubbedTranslator::class) + ->addArgument(new Reference('translator.loader_1_locator')); + + // one method calls + $container->register('translator.loader_2', 'stdClass'); + $container->register('translator.loader_2_locator', ServiceLocator::class) + ->setPublic(false) + ->addArgument(array( + 'translator.loader_2' => new ServiceClosureArgument(new Reference('translator.loader_2')), + )); + $container->register('translator_2', StubbedTranslator::class) + ->addArgument(new Reference('translator.loader_2_locator')) + ->addMethodCall('addResource', array('db', new Reference('translator.loader_2'), 'nl')); + + // two method calls + $container->register('translator.loader_3', 'stdClass'); + $container->register('translator.loader_3_locator', ServiceLocator::class) + ->setPublic(false) + ->addArgument(array( + 'translator.loader_3' => new ServiceClosureArgument(new Reference('translator.loader_3')), + )); + $container->register('translator_3', StubbedTranslator::class) + ->addArgument(new Reference('translator.loader_3_locator')) + ->addMethodCall('addResource', array('db', new Reference('translator.loader_3'), 'nl')) + ->addMethodCall('addResource', array('db', new Reference('translator.loader_3'), 'en')); + + $nil->setValues(array(null)); + $container->register('bar_service', 'stdClass')->setArguments(array(new Reference('baz_service'))); + $container->register('baz_service', 'stdClass')->setPublic(false); + $container->compile(); + + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_locator.php', $dumper->dump()); + } + + public function testServiceSubscriber() + { + $container = new ContainerBuilder(); + $container->register('foo_service', TestServiceSubscriber::class) + ->setAutowired(true) + ->addArgument(new Reference(ContainerInterface::class)) + ->addTag('container.service_subscriber', array( + 'key' => 'bar', + 'id' => TestServiceSubscriber::class, + )) + ; + $container->register(TestServiceSubscriber::class, TestServiceSubscriber::class); + + $container->register(CustomDefinition::class, CustomDefinition::class) + ->setPublic(false); + $container->compile(); + + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_subscriber.php', $dumper->dump()); + } + + public function testPrivateWithIgnoreOnInvalidReference() + { + require_once self::$fixturesPath.'/includes/classes.php'; + + $container = new ContainerBuilder(); + $container->register('not_invalid', 'BazClass') + ->setPublic(false); + $container->register('bar', 'BarClass') + ->addMethodCall('setBaz', array(new Reference('not_invalid', SymfonyContainerInterface::IGNORE_ON_INVALID_REFERENCE))); + $container->compile(); + + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Private_With_Ignore_On_Invalid_Reference'))); + + $container = new \Symfony_DI_PhpDumper_Test_Private_With_Ignore_On_Invalid_Reference(); + $this->assertInstanceOf('BazClass', $container->get('bar')->getBaz()); + } + + public function testArrayParameters() + { + $container = new ContainerBuilder(); + $container->setParameter('array_1', array(123)); + $container->setParameter('array_2', array(__DIR__)); + $container->register('bar', 'BarClass') + ->addMethodCall('setBaz', array('%array_1%', '%array_2%', '%%array_1%%')); + $container->compile(); + + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_array_params.php', str_replace('\\\\Dumper', '/Dumper', $dumper->dump(array('file' => self::$fixturesPath.'/php/services_array_params.php')))); + } + + public function testExpressionReferencingPrivateService() + { + $container = new ContainerBuilder(); + $container->register('private_bar', 'stdClass') + ->setPublic(false); + $container->register('private_foo', 'stdClass') + ->setPublic(false); + $container->register('public_foo', 'stdClass') + ->addArgument(new Expression('service("private_foo")')); + + $container->compile(); + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_private_in_expression.php', $dumper->dump()); + } + public function testDumpHandlesLiteralClassWithRootNamespace() { $container = new ContainerBuilder(); $container->register('foo', '\\stdClass'); + $container->compile(); $dumper = new PhpDumper($container); eval('?>'.$dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Literal_Class_With_Root_Namespace'))); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php index c8d429db44e2b..3ac6628b737e6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php @@ -45,27 +45,6 @@ public function testAddParameters() $this->assertXmlStringEqualsXmlFile(self::$fixturesPath.'/xml/services8.xml', $dumper->dump(), '->dump() dumps parameters'); } - /** - * @group legacy - */ - public function testLegacyAddService() - { - $container = include self::$fixturesPath.'/containers/legacy-container9.php'; - $dumper = new XmlDumper($container); - - $this->assertEquals(str_replace('%path%', self::$fixturesPath.DIRECTORY_SEPARATOR.'includes'.DIRECTORY_SEPARATOR, file_get_contents(self::$fixturesPath.'/xml/legacy-services9.xml')), $dumper->dump(), '->dump() dumps services'); - - $dumper = new XmlDumper($container = new ContainerBuilder()); - $container->register('foo', 'FooClass')->addArgument(new \stdClass()); - try { - $dumper->dump(); - $this->fail('->dump() throws a RuntimeException if the container to be dumped has reference to objects or resources'); - } catch (\Exception $e) { - $this->assertInstanceOf('\RuntimeException', $e, '->dump() throws a RuntimeException if the container to be dumped has reference to objects or resources'); - $this->assertEquals('Unable to dump a service container if a parameter is an object or a resource.', $e->getMessage(), '->dump() throws a RuntimeException if the container to be dumped has reference to objects or resources'); - } - } - public function testAddService() { $container = include self::$fixturesPath.'/containers/container9.php'; @@ -91,6 +70,7 @@ public function testDumpAnonymousServices() $this->assertEquals(' + @@ -100,6 +80,8 @@ public function testDumpAnonymousServices() + + ', $dumper->dump()); @@ -112,10 +94,13 @@ public function testDumpEntities() $this->assertEquals(" + foo<>&bar + + ", $dumper->dump()); @@ -138,14 +123,20 @@ public function provideDecoratedServicesData() array(" + + + ", include $fixturesPath.'/containers/container15.php'), array(" + + + ", include $fixturesPath.'/containers/container16.php'), diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php index e61eca1d0151d..968385633b549 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php @@ -14,9 +14,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Dumper\YamlDumper; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\Yaml\Yaml; +use Symfony\Component\Yaml\Parser; class YamlDumperTest extends TestCase { @@ -41,27 +43,6 @@ public function testAddParameters() $this->assertEqualYamlStructure(file_get_contents(self::$fixturesPath.'/yaml/services8.yml'), $dumper->dump(), '->dump() dumps parameters'); } - /** - * @group legacy - */ - public function testLegacyAddService() - { - $container = include self::$fixturesPath.'/containers/legacy-container9.php'; - $dumper = new YamlDumper($container); - - $this->assertEquals(str_replace('%path%', self::$fixturesPath.DIRECTORY_SEPARATOR.'includes'.DIRECTORY_SEPARATOR, file_get_contents(self::$fixturesPath.'/yaml/legacy-services9.yml')), $dumper->dump(), '->dump() dumps services'); - - $dumper = new YamlDumper($container = new ContainerBuilder()); - $container->register('foo', 'FooClass')->addArgument(new \stdClass()); - try { - $dumper->dump(); - $this->fail('->dump() throws a RuntimeException if the container to be dumped has reference to objects or resources'); - } catch (\Exception $e) { - $this->assertInstanceOf('\RuntimeException', $e, '->dump() throws a RuntimeException if the container to be dumped has reference to objects or resources'); - $this->assertEquals('Unable to dump a service container if a parameter is an object or a resource.', $e->getMessage(), '->dump() throws a RuntimeException if the container to be dumped has reference to objects or resources'); - } - } - public function testAddService() { $container = include self::$fixturesPath.'/containers/container9.php'; @@ -96,8 +77,23 @@ public function testDumpLoad() $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_dump_load.yml', $dumper->dump()); } - private function assertEqualYamlStructure($yaml, $expected, $message = '') + public function testInlineServices() { - $this->assertEquals(Yaml::parse($expected), Yaml::parse($yaml), $message); + $container = new ContainerBuilder(); + $container->register('foo', 'Class1') + ->addArgument((new Definition('Class2')) + ->addArgument(new Definition('Class2')) + ) + ; + + $dumper = new YamlDumper($container); + $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_inline.yml', $dumper->dump()); + } + + private function assertEqualYamlStructure($expected, $yaml, $message = '') + { + $parser = new Parser(); + + $this->assertEquals($parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS), $parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS), $message); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php new file mode 100644 index 0000000000000..d243866d36ef9 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class Bar implements BarInterface +{ + public function __construct($quz = null, \NonExistent $nonExistent = null, BarInterface $decorated = null, array $foo = array()) + { + } + + public static function create(\NonExistent $nonExistent = null, $factory = null) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/BarInterface.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/BarInterface.php new file mode 100644 index 0000000000000..dc81f33f5d950 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/BarInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +interface BarInterface +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CaseSensitiveClass.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CaseSensitiveClass.php new file mode 100644 index 0000000000000..87b258429e6a3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CaseSensitiveClass.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class CaseSensitiveClass +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FactoryDummy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FactoryDummy.php new file mode 100644 index 0000000000000..b7ceac7d8a4eb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FactoryDummy.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class FactoryDummy extends FactoryParent +{ + public static function createFactory(): FactoryDummy + { + } + + public function create(): \stdClass + { + } + + public function createBuiltin(): int + { + } + + public static function createSelf(): self + { + } + + public static function createParent(): parent + { + } +} + +class FactoryParent +{ +} + +function factoryFunction(): FactoryDummy +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php new file mode 100644 index 0000000000000..09d907dfae769 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php @@ -0,0 +1,21 @@ + + */ +class NamedArgumentsDummy +{ + public function __construct(CaseSensitiveClass $c, $apiKey, $hostName) + { + } + + public function setApiKey($apiKey) + { + } + + public function setSensitiveClass(CaseSensitiveClass $c) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php new file mode 100644 index 0000000000000..1e4f283c8f16e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php @@ -0,0 +1,7 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +use Psr\Container\ContainerInterface; + +/** + * @author Iltar van der Berg + */ +class StubbedTranslator +{ + public function __construct(ContainerInterface $container) + { + + } + + public function addResource($format, $resource, $locale, $domain = null) + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriber.php new file mode 100644 index 0000000000000..875abe9e02e12 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriber.php @@ -0,0 +1,22 @@ + CustomDefinition::class, + 'baz' => '?'.CustomDefinition::class, + ); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/CustomContainer.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/CustomContainer.php new file mode 100644 index 0000000000000..2251435324b38 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/CustomContainer.php @@ -0,0 +1,17 @@ +register('request', 'Request') - ->setSynchronized(true) -; -$container - ->register('depends_on_request', 'stdClass') - ->addMethodCall('setRequest', array(new Reference('request', ContainerInterface::NULL_ON_INVALID_REFERENCE, false))) -; - -return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container24.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container24.php index df45b0d5ac644..cba10b526b2a8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container24.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container24.php @@ -1,15 +1,12 @@ register('foo', 'Foo') ->setAutowired(true) - ->addAutowiringType('A') - ->addAutowiringType('B') ; return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container33.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container33.php new file mode 100644 index 0000000000000..ec68f929d449e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container33.php @@ -0,0 +1,12 @@ +register(\Foo\Foo::class); +$container->register(\Bar\Foo::class); + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php index 89475f1fd077b..98036586b61b4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php @@ -1,7 +1,9 @@ register('foo_bar', '%foo_class%') + ->addArgument(new Reference('deprecated_service')) ->setShared(false) ; $container->getParameterBag()->clear(); @@ -78,6 +81,15 @@ ->register('configured_service', 'stdClass') ->setConfigurator(array(new Reference('configurator_service'), 'configureStdClass')) ; +$container + ->register('configurator_service_simple', 'ConfClass') + ->addArgument('bar') + ->setPublic(false) +; +$container + ->register('configured_service_simple', 'stdClass') + ->setConfigurator(array(new Reference('configurator_service_simple'), 'configureStdClass')) +; $container ->register('decorated', 'stdClass') ; @@ -111,5 +123,29 @@ ->register('service_from_static_method', 'Bar\FooClass') ->setFactory(array('Bar\FooClass', 'getInstance')) ; +$container + ->register('factory_simple', 'SimpleFactoryClass') + ->addArgument('foo') + ->setDeprecated(true) + ->setPublic(false) +; +$container + ->register('factory_service_simple', 'Bar') + ->setFactory(array(new Reference('factory_simple'), 'getInstance')) +; +$container + ->register('lazy_context', 'LazyContext') + ->setArguments(array(new IteratorArgument(array('k1' => new Reference('foo.baz'), 'k2' => new Reference('service_container'))), new IteratorArgument(array()))) +; +$container + ->register('lazy_context_ignore_invalid_ref', 'LazyContext') + ->setArguments(array(new IteratorArgument(array(new Reference('foo.baz'), new Reference('invalid', ContainerInterface::IGNORE_ON_INVALID_REFERENCE))), new IteratorArgument(array()))) +; +$container + ->register('BAR', 'stdClass') + ->setProperty('bar', new Reference('bar')) +; +$container->register('bar2', 'stdClass'); +$container->register('BAR2', 'stdClass'); return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/legacy-container18.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/legacy-container18.php deleted file mode 100644 index 0248ed462722e..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/legacy-container18.php +++ /dev/null @@ -1,14 +0,0 @@ -addScope(new Scope('request')); -$container-> - register('foo', 'FooClass')-> - setScope('request') -; -$container->compile(); - -return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/legacy-container9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/legacy-container9.php deleted file mode 100644 index 9ac477b77ac73..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/legacy-container9.php +++ /dev/null @@ -1,43 +0,0 @@ - - register('foo', 'Bar\FooClass')-> - addTag('foo', array('foo' => 'foo'))-> - addTag('foo', array('bar' => 'bar'))-> - setFactoryClass('Bar\\FooClass')-> - setFactoryMethod('getInstance')-> - setArguments(array('foo', new Reference('foo.baz'), array('%foo%' => 'foo is %foo%', 'foobar' => '%foo%'), true, new Reference('service_container')))-> - setProperties(array('foo' => 'bar', 'moo' => new Reference('foo.baz'), 'qux' => array('%foo%' => 'foo is %foo%', 'foobar' => '%foo%')))-> - addMethodCall('setBar', array(new Reference('bar')))-> - addMethodCall('initialize')-> - setConfigurator('sc_configure') -; -$container-> - register('foo.baz', '%baz_class%')-> - setFactoryClass('%baz_class%')-> - setFactoryMethod('getInstance')-> - setConfigurator(array('%baz_class%', 'configureStatic1')) -; -$container-> - register('factory_service', 'Bar')-> - setFactoryService('foo.baz')-> - setFactoryMethod('getInstance') -; -$container - ->register('foo_bar', '%foo_class%') - ->setScope('prototype') -; -$container->getParameterBag()->clear(); -$container->getParameterBag()->add(array( - 'foo_class' => 'Bar\FooClass', - 'baz_class' => 'BazClass', - 'foo' => 'bar', -)); - -return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/legacy-services9.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/legacy-services9.dot deleted file mode 100644 index ce52c6dcd01c4..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/legacy-services9.dot +++ /dev/null @@ -1,16 +0,0 @@ -digraph sc { - ratio="compress" - node [fontsize="11" fontname="Arial" shape="record"]; - edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; - - node_foo [label="foo\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_foo_baz [label="foo.baz\nBazClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_factory_service [label="factory_service\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_foo_bar [label="foo_bar\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="dotted"]; - node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; - node_bar [label="bar\n\n", shape=record, fillcolor="#ff9999", style="filled"]; - node_foo -> node_foo_baz [label="" style="filled"]; - node_foo -> node_service_container [label="" style="filled"]; - node_foo -> node_foo_baz [label="" style="dashed"]; - node_foo -> node_bar [label="setBar()" style="dashed"]; -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services1.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services1.dot index 1bb7c30b8c702..e74010809b991 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services1.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services1.dot @@ -3,5 +3,5 @@ digraph sc { node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; - node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; + node_service_container [label="service_container (Psr\Container\ContainerInterface, Symfony\Component\DependencyInjection\ContainerInterface)\nSymfony\\Component\\DependencyInjection\\ContainerInterface\n", shape=record, fillcolor="#eeeeee", style="filled"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10-1.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10-1.dot index 0e578b161b8f5..9fdf341062dbf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10-1.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10-1.dot @@ -3,8 +3,8 @@ digraph sc { node [fontsize="13" fontname="Verdana" shape="square"]; edge [fontsize="12" fontname="Verdana" color="white" arrowhead="closed" arrowsize="1"]; + node_service_container [label="service_container (Psr\Container\ContainerInterface, Symfony\Component\DependencyInjection\ContainerInterface)\nSymfony\\Component\\DependencyInjection\\ContainerInterface\n", shape=square, fillcolor="grey", style="filled"]; node_foo [label="foo\nFooClass\n", shape=square, fillcolor="grey", style="filled"]; - node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=square, fillcolor="green", style="empty"]; node_bar [label="bar\n\n", shape=square, fillcolor="red", style="empty"]; node_foo -> node_bar [label="" style="filled"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10.dot index f17857fe428ef..309388eac6f22 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services10.dot @@ -3,8 +3,8 @@ digraph sc { node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; + node_service_container [label="service_container (Psr\Container\ContainerInterface, Symfony\Component\DependencyInjection\ContainerInterface)\nSymfony\\Component\\DependencyInjection\\ContainerInterface\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo [label="foo\nFooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; node_bar [label="bar\n\n", shape=record, fillcolor="#ff9999", style="filled"]; node_foo -> node_bar [label="" style="filled"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services13.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services13.dot index bc7f81317e50a..fffe8f1b54743 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services13.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services13.dot @@ -3,8 +3,8 @@ digraph sc { node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; + node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerInterface\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo [label="foo\nFooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_bar [label="bar\nBarClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; node_foo -> node_bar [label="" style="filled"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services14.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services14.dot index d07dc389e0b2e..e74010809b991 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services14.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services14.dot @@ -3,5 +3,5 @@ digraph sc { node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; - node_service_container [label="service_container\nContainer14\\ProjectServiceContainer\n", shape=record, fillcolor="#9999ff", style="filled"]; + node_service_container [label="service_container (Psr\Container\ContainerInterface, Symfony\Component\DependencyInjection\ContainerInterface)\nSymfony\\Component\\DependencyInjection\\ContainerInterface\n", shape=record, fillcolor="#eeeeee", style="filled"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot index a6d04bf5a097f..e177fae2aac25 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services17.dot @@ -3,6 +3,6 @@ digraph sc { node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; + node_service_container [label="service_container (Psr\Container\ContainerInterface, Symfony\Component\DependencyInjection\ContainerInterface)\nSymfony\\Component\\DependencyInjection\\ContainerInterface\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo [label="foo\n%foo.class%\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot index f6536980aa54f..9ee105da5b6cb 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot @@ -3,6 +3,7 @@ digraph sc { node [fontsize="11" fontname="Arial" shape="record"]; edge [fontsize="9" fontname="Arial" color="grey" arrowhead="open" arrowsize="0.5"]; + node_service_container [label="service_container (Psr\Container\ContainerInterface, Symfony\Component\DependencyInjection\ContainerInterface)\nSymfony\\Component\\DependencyInjection\\ContainerInterface\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo [label="foo (alias_for_foo)\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo_baz [label="foo.baz\nBazClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_bar [label="bar\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; @@ -14,6 +15,8 @@ digraph sc { node_request [label="request\nRequest\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_configurator_service [label="configurator_service\nConfClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_configured_service [label="configured_service\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_configurator_service_simple [label="configurator_service_simple\nConfClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_configured_service_simple [label="configured_service_simple\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_decorated [label="decorated\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_decorator_service [label="decorator_service\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_decorator_service_with_name [label="decorator_service_with_name\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; @@ -22,15 +25,23 @@ digraph sc { node_factory_service [label="factory_service\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_new_factory_service [label="new_factory_service\nFooBarBaz\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_service_from_static_method [label="service_from_static_method\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; - node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"]; + node_factory_simple [label="factory_simple\nSimpleFactoryClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_factory_service_simple [label="factory_service_simple\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_lazy_context [label="lazy_context\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_lazy_context_ignore_invalid_ref [label="lazy_context_ignore_invalid_ref\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_BAR [label="BAR\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_bar2 [label="bar2\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_BAR2 [label="BAR2\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"]; node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"]; node_foobaz [label="foobaz\n\n", shape=record, fillcolor="#ff9999", style="filled"]; + node_invalid [label="invalid\n\n", shape=record, fillcolor="#ff9999", style="filled"]; node_foo -> node_foo_baz [label="" style="filled"]; node_foo -> node_service_container [label="" style="filled"]; node_foo -> node_foo_baz [label="" style="dashed"]; node_foo -> node_bar [label="setBar()" style="dashed"]; node_bar -> node_foo_baz [label="" style="filled"]; + node_foo_bar -> node_deprecated_service [label="" style="filled"]; node_method_call1 -> node_foo [label="setBar()" style="dashed"]; node_method_call1 -> node_foo2 [label="setBar()" style="dashed"]; node_method_call1 -> node_foo3 [label="setBar()" style="dashed"]; @@ -39,4 +50,9 @@ digraph sc { node_inlined -> node_baz [label="setBaz()" style="dashed"]; node_baz -> node_foo_with_inline [label="setFoo()" style="dashed"]; node_configurator_service -> node_baz [label="setFoo()" style="dashed"]; + node_lazy_context -> node_foo_baz [label="" style="filled" color="#9999ff"]; + node_lazy_context -> node_service_container [label="" style="filled" color="#9999ff"]; + node_lazy_context_ignore_invalid_ref -> node_foo_baz [label="" style="filled" color="#9999ff"]; + node_lazy_context_ignore_invalid_ref -> node_invalid [label="" style="filled" color="#9999ff"]; + node_BAR -> node_bar [label="" style="dashed"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/FooVariadic.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/FooVariadic.php new file mode 100644 index 0000000000000..12861c5611735 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/FooVariadic.php @@ -0,0 +1,16 @@ +lazyValues = $lazyValues; + $this->lazyEmptyValues = $lazyEmptyValues; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/almostvalid.ini b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/almostvalid.ini new file mode 100644 index 0000000000000..9fdef85580a6b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/almostvalid.ini @@ -0,0 +1,2 @@ +foo = ' +bar = bar diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/ini_with_wrong_ext.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/ini_with_wrong_ext.xml new file mode 100644 index 0000000000000..7f1b4d3dc2001 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/ini_with_wrong_ext.xml @@ -0,0 +1,2 @@ +[parameters] + with_wrong_ext = 'from ini' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/types.ini b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/types.ini new file mode 100644 index 0000000000000..19cc5b3b31e42 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ini/types.ini @@ -0,0 +1,27 @@ +[parameters] + true = true + true_comment = true ; comment + false = false + null = null + on = on + off = off + yes = yes + no = no + none = none + constant = PHP_VERSION + 12 = 12 + 12_string = '12' + 12_comment = 12 ; comment + 12_string_comment = '12' ; comment + 12_string_comment_again = "12" ; comment + -12 = -12 + 0 = 0 + 1 = 1 + 0b0110 = 0b0110 + 11112222333344445555 = 1111,2222,3333,4444,5555 + 0777 = 0777 + 255 = 0xFF + 100.0 = 1e2 + -120.0 = -1.2E2 + -10100.1 = -10100.1 + -10,100.1 = -10,100.1 diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/php_with_wrong_ext.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/php_with_wrong_ext.yml new file mode 100644 index 0000000000000..fdd312501df4c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/php_with_wrong_ext.yml @@ -0,0 +1,3 @@ +setParameter('with_wrong_ext', 'from php'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php index 0fede650233e5..5f74931f7a290 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1-1.php @@ -2,30 +2,60 @@ namespace Symfony\Component\DependencyInjection\Dump; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\DependencyInjection\Exception\InactiveScopeException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; /** * Container. * * This class has been auto-generated * by the Symfony Dependency Injection Component. + * + * @final since Symfony 3.3 */ class Container extends AbstractContainer { private $parameters; private $targetDirs = array(); + private $privates = array(); /** * Constructor. */ public function __construct() { - parent::__construct(); + $this->services = $this->privates = array(); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1.php index 5497a7587ab54..ca73b472a5710 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services1.php @@ -1,29 +1,59 @@ services = $this->privates = array(); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php index 923c437bb06a1..9f79c99317322 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services10.php @@ -1,8 +1,8 @@ parameters = $this->getDefaultParameters(); - $this->services = - $this->scopedServices = - $this->scopeStacks = array(); - $this->scopes = array(); - $this->scopeChildren = array(); + $this->services = $this->privates = array(); $this->methodMap = array( 'test' => 'getTestService', ); @@ -38,18 +37,27 @@ public function __construct() $this->aliases = array(); } + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + /** * {@inheritdoc} */ public function compile() { - throw new LogicException('You cannot compile a dumped frozen container.'); + throw new LogicException('You cannot compile a dumped container that was already compiled.'); } /** * {@inheritdoc} */ - public function isFrozen() + public function isCompiled() { return true; } @@ -69,10 +77,15 @@ protected function getTestService() */ public function getParameter($name) { - $name = strtolower($name); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { - throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); } return $this->parameters[$name]; @@ -85,7 +98,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); } /** @@ -102,12 +115,33 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; } + private $loadedDynamicParameters = array(); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + /** * Gets the default parameters. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php index 2119719828d67..05bd46c18dd96 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services12.php @@ -1,8 +1,8 @@ parameters = $this->getDefaultParameters(); - $this->services = - $this->scopedServices = - $this->scopeStacks = array(); - $this->scopes = array(); - $this->scopeChildren = array(); + $this->services = $this->privates = array(); $this->methodMap = array( 'test' => 'getTestService', ); @@ -42,18 +41,27 @@ public function __construct() $this->aliases = array(); } + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + /** * {@inheritdoc} */ public function compile() { - throw new LogicException('You cannot compile a dumped frozen container.'); + throw new LogicException('You cannot compile a dumped container that was already compiled.'); } /** * {@inheritdoc} */ - public function isFrozen() + public function isCompiled() { return true; } @@ -73,10 +81,15 @@ protected function getTestService() */ public function getParameter($name) { - $name = strtolower($name); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { - throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); } return $this->parameters[$name]; @@ -89,7 +102,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); } /** @@ -106,12 +119,43 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; } + private $loadedDynamicParameters = array( + 'foo' => false, + 'buz' => false, + ); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + switch ($name) { + case 'foo': $value = ('wiz'.$this->targetDirs[1]); break; + case 'buz': $value = $this->targetDirs[2]; break; + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; + } + /** * Gets the default parameters. * @@ -120,10 +164,8 @@ public function getParameterBag() protected function getDefaultParameters() { return array( - 'foo' => ('wiz'.$this->targetDirs[1]), 'bar' => __DIR__, 'baz' => (__DIR__.'/PhpDumperTest.php'), - 'buz' => $this->targetDirs[2], ); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services13.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services13.php index eaf9c4bef0edc..06a5de87fb950 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services13.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services13.php @@ -1,8 +1,8 @@ services = - $this->scopedServices = - $this->scopeStacks = array(); - $this->scopes = array(); - $this->scopeChildren = array(); + $this->services = $this->privates = array(); $this->methodMap = array( 'bar' => 'getBarService', ); @@ -36,18 +35,27 @@ public function __construct() $this->aliases = array(); } + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + /** * {@inheritdoc} */ public function compile() { - throw new LogicException('You cannot compile a dumped frozen container.'); + throw new LogicException('You cannot compile a dumped container that was already compiled.'); } /** * {@inheritdoc} */ - public function isFrozen() + public function isCompiled() { return true; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php index a3fde04b78a4b..7e0fb44cc2d60 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php @@ -1,34 +1,64 @@ services = $this->privates = array(); $this->methodMap = array( 'service_from_anonymous_factory' => 'getServiceFromAnonymousFactoryService', 'service_with_method_call_and_factory' => 'getServiceWithMethodCallAndFactoryService', ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; } /** @@ -38,7 +68,7 @@ public function __construct() */ protected function getServiceFromAnonymousFactoryService() { - return $this->services['service_from_anonymous_factory'] = call_user_func(array(new \Bar\FooClass(), 'getInstance')); + return $this->services['service_from_anonymous_factory'] = (new \Bar\FooClass())->getInstance(); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services20.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services20.php deleted file mode 100644 index ba9a8902e4d06..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services20.php +++ /dev/null @@ -1,67 +0,0 @@ -methodMap = array( - 'depends_on_request' => 'getDependsOnRequestService', - 'request' => 'getRequestService', - ); - } - - /** - * Gets the public 'depends_on_request' shared service. - * - * @return \stdClass - */ - protected function getDependsOnRequestService() - { - $this->services['depends_on_request'] = $instance = new \stdClass(); - - $instance->setRequest($this->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - - return $instance; - } - - /** - * Gets the public 'request' shared service. - * - * @return \Request - */ - protected function getRequestService() - { - return $this->services['request'] = new \Request(); - } - - /** - * Updates the 'request' service. - */ - protected function synchronizeRequestService() - { - if ($this->initialized('depends_on_request')) { - $this->get('depends_on_request')->setRequest($this->get('request', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - } - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services24.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services24.php index 3d9e57f03922a..8a313adce64d7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services24.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services24.php @@ -1,33 +1,63 @@ services = $this->privates = array(); $this->methodMap = array( 'foo' => 'getFooService', ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php new file mode 100644 index 0000000000000..a2e3937d12860 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services26.php @@ -0,0 +1,166 @@ +parameters = $this->getDefaultParameters(); + + $this->services = $this->privates = array(); + $this->methodMap = array( + 'test' => 'getTestService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the public 'test' shared service. + * + * @return object A %env(FOO)% instance + */ + protected function getTestService() + { + $class = $this->getEnv('FOO'); + + return $this->services['test'] = new $class($this->getEnv('Bar'), 'foo'.$this->getEnv('FOO').'baz'); + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) + { + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); + + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + + return $this->parameters[$name]; + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) + { + $name = strtolower($name); + + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + /** + * {@inheritdoc} + */ + public function getParameterBag() + { + if (null === $this->parameterBag) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = array( + 'bar' => false, + ); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + switch ($name) { + case 'bar': $value = $this->getEnv('FOO'); break; + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; + } + + /** + * Gets the default parameters. + * + * @return array An array of the default parameters + */ + protected function getDefaultParameters() + { + return array( + 'env(foo)' => 'foo', + ); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services31.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services31.php new file mode 100644 index 0000000000000..4d40bb35c54da --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services31.php @@ -0,0 +1,66 @@ +services = array(); + $this->methodMap = array( + 'bar' => 'getBarService', + 'foo' => 'getFooService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the 'foo' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo A Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo instance + */ + protected function getFooService() + { + return $this->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo(); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services33.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services33.php new file mode 100644 index 0000000000000..b069fb8ba06b0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services33.php @@ -0,0 +1,83 @@ +services = $this->privates = array(); + $this->methodMap = array( + 'Bar\\Foo' => 'getFooService', + 'Foo\\Foo' => 'getFoo2Service', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the public 'Bar\Foo' shared service. + * + * @return \Bar\Foo + */ + protected function getFooService() + { + return $this->services['Bar\Foo'] = new \Bar\Foo(); + } + + /** + * Gets the public 'Foo\Foo' shared service. + * + * @return \Foo\Foo + */ + protected function getFoo2Service() + { + return $this->services['Foo\Foo'] = new \Foo\Foo(); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php index c2c52fe3351c7..004c7b8008be6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php @@ -1,30 +1,132 @@ getDefaultParameters())); + $this->parameters = $this->getDefaultParameters(); + + $this->services = $this->privates = array(); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) + { + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); + + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + + return $this->parameters[$name]; + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) + { + $name = strtolower($name); + + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + /** + * {@inheritdoc} + */ + public function getParameterBag() + { + if (null === $this->parameterBag) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = array(); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); } /** @@ -35,9 +137,9 @@ public function __construct() protected function getDefaultParameters() { return array( - 'foo' => '%baz%', + 'foo' => 'bar', 'baz' => 'bar', - 'bar' => 'foo is %%foo bar', + 'bar' => 'foo is %foo bar', 'escape' => '@escapeme', 'values' => array( 0 => true, diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php deleted file mode 100644 index c0f11c662c0c3..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php +++ /dev/null @@ -1,328 +0,0 @@ -getDefaultParameters())); - $this->methodMap = array( - 'bar' => 'getBarService', - 'baz' => 'getBazService', - 'configurator_service' => 'getConfiguratorServiceService', - 'configured_service' => 'getConfiguredServiceService', - 'decorated' => 'getDecoratedService', - 'decorator_service' => 'getDecoratorServiceService', - 'decorator_service_with_name' => 'getDecoratorServiceWithNameService', - 'deprecated_service' => 'getDeprecatedServiceService', - 'factory_service' => 'getFactoryServiceService', - 'foo' => 'getFooService', - 'foo.baz' => 'getFoo_BazService', - 'foo_bar' => 'getFooBarService', - 'foo_with_inline' => 'getFooWithInlineService', - 'inlined' => 'getInlinedService', - 'method_call1' => 'getMethodCall1Service', - 'new_factory' => 'getNewFactoryService', - 'new_factory_service' => 'getNewFactoryServiceService', - 'request' => 'getRequestService', - 'service_from_static_method' => 'getServiceFromStaticMethodService', - ); - $this->aliases = array( - 'alias_for_alias' => 'foo', - 'alias_for_foo' => 'foo', - ); - } - - /** - * Gets the public 'bar' shared service. - * - * @return \Bar\FooClass - */ - protected function getBarService() - { - $a = $this->get('foo.baz'); - - $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); - - $a->configure($instance); - - return $instance; - } - - /** - * Gets the public 'baz' shared service. - * - * @return \Baz - */ - protected function getBazService() - { - $this->services['baz'] = $instance = new \Baz(); - - $instance->setFoo($this->get('foo_with_inline')); - - return $instance; - } - - /** - * Gets the public 'configured_service' shared service. - * - * @return \stdClass - */ - protected function getConfiguredServiceService() - { - $this->services['configured_service'] = $instance = new \stdClass(); - - $this->get('configurator_service')->configureStdClass($instance); - - return $instance; - } - - /** - * Gets the public 'decorated' shared service. - * - * @return \stdClass - */ - protected function getDecoratedService() - { - return $this->services['decorated'] = new \stdClass(); - } - - /** - * Gets the public 'decorator_service' shared service. - * - * @return \stdClass - */ - protected function getDecoratorServiceService() - { - return $this->services['decorator_service'] = new \stdClass(); - } - - /** - * Gets the public 'decorator_service_with_name' shared service. - * - * @return \stdClass - */ - protected function getDecoratorServiceWithNameService() - { - return $this->services['decorator_service_with_name'] = new \stdClass(); - } - - /** - * Gets the public 'deprecated_service' shared service. - * - * @return \stdClass - * - * @deprecated The "deprecated_service" service is deprecated. You should stop using it, as it will soon be removed. - */ - protected function getDeprecatedServiceService() - { - @trigger_error('The "deprecated_service" service is deprecated. You should stop using it, as it will soon be removed.', E_USER_DEPRECATED); - - return $this->services['deprecated_service'] = new \stdClass(); - } - - /** - * Gets the public 'factory_service' shared service. - * - * @return \Bar - */ - protected function getFactoryServiceService() - { - return $this->services['factory_service'] = $this->get('foo.baz')->getInstance(); - } - - /** - * Gets the public 'foo' shared service. - * - * @return \Bar\FooClass - */ - protected function getFooService() - { - $a = $this->get('foo.baz'); - - $this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array($this->getParameter('foo') => 'foo is '.$this->getParameter('foo').'', 'foobar' => $this->getParameter('foo')), true, $this); - - $instance->foo = 'bar'; - $instance->moo = $a; - $instance->qux = array($this->getParameter('foo') => 'foo is '.$this->getParameter('foo').'', 'foobar' => $this->getParameter('foo')); - $instance->setBar($this->get('bar')); - $instance->initialize(); - sc_configure($instance); - - return $instance; - } - - /** - * Gets the public 'foo.baz' shared service. - * - * @return object A %baz_class% instance - */ - protected function getFoo_BazService() - { - $this->services['foo.baz'] = $instance = call_user_func(array($this->getParameter('baz_class'), 'getInstance')); - - call_user_func(array($this->getParameter('baz_class'), 'configureStatic1'), $instance); - - return $instance; - } - - /** - * Gets the public 'foo_bar' service. - * - * @return object A %foo_class% instance - */ - protected function getFooBarService() - { - $class = $this->getParameter('foo_class'); - - return new $class(); - } - - /** - * Gets the public 'foo_with_inline' shared service. - * - * @return \Foo - */ - protected function getFooWithInlineService() - { - $this->services['foo_with_inline'] = $instance = new \Foo(); - - $instance->setBar($this->get('inlined')); - - return $instance; - } - - /** - * Gets the public 'method_call1' shared service. - * - * @return \Bar\FooClass - */ - protected function getMethodCall1Service() - { - require_once '%path%foo.php'; - - $this->services['method_call1'] = $instance = new \Bar\FooClass(); - - $instance->setBar($this->get('foo')); - $instance->setBar($this->get('foo2', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - if ($this->has('foo3')) { - $instance->setBar($this->get('foo3', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - } - if ($this->has('foobaz')) { - $instance->setBar($this->get('foobaz', ContainerInterface::NULL_ON_INVALID_REFERENCE)); - } - $instance->setBar(($this->get("foo")->foo() . (($this->hasParameter("foo")) ? ($this->getParameter("foo")) : ("default")))); - - return $instance; - } - - /** - * Gets the public 'new_factory_service' shared service. - * - * @return \FooBarBaz - */ - protected function getNewFactoryServiceService() - { - $this->services['new_factory_service'] = $instance = $this->get('new_factory')->getInstance(); - - $instance->foo = 'bar'; - - return $instance; - } - - /** - * Gets the public 'request' shared service. - * - * @throws RuntimeException always since this service is expected to be injected dynamically - */ - protected function getRequestService() - { - throw new RuntimeException('You have requested a synthetic service ("request"). The DIC does not know how to construct this service.'); - } - - /** - * Gets the public 'service_from_static_method' shared service. - * - * @return \Bar\FooClass - */ - protected function getServiceFromStaticMethodService() - { - return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance(); - } - - /** - * Gets the private 'configurator_service' shared service. - * - * @return \ConfClass - */ - protected function getConfiguratorServiceService() - { - $this->services['configurator_service'] = $instance = new \ConfClass(); - - $instance->setFoo($this->get('baz')); - - return $instance; - } - - /** - * Gets the private 'inlined' shared service. - * - * @return \Bar - */ - protected function getInlinedService() - { - $this->services['inlined'] = $instance = new \Bar(); - - $instance->pub = 'pub'; - $instance->setBaz($this->get('baz')); - - return $instance; - } - - /** - * Gets the private 'new_factory' shared service. - * - * @return \FactoryClass - */ - protected function getNewFactoryService() - { - $this->services['new_factory'] = $instance = new \FactoryClass(); - - $instance->foo = 'bar'; - - return $instance; - } - - /** - * Gets the default parameters. - * - * @return array An array of the default parameters - */ - protected function getDefaultParameters() - { - return array( - 'baz_class' => 'BazClass', - 'foo_class' => 'Bar\\FooClass', - 'foo' => 'bar', - ); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt new file mode 100644 index 0000000000000..13cfae45af87d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt @@ -0,0 +1,479 @@ +Array +( + [Container%s/getBARService.php] => services['BAR'] = $instance = new \stdClass(); + +$instance->bar = ($this->services['bar'] ?? $this->load(__DIR__.'/getBar3Service.php')); + +return $instance; + + [Container%s/getBAR2Service.php] => services['BAR2'] = new \stdClass(); + + [Container%s/getBar3Service.php] => services['foo.baz'] ?? $this->load(__DIR__.'/getFoo_BazService.php')); + +$this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); + +$a->configure($instance); + +return $instance; + + [Container%s/getBar22Service.php] => services['bar2'] = new \stdClass(); + + [Container%s/getBazService.php] => services['baz'] = $instance = new \Baz(); + +$instance->setFoo(($this->services['foo_with_inline'] ?? $this->load(__DIR__.'/getFooWithInlineService.php'))); + +return $instance; + + [Container%s/getConfiguredServiceService.php] => setFoo(($this->services['baz'] ?? $this->load(__DIR__.'/getBazService.php'))); + +$this->services['configured_service'] = $instance = new \stdClass(); + +$a->configureStdClass($instance); + +return $instance; + + [Container%s/getConfiguredServiceSimpleService.php] => services['configured_service_simple'] = $instance = new \stdClass(); + +(new \ConfClass('bar'))->configureStdClass($instance); + +return $instance; + + [Container%s/getDecoratorServiceService.php] => services['decorator_service'] = new \stdClass(); + + [Container%s/getDecoratorServiceWithNameService.php] => services['decorator_service_with_name'] = new \stdClass(); + + [Container%s/getDeprecatedServiceService.php] => services['deprecated_service'] = new \stdClass(); + + [Container%s/getFactoryServiceService.php] => services['factory_service'] = ($this->services['foo.baz'] ?? $this->load(__DIR__.'/getFoo_BazService.php'))->getInstance(); + + [Container%s/getFactoryServiceSimpleService.php] => services['factory_service_simple'] = ($this->privates['factory_simple'] ?? $this->load(__DIR__.'/getFactorySimpleService.php'))->getInstance(); + + [Container%s/getFactorySimpleService.php] => privates['factory_simple'] = new \SimpleFactoryClass('foo'); + + [Container%s/getFooService.php] => services['foo.baz'] ?? $this->load(__DIR__.'/getFoo_BazService.php')); + +$this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array('bar' => 'foo is bar', 'foobar' => 'bar'), true, $this); + +$instance->foo = 'bar'; +$instance->moo = $a; +$instance->qux = array('bar' => 'foo is bar', 'foobar' => 'bar'); +$instance->setBar(($this->services['bar'] ?? $this->load(__DIR__.'/getBar3Service.php'))); +$instance->initialize(); +sc_configure($instance); + +return $instance; + + [Container%s/getFoo_BazService.php] => services['foo.baz'] = $instance = \BazClass::getInstance(); + +\BazClass::configureStatic1($instance); + +return $instance; + + [Container%s/getFooWithInlineService.php] => services['foo_with_inline'] = $instance = new \Foo(); + +$a->pub = 'pub'; +$a->setBaz(($this->services['baz'] ?? $this->load(__DIR__.'/getBazService.php'))); + +$instance->setBar($a); + +return $instance; + + [Container%s/getLazyContextService.php] => services['lazy_context'] = new \LazyContext(new RewindableGenerator(function () { + yield 'k1' => ($this->services['foo.baz'] ?? $this->load(__DIR__.'/getFoo_BazService.php')); + yield 'k2' => $this; +}, 2), new RewindableGenerator(function () { + return new \EmptyIterator(); +}, 0)); + + [Container%s/getLazyContextIgnoreInvalidRefService.php] => services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function () { + yield 0 => ($this->services['foo.baz'] ?? $this->load(__DIR__.'/getFoo_BazService.php')); +}, 1), new RewindableGenerator(function () { + return new \EmptyIterator(); +}, 0)); + + [Container%s/getMethodCall1Service.php] => targetDirs[0].'/Fixtures/includes/foo.php'); + +$this->services['method_call1'] = $instance = new \Bar\FooClass(); + +$instance->setBar(($this->services['foo'] ?? $this->load(__DIR__.'/getFooService.php'))); +$instance->setBar(NULL); +$instance->setBar((($this->services['foo'] ?? $this->load(__DIR__.'/getFooService.php'))->foo() . (($this->hasParameter("foo")) ? ($this->getParameter("foo")) : ("default")))); + +return $instance; + + [Container%s/getNewFactoryServiceService.php] => foo = 'bar'; + +$this->services['new_factory_service'] = $instance = $a->getInstance(); + +$instance->foo = 'bar'; + +return $instance; + + [Container%s/getServiceFromStaticMethodService.php] => services['service_from_static_method'] = \Bar\FooClass::getInstance(); + + [Container%s/Container.php] => targetDirs[0] = dirname(__DIR__); + for ($i = 1; $i <= 5; ++$i) { + $this->targetDirs[$i] = $dir = dirname($dir); + } + $this->parameters = $this->getDefaultParameters(); + + $this->services = $this->privates = array(); + $this->methodMap = array( + 'foo_bar' => 'getFooBarService', + ); + $this->fileMap = array( + 'BAR' => __DIR__.'/getBARService.php', + 'BAR2' => __DIR__.'/getBAR2Service.php', + 'bar' => __DIR__.'/getBar3Service.php', + 'bar2' => __DIR__.'/getBar22Service.php', + 'baz' => __DIR__.'/getBazService.php', + 'configured_service' => __DIR__.'/getConfiguredServiceService.php', + 'configured_service_simple' => __DIR__.'/getConfiguredServiceSimpleService.php', + 'decorator_service' => __DIR__.'/getDecoratorServiceService.php', + 'decorator_service_with_name' => __DIR__.'/getDecoratorServiceWithNameService.php', + 'deprecated_service' => __DIR__.'/getDeprecatedServiceService.php', + 'factory_service' => __DIR__.'/getFactoryServiceService.php', + 'factory_service_simple' => __DIR__.'/getFactoryServiceSimpleService.php', + 'foo' => __DIR__.'/getFooService.php', + 'foo.baz' => __DIR__.'/getFoo_BazService.php', + 'foo_with_inline' => __DIR__.'/getFooWithInlineService.php', + 'lazy_context' => __DIR__.'/getLazyContextService.php', + 'lazy_context_ignore_invalid_ref' => __DIR__.'/getLazyContextIgnoreInvalidRefService.php', + 'method_call1' => __DIR__.'/getMethodCall1Service.php', + 'new_factory_service' => __DIR__.'/getNewFactoryServiceService.php', + 'service_from_static_method' => __DIR__.'/getServiceFromStaticMethodService.php', + ); + $this->aliases = array( + 'alias_for_alias' => 'foo', + 'alias_for_foo' => 'foo', + 'decorated' => 'decorator_service_with_name', + ); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * {@inheritdoc} + */ + protected function load($file, $lazyLoad = true) + { + return require $file; + } + + /** + * Gets the public 'foo_bar' service. + * + * @return \Bar\FooClass + */ + protected function getFooBarService() + { + return new \Bar\FooClass(($this->services['deprecated_service'] ?? $this->load(__DIR__.'/getDeprecatedServiceService.php'))); + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) + { + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); + + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + + return $this->parameters[$name]; + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) + { + $name = strtolower($name); + + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + /** + * {@inheritdoc} + */ + public function getParameterBag() + { + if (null === $this->parameterBag) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = array(); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + + /** + * Gets the default parameters. + * + * @return array An array of the default parameters + */ + protected function getDefaultParameters() + { + return array( + 'baz_class' => 'BazClass', + 'foo_class' => 'Bar\\FooClass', + 'foo' => 'bar', + ); + } +} + + [ProjectServiceContainer.php] => parameters = $this->getDefaultParameters(); - $this->services = - $this->scopedServices = - $this->scopeStacks = array(); - $this->scopes = array(); - $this->scopeChildren = array(); + $this->services = $this->privates = array(); $this->methodMap = array( - 'bar' => 'getBarService', + 'BAR' => 'getBARService', + 'BAR2' => 'getBAR2Service', + 'bar' => 'getBar3Service', + 'bar2' => 'getBar22Service', 'baz' => 'getBazService', 'configured_service' => 'getConfiguredServiceService', + 'configured_service_simple' => 'getConfiguredServiceSimpleService', 'decorator_service' => 'getDecoratorServiceService', 'decorator_service_with_name' => 'getDecoratorServiceWithNameService', 'deprecated_service' => 'getDeprecatedServiceService', 'factory_service' => 'getFactoryServiceService', + 'factory_service_simple' => 'getFactoryServiceSimpleService', 'foo' => 'getFooService', 'foo.baz' => 'getFoo_BazService', 'foo_bar' => 'getFooBarService', 'foo_with_inline' => 'getFooWithInlineService', + 'lazy_context' => 'getLazyContextService', + 'lazy_context_ignore_invalid_ref' => 'getLazyContextIgnoreInvalidRefService', 'method_call1' => 'getMethodCall1Service', 'new_factory_service' => 'getNewFactoryServiceService', - 'request' => 'getRequestService', 'service_from_static_method' => 'getServiceFromStaticMethodService', ); $this->aliases = array( @@ -55,30 +60,63 @@ public function __construct() ); } + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + /** * {@inheritdoc} */ public function compile() { - throw new LogicException('You cannot compile a dumped frozen container.'); + throw new LogicException('You cannot compile a dumped container that was already compiled.'); } /** * {@inheritdoc} */ - public function isFrozen() + public function isCompiled() { return true; } + /** + * Gets the public 'BAR' shared service. + * + * @return \stdClass + */ + protected function getBARService() + { + $this->services['BAR'] = $instance = new \stdClass(); + + $instance->bar = ($this->services['bar'] ?? $this->getBar3Service()); + + return $instance; + } + + /** + * Gets the public 'BAR2' shared service. + * + * @return \stdClass + */ + protected function getBAR2Service() + { + return $this->services['BAR2'] = new \stdClass(); + } + /** * Gets the public 'bar' shared service. * * @return \Bar\FooClass */ - protected function getBarService() + protected function getBar3Service() { - $a = $this->get('foo.baz'); + $a = ($this->services['foo.baz'] ?? $this->getFoo_BazService()); $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); @@ -87,6 +125,16 @@ protected function getBarService() return $instance; } + /** + * Gets the public 'bar2' shared service. + * + * @return \stdClass + */ + protected function getBar22Service() + { + return $this->services['bar2'] = new \stdClass(); + } + /** * Gets the public 'baz' shared service. * @@ -96,7 +144,7 @@ protected function getBazService() { $this->services['baz'] = $instance = new \Baz(); - $instance->setFoo($this->get('foo_with_inline')); + $instance->setFoo(($this->services['foo_with_inline'] ?? $this->getFooWithInlineService())); return $instance; } @@ -109,7 +157,7 @@ protected function getBazService() protected function getConfiguredServiceService() { $a = new \ConfClass(); - $a->setFoo($this->get('baz')); + $a->setFoo(($this->services['baz'] ?? $this->getBazService())); $this->services['configured_service'] = $instance = new \stdClass(); @@ -118,6 +166,20 @@ protected function getConfiguredServiceService() return $instance; } + /** + * Gets the public 'configured_service_simple' shared service. + * + * @return \stdClass + */ + protected function getConfiguredServiceSimpleService() + { + $this->services['configured_service_simple'] = $instance = new \stdClass(); + + (new \ConfClass('bar'))->configureStdClass($instance); + + return $instance; + } + /** * Gets the public 'decorator_service' shared service. * @@ -159,7 +221,17 @@ protected function getDeprecatedServiceService() */ protected function getFactoryServiceService() { - return $this->services['factory_service'] = $this->get('foo.baz')->getInstance(); + return $this->services['factory_service'] = ($this->services['foo.baz'] ?? $this->getFoo_BazService())->getInstance(); + } + + /** + * Gets the public 'factory_service_simple' shared service. + * + * @return \Bar + */ + protected function getFactoryServiceSimpleService() + { + return $this->services['factory_service_simple'] = ($this->privates['factory_simple'] ?? $this->getFactorySimpleService())->getInstance(); } /** @@ -169,14 +241,14 @@ protected function getFactoryServiceService() */ protected function getFooService() { - $a = $this->get('foo.baz'); + $a = ($this->services['foo.baz'] ?? $this->getFoo_BazService()); $this->services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array('bar' => 'foo is bar', 'foobar' => 'bar'), true, $this); $instance->foo = 'bar'; $instance->moo = $a; $instance->qux = array('bar' => 'foo is bar', 'foobar' => 'bar'); - $instance->setBar($this->get('bar')); + $instance->setBar(($this->services['bar'] ?? $this->getBar3Service())); $instance->initialize(); sc_configure($instance); @@ -204,7 +276,7 @@ protected function getFoo_BazService() */ protected function getFooBarService() { - return new \Bar\FooClass(); + return new \Bar\FooClass(($this->services['deprecated_service'] ?? $this->getDeprecatedServiceService())); } /** @@ -219,13 +291,42 @@ protected function getFooWithInlineService() $this->services['foo_with_inline'] = $instance = new \Foo(); $a->pub = 'pub'; - $a->setBaz($this->get('baz')); + $a->setBaz(($this->services['baz'] ?? $this->getBazService())); $instance->setBar($a); return $instance; } + /** + * Gets the public 'lazy_context' shared service. + * + * @return \LazyContext + */ + protected function getLazyContextService() + { + return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function () { + yield 'k1' => ($this->services['foo.baz'] ?? $this->getFoo_BazService()); + yield 'k2' => $this; + }, 2), new RewindableGenerator(function () { + return new \EmptyIterator(); + }, 0)); + } + + /** + * Gets the public 'lazy_context_ignore_invalid_ref' shared service. + * + * @return \LazyContext + */ + protected function getLazyContextIgnoreInvalidRefService() + { + return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function () { + yield 0 => ($this->services['foo.baz'] ?? $this->getFoo_BazService()); + }, 1), new RewindableGenerator(function () { + return new \EmptyIterator(); + }, 0)); + } + /** * Gets the public 'method_call1' shared service. * @@ -237,9 +338,9 @@ protected function getMethodCall1Service() $this->services['method_call1'] = $instance = new \Bar\FooClass(); - $instance->setBar($this->get('foo')); + $instance->setBar(($this->services['foo'] ?? $this->getFooService())); $instance->setBar(NULL); - $instance->setBar(($this->get("foo")->foo() . (($this->hasParameter("foo")) ? ($this->getParameter("foo")) : ("default")))); + $instance->setBar((($this->services['foo'] ?? $this->getFooService())->foo() . (($this->hasParameter("foo")) ? ($this->getParameter("foo")) : ("default")))); return $instance; } @@ -262,23 +363,27 @@ protected function getNewFactoryServiceService() } /** - * Gets the public 'request' shared service. + * Gets the public 'service_from_static_method' shared service. * - * @throws RuntimeException always since this service is expected to be injected dynamically + * @return \Bar\FooClass */ - protected function getRequestService() + protected function getServiceFromStaticMethodService() { - throw new RuntimeException('You have requested a synthetic service ("request"). The DIC does not know how to construct this service.'); + return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance(); } /** - * Gets the public 'service_from_static_method' shared service. + * Gets the private 'factory_simple' shared service. * - * @return \Bar\FooClass + * @return \SimpleFactoryClass + * + * @deprecated The "factory_simple" service is deprecated. You should stop using it, as it will soon be removed. */ - protected function getServiceFromStaticMethodService() + protected function getFactorySimpleService() { - return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance(); + @trigger_error('The "factory_simple" service is deprecated. You should stop using it, as it will soon be removed.', E_USER_DEPRECATED); + + return $this->privates['factory_simple'] = new \SimpleFactoryClass('foo'); } /** @@ -286,10 +391,15 @@ protected function getServiceFromStaticMethodService() */ public function getParameter($name) { - $name = strtolower($name); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); - if (!(isset($this->parameters[$name]) || array_key_exists($name, $this->parameters))) { - throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); } return $this->parameters[$name]; @@ -302,7 +412,7 @@ public function hasParameter($name) { $name = strtolower($name); - return isset($this->parameters[$name]) || array_key_exists($name, $this->parameters); + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); } /** @@ -319,12 +429,33 @@ public function setParameter($name, $value) public function getParameterBag() { if (null === $this->parameterBag) { - $this->parameterBag = new FrozenParameterBag($this->parameters); + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); } return $this->parameterBag; } + private $loadedDynamicParameters = array(); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + /** * Gets the default parameters. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_array_params.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_array_params.php new file mode 100644 index 0000000000000..629a41e027161 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_array_params.php @@ -0,0 +1,176 @@ +targetDirs[$i] = $dir = dirname($dir); + } + $this->parameters = $this->getDefaultParameters(); + + $this->services = $this->privates = array(); + $this->methodMap = array( + 'bar' => 'getBarService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the public 'bar' shared service. + * + * @return \BarClass + */ + protected function getBarService() + { + $this->services['bar'] = $instance = new \BarClass(); + + $instance->setBaz($this->parameters['array_1'], $this->getParameter('array_2'), '%array_1%'); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) + { + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = strtolower($name); + + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + + return $this->parameters[$name]; + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) + { + $name = strtolower($name); + + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + /** + * {@inheritdoc} + */ + public function getParameterBag() + { + if (null === $this->parameterBag) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = array( + 'array_2' => false, + ); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + switch ($name) { + case 'array_2': $value = array( + 0 => ($this->targetDirs[2].'/Dumper'), + ); break; + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; + } + + /** + * Gets the default parameters. + * + * @return array An array of the default parameters + */ + protected function getDefaultParameters() + { + return array( + 'array_1' => array( + 0 => 123, + ), + ); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php new file mode 100644 index 0000000000000..852011ac7d1ca --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_locator.php @@ -0,0 +1,172 @@ +services = $this->privates = array(); + $this->methodMap = array( + 'bar_service' => 'getBarServiceService', + 'foo_service' => 'getFooServiceService', + 'translator.loader_1' => 'getTranslator_Loader1Service', + 'translator.loader_2' => 'getTranslator_Loader2Service', + 'translator.loader_3' => 'getTranslator_Loader3Service', + 'translator_1' => 'getTranslator1Service', + 'translator_2' => 'getTranslator2Service', + 'translator_3' => 'getTranslator3Service', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the public 'bar_service' shared service. + * + * @return \stdClass + */ + protected function getBarServiceService() + { + return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass()))); + } + + /** + * Gets the public 'foo_service' shared service. + * + * @return \Symfony\Component\DependencyInjection\ServiceLocator + */ + protected function getFooServiceService() + { + return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\ServiceLocator(array('bar' => function () { + return ($this->services['bar_service'] ?? $this->getBarServiceService()); + }, 'baz' => function (): \stdClass { + return ($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass())); + }, 'nil' => function () { + return NULL; + })); + } + + /** + * Gets the public 'translator.loader_1' shared service. + * + * @return \stdClass + */ + protected function getTranslator_Loader1Service() + { + return $this->services['translator.loader_1'] = new \stdClass(); + } + + /** + * Gets the public 'translator.loader_2' shared service. + * + * @return \stdClass + */ + protected function getTranslator_Loader2Service() + { + return $this->services['translator.loader_2'] = new \stdClass(); + } + + /** + * Gets the public 'translator.loader_3' shared service. + * + * @return \stdClass + */ + protected function getTranslator_Loader3Service() + { + return $this->services['translator.loader_3'] = new \stdClass(); + } + + /** + * Gets the public 'translator_1' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator + */ + protected function getTranslator1Service() + { + return $this->services['translator_1'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_1' => function () { + return ($this->services['translator.loader_1'] ?? $this->getTranslator_Loader1Service()); + }))); + } + + /** + * Gets the public 'translator_2' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator + */ + protected function getTranslator2Service() + { + $this->services['translator_2'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_2' => function () { + return ($this->services['translator.loader_2'] ?? $this->getTranslator_Loader2Service()); + }))); + + $instance->addResource('db', ($this->services['translator.loader_2'] ?? $this->getTranslator_Loader2Service()), 'nl'); + + return $instance; + } + + /** + * Gets the public 'translator_3' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator + */ + protected function getTranslator3Service() + { + $a = ($this->services['translator.loader_3'] ?? $this->getTranslator_Loader3Service()); + + $this->services['translator_3'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_3' => function () { + return ($this->services['translator.loader_3'] ?? $this->getTranslator_Loader3Service()); + }))); + + $instance->addResource('db', $a, 'nl'); + $instance->addResource('db', $a, 'en'); + + return $instance; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php new file mode 100644 index 0000000000000..9a0611949b57e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_frozen.php @@ -0,0 +1,83 @@ +services = $this->privates = array(); + $this->methodMap = array( + 'bar_service' => 'getBarServiceService', + 'foo_service' => 'getFooServiceService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the public 'bar_service' shared service. + * + * @return \stdClass + */ + protected function getBarServiceService() + { + return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass()))); + } + + /** + * Gets the public 'foo_service' shared service. + * + * @return \stdClass + */ + protected function getFooServiceService() + { + return $this->services['foo_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass()))); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_in_expression.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_in_expression.php new file mode 100644 index 0000000000000..8e3060e130436 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_private_in_expression.php @@ -0,0 +1,72 @@ +services = $this->privates = array(); + $this->methodMap = array( + 'public_foo' => 'getPublicFooService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the public 'public_foo' shared service. + * + * @return \stdClass + */ + protected function getPublicFooService() + { + return $this->services['public_foo'] = new \stdClass(($this->privates['private_foo'] ?? ($this->privates['private_foo'] = new \stdClass()))); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php new file mode 100644 index 0000000000000..10d8f67c51e7f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -0,0 +1,91 @@ +services = $this->privates = array(); + $this->methodMap = array( + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => 'getTestServiceSubscriberService', + 'foo_service' => 'getFooServiceService', + ); + + $this->aliases = array(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->privates = array(); + parent::reset(); + } + + /** + * {@inheritdoc} + */ + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + /** + * {@inheritdoc} + */ + public function isCompiled() + { + return true; + } + + /** + * Gets the public 'Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber + */ + protected function getTestServiceSubscriberService() + { + return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber(); + } + + /** + * Gets the public 'foo_service' shared autowired service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber + */ + protected function getFooServiceService() + { + return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber(new \Symfony\Component\DependencyInjection\ServiceLocator(array('Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => function (): ?\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition { + return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition())); + }, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => function (): \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber { + return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? $this->getTestServiceSubscriberService()); + }, 'bar' => function (): \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition { + return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? $this->getTestServiceSubscriberService()); + }, 'baz' => function (): ?\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition { + return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition())); + }))); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/class_from_id.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/class_from_id.xml new file mode 100644 index 0000000000000..45415cce472f0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/class_from_id.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/invalid_alias_definition.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/invalid_alias_definition.xml new file mode 100644 index 0000000000000..8e99561dca608 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/invalid_alias_definition.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/legacy-services6.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/legacy-services6.xml deleted file mode 100644 index c8e6e30bc9db6..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/legacy-services6.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/legacy-services9.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/legacy-services9.xml deleted file mode 100644 index dcb312a1e9f4c..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/legacy-services9.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - Bar\FooClass - BazClass - bar - - - - - - foo - - - foo is %foo% - %foo% - - true - - bar - - - foo is %foo% - %foo% - - - - - - - - - - - - - - diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/nested_service_without_id.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/nested_service_without_id.xml new file mode 100644 index 0000000000000..f8eb009949943 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/nested_service_without_id.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services1.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services1.xml index 6aa5732f9afc7..fd7bb3cf9b080 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services1.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services1.xml @@ -1,2 +1,8 @@ - + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services20.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services20.xml deleted file mode 100644 index 5d799fc944c80..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services20.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services21.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services21.xml index 329bc3d72a25a..2ed88fee5a0d4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services21.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services21.xml @@ -1,6 +1,7 @@ + @@ -17,5 +18,7 @@ + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services22.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services22.xml deleted file mode 100644 index fa79d389489fb..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services22.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Bar - Baz - - - diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services24.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services24.xml index 476588aa4df97..c4e32cb634e0c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services24.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services24.xml @@ -1,9 +1,9 @@ - - A - B - + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services28.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services28.xml new file mode 100644 index 0000000000000..0076cc31ebbf1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services28.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services4.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services4.xml index 03ad9f8081dd3..47ec04e68505f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services4.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services4.xml @@ -9,5 +9,6 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services5.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services5.xml index 347df977dd26b..1b72e778c3835 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services5.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services5.xml @@ -18,8 +18,5 @@ - - - diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml index 11b2993844342..5134ff7671f11 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services6.xml @@ -45,6 +45,7 @@ + @@ -57,5 +58,8 @@ + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml index b17e50043cd1b..533d2a38d765e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml @@ -19,4 +19,9 @@ null + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml index 72224fe56f65b..093a766d9b778 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml @@ -6,6 +6,7 @@ bar + @@ -40,7 +41,9 @@ %foo_bar% - + + + %path%foo.php @@ -84,6 +87,12 @@ + + bar + + + + @@ -103,6 +112,34 @@ + + foo + The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_abstract.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_abstract.xml index 97e5ce419befc..334e8b045f237 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_abstract.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_abstract.xml @@ -1,6 +1,9 @@ + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_autoconfigure.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_autoconfigure.xml new file mode 100644 index 0000000000000..5e855c097288e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_autoconfigure.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_autoconfigure_with_parent.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_autoconfigure_with_parent.xml new file mode 100644 index 0000000000000..103045d38fcb3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_autoconfigure_with_parent.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml new file mode 100644 index 0000000000000..408bca5f75346 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml @@ -0,0 +1,21 @@ + + + + + null + quz + factory + + + + + + null + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_case.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_case.xml new file mode 100644 index 0000000000000..31fea2eb260ad --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_case.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_defaults_with_parent.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_defaults_with_parent.xml new file mode 100644 index 0000000000000..875ed6d51f996 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_defaults_with_parent.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml new file mode 100644 index 0000000000000..839776a3fed97 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof_with_parent.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof_with_parent.xml new file mode 100644 index 0000000000000..67ce6917249bd --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof_with_parent.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_named_args.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_named_args.xml new file mode 100644 index 0000000000000..95dabde9c4b6b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_named_args.xml @@ -0,0 +1,12 @@ + + + + + ABCD + null + + 123 + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml new file mode 100644 index 0000000000000..333e71ce57d5a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_without_id.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_without_id.xml new file mode 100644 index 0000000000000..afabf3d891d39 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_without_id.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/xml_with_wrong_ext.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/xml_with_wrong_ext.php new file mode 100644 index 0000000000000..91ff5d460203b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/xml_with_wrong_ext.php @@ -0,0 +1,9 @@ + + + + + from xml + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services.yml new file mode 100644 index 0000000000000..fe54b8987e7f3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services.yml @@ -0,0 +1,14 @@ +imports: + # Ensure the anonymous services count is reset after importing a file + - { resource: anonymous_services_in_instanceof.yml } + +services: + _defaults: + autowire: true + + Foo: + arguments: + - !service + class: Bar + autowire: true + factory: [ !service { class: Quz }, 'constructFoo' ] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_alias.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_alias.yml new file mode 100644 index 0000000000000..96546b83ac41c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_alias.yml @@ -0,0 +1,7 @@ +services: + Bar: ~ + + Foo: + arguments: + - !service + alias: Bar diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_instanceof.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_instanceof.yml new file mode 100644 index 0000000000000..ea0bebaf12433 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_instanceof.yml @@ -0,0 +1,15 @@ +services: + _instanceof: + # Ensure previous conditionals aren't applied on anonymous services + Quz: + autowire: true + + DummyInterface: + properties: + foo: !service { class: Anonymous } + + # Ensure next conditionals are not considered as services + Bar: + autowire: true + + Dummy: ~ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_parameters.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_parameters.yml new file mode 100644 index 0000000000000..9d9bea344efec --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/anonymous_services_in_parameters.yml @@ -0,0 +1,2 @@ +parameters: + foo: [ !service { } ] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_alias.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_alias.yml new file mode 100644 index 0000000000000..78975e5092866 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_alias.yml @@ -0,0 +1,11 @@ +services: + foo: + class: stdClass + public: false + + bar: + alias: foo + public: true + # keys other than "alias" and "public" are invalid when defining an alias. + calls: + - [doSomething] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_empty_defaults.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_empty_defaults.yml new file mode 100644 index 0000000000000..85d910e3e31fb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_empty_defaults.yml @@ -0,0 +1,2 @@ +services: + _defaults: diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_empty_instanceof.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_empty_instanceof.yml new file mode 100644 index 0000000000000..2c8ce09ff6f3e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_empty_instanceof.yml @@ -0,0 +1,2 @@ +services: + _instanceof: diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_import.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_import.yml index 0765dc8dd0856..2dbbcbf2653f3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_import.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_import.yml @@ -1,2 +1,2 @@ imports: - - foo.yml + - { resource: ~ } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_keyword.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_keyword.yml new file mode 100644 index 0000000000000..8487e854d4c36 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_keyword.yml @@ -0,0 +1,10 @@ +services: + # This definition is valid and should not raise any deprecation notice + foo: + class: stdClass + arguments: [ 'foo', 'bar' ] + + # This definition is invalid and must raise a deprecation notice + bar: + class: stdClass + private: true # the "private" keyword is invalid diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_types1.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_types1.yml deleted file mode 100644 index 891e01497cadf..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_types1.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - foo_service: - class: FooClass - # types is not an array - autowiring_types: 1 diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_types2.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_types2.yml deleted file mode 100644 index fb1d53e151014..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bad_types2.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - foo_service: - class: FooClass - # autowiring_types is not a string - autowiring_types: [ 1 ] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag4.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag4.yml deleted file mode 100644 index e8e99395b1fdf..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/badtag4.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - foo_service: - class: FooClass - tags: - # tag is not an array - - foo diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bar/services.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bar/services.yml new file mode 100644 index 0000000000000..0f846f5f76c6e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/bar/services.yml @@ -0,0 +1,4 @@ +services: + AppBundle\Foo: + arguments: + - !service {class: AppBundle\Bar } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/class_from_id.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/class_from_id.yml new file mode 100644 index 0000000000000..33f0b2ea6821f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/class_from_id.yml @@ -0,0 +1,3 @@ +services: + Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass: + autowire: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/foo/services.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/foo/services.yml new file mode 100644 index 0000000000000..76eee552fac22 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/foo/services.yml @@ -0,0 +1,4 @@ +services: + AppBundle\Hello: + arguments: + - !service {class: AppBundle\World} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/_child.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/_child.yml new file mode 100644 index 0000000000000..89d8b914491eb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/_child.yml @@ -0,0 +1,4 @@ +services: + child_service: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/expected.yml new file mode 100644 index 0000000000000..ca08caad673f7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/expected.yml @@ -0,0 +1,10 @@ +services: + child_service_expected: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + # the parent has autoconfigure true, but that does not cascade to the child + autoconfigure: false + # an autoconfigured "instanceof" is setup for IntegrationTestStub + # but its calls are NOT added, because the class was only + # set on the parent, not the child + #calls: + # - [enableSummer, [true]] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/main.yml new file mode 100644 index 0000000000000..02533bf0f5739 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_child_not_applied/main.yml @@ -0,0 +1,7 @@ +imports: + - { resource: _child.yml } + +services: + parent_service: + autoconfigure: true + abstract: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/_child.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/_child.yml new file mode 100644 index 0000000000000..5319c2045f93a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/_child.yml @@ -0,0 +1,3 @@ +services: + child_service: + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/expected.yml new file mode 100644 index 0000000000000..c1dca0763cc90 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/expected.yml @@ -0,0 +1,5 @@ +services: + child_service_expected: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + # autoconfigure is set on the parent, but not on the child + autoconfigure: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/main.yml new file mode 100644 index 0000000000000..ab9877d16b9e7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child/main.yml @@ -0,0 +1,7 @@ +imports: + - { resource: _child.yml } + +services: + parent_service: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + autoconfigure: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/_child.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/_child.yml new file mode 100644 index 0000000000000..5319c2045f93a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/_child.yml @@ -0,0 +1,3 @@ +services: + child_service: + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/expected.yml new file mode 100644 index 0000000000000..02cf0037e215d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/expected.yml @@ -0,0 +1,6 @@ +services: + child_service_expected: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + # from an autoconfigured "instanceof" applied to parent class + # but NOT inherited down to child + # tags: [from_autoconfigure] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/main.yml new file mode 100644 index 0000000000000..ab9877d16b9e7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/autoconfigure_parent_child_tags/main.yml @@ -0,0 +1,7 @@ +imports: + - { resource: _child.yml } + +services: + parent_service: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + autoconfigure: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/child_parent/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/child_parent/expected.yml new file mode 100644 index 0000000000000..54cd91c2022a8 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/child_parent/expected.yml @@ -0,0 +1,9 @@ +services: + # child_service in the end should be identical to this + child_service_expected: + class: stdClass + autowire: false + public: true + lazy: true + # ONLY the child tag, the parent tag does not inherit + tags: [from_child] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/child_parent/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/child_parent/main.yml new file mode 100644 index 0000000000000..edaa3a3b43993 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/child_parent/main.yml @@ -0,0 +1,13 @@ +services: + parent_service: + abstract: true + lazy: true + autowire: false + public: false + tags: [from_parent] + + child_service: + class: stdClass + parent: parent_service + public: true + tags: [from_child] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_child_tags/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_child_tags/expected.yml new file mode 100644 index 0000000000000..cb36636e00213 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_child_tags/expected.yml @@ -0,0 +1,8 @@ +services: + child_service_expected: + class: stdClass + # set explicitly on child (not overridden by parent) + autoconfigure: false + # set explicitly on child + autowire: true + tags: [from_defaults] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_child_tags/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_child_tags/main.yml new file mode 100644 index 0000000000000..b5dc66ff0eb04 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_child_tags/main.yml @@ -0,0 +1,18 @@ +services: + _defaults: + autoconfigure: true + autowire: true + tags: [from_defaults] + + parent_service: + class: stdClass + # will not override child + autoconfigure: true + # parent definitions are not applied to children + tags: [from_parent] + + child_service: + parent: parent_service + # _defaults is ok because these are explicitly set + autoconfigure: false + autowire: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_instanceof_importance/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_instanceof_importance/expected.yml new file mode 100644 index 0000000000000..e9161dccfc079 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_instanceof_importance/expected.yml @@ -0,0 +1,26 @@ +services: + # main_service should look like this in the end + main_service_expected: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + # _instanceof overrides _defaults + autowire: false + # inherited from _defaults + autoconfigure: true + # from _instanceof + shared: false + # service definition overrides _instanceof + public: true + + tags: + - { name: foo_tag, tag_option: from_service } + # these 2 are from instanceof + - { name: foo_tag, tag_option: from_instanceof } + - { name: bar_tag } + - { name: from_defaults } + # calls from instanceof are kept, but this comes later + calls: + # first call is from instanceof + - [setSunshine, [bright]] + # + - [enableSummer, [true]] + - [setSunshine, [warm]] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_instanceof_importance/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_instanceof_importance/main.yml new file mode 100644 index 0000000000000..406a351d1d3ef --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_instanceof_importance/main.yml @@ -0,0 +1,30 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: true + tags: [from_defaults] + + _instanceof: + Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStubParent: + autowire: false + shared: false + public: false + tags: + - { name: foo_tag, tag_option: from_instanceof } + calls: + - [setSunshine, [bright]] + + Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub: + tags: + - { name: bar_tag } + + main_service: + class: Symfony\Component\DependencyInjection\Tests\Compiler\IntegrationTestStub + public: true + tags: + - { name: foo_tag, tag_option: from_service } + # calls from instanceof are kept, but this comes later + calls: + - [enableSummer, [true]] + - [setSunshine, [warm]] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/_child.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/_child.yml new file mode 100644 index 0000000000000..86ef83c26dfca --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/_child.yml @@ -0,0 +1,4 @@ +services: + # loaded here to avoid defaults in other file + child_service: + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/expected.yml new file mode 100644 index 0000000000000..012672ff8b8fb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/expected.yml @@ -0,0 +1,6 @@ +services: + child_service_expected: + class: stdClass + # _defaults is applied to the parent, but autoconfigure: true + # does not cascade to the child + autoconfigure: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/main.yml new file mode 100644 index 0000000000000..8b90b4e985892 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/defaults_parent_child/main.yml @@ -0,0 +1,9 @@ +imports: + - { resource: _child.yml } + +services: + _defaults: + autoconfigure: true + + parent_service: + class: stdClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/_child.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/_child.yml new file mode 100644 index 0000000000000..86ef83c26dfca --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/_child.yml @@ -0,0 +1,4 @@ +services: + # loaded here to avoid defaults in other file + child_service: + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/expected.yml new file mode 100644 index 0000000000000..074ed01d03c4c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/expected.yml @@ -0,0 +1,7 @@ +services: + child_service_expected: + class: stdClass + # applied to _instanceof of parent + autowire: true + # from _instanceof, applies to parent, but does NOT inherit to here + # tags: [from_instanceof] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/main.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/main.yml new file mode 100644 index 0000000000000..44cf9b0045d40 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/integration/instanceof_parent_child/main.yml @@ -0,0 +1,11 @@ +imports: + - { resource: _child.yml } + +services: + _instanceof: + stdClass: + autowire: true + tags: [from_instanceof] + + parent_service: + class: stdClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy-services6.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy-services6.yml deleted file mode 100644 index 2e702bff2f89b..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy-services6.yml +++ /dev/null @@ -1,11 +0,0 @@ -services: - constructor: { class: FooClass, factory_method: getInstance } - factory_service: { class: BazClass, factory_method: getInstance, factory_service: baz_factory } - scope.container: { class: FooClass, scope: container } - scope.custom: { class: FooClass, scope: custom } - scope.prototype: { class: FooClass, scope: prototype } - request: - class: Request - synthetic: true - synchronized: true - lazy: true diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy-services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy-services9.yml deleted file mode 100644 index c1bf81a239293..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy-services9.yml +++ /dev/null @@ -1,33 +0,0 @@ -parameters: - foo_class: Bar\FooClass - baz_class: BazClass - foo: bar - -services: - foo: - class: Bar\FooClass - tags: - - { name: foo, foo: foo } - - { name: foo, bar: bar } - factory_class: Bar\FooClass - factory_method: getInstance - arguments: [foo, '@foo.baz', { '%foo%': 'foo is %foo%', foobar: '%foo%' }, true, '@service_container'] - properties: { foo: bar, moo: '@foo.baz', qux: { '%foo%': 'foo is %foo%', foobar: '%foo%' } } - calls: - - [setBar, ['@bar']] - - [initialize, { }] - - configurator: sc_configure - foo.baz: - class: '%baz_class%' - factory_class: '%baz_class%' - factory_method: getInstance - configurator: ['%baz_class%', configureStatic1] - factory_service: - class: Bar - factory_method: getInstance - factory_service: foo.baz - foo_bar: - class: '%foo_class%' - shared: false - scope: prototype diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy_invalid_alias_definition.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy_invalid_alias_definition.yml new file mode 100644 index 0000000000000..00c011c1ddd09 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/legacy_invalid_alias_definition.yml @@ -0,0 +1,5 @@ +services: + foo: + alias: bar + factory: foo + parent: quz diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services1.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services1.yml index 8b137891791fe..7b0d3dc697852 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services1.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services1.yml @@ -1 +1,10 @@ - +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + synthetic: true + Psr\Container\ContainerInterface: + alias: service_container + public: false + Symfony\Component\DependencyInjection\ContainerInterface: + alias: service_container + public: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services2.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services2.yml index b62d5ccfb5577..7ab302bcb9f9a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services2.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services2.yml @@ -5,6 +5,7 @@ parameters: - false - 0 - 1000.3 + - !php/const PHP_INT_MAX bar: foo escape: '@@escapeme' foo_bar: '@foo_bar' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services20.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services20.yml deleted file mode 100644 index 847f656886d5d..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services20.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - request: - class: Request - synthetic: true - synchronized: true - depends_on_request: - class: stdClass - calls: - - [setRequest, ['@?request']] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services22.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services22.yml deleted file mode 100644 index 55d015baea0fb..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services22.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - foo_service: - class: FooClass - autowiring_types: [ Foo, Bar ] - - baz_service: - class: Baz - autowiring_types: Foo diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services24.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services24.yml index 1894077e4b42f..afed157017f4d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services24.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services24.yml @@ -1,8 +1,14 @@ services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + synthetic: true foo: class: Foo autowire: true - autowiring_types: - - A - - B + Psr\Container\ContainerInterface: + alias: service_container + public: false + Symfony\Component\DependencyInjection\ContainerInterface: + alias: service_container + public: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services26.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services26.yml new file mode 100644 index 0000000000000..2ef23c1af545f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services26.yml @@ -0,0 +1,10 @@ +parameters: + env(FOO): foo + bar: '%env(FOO)%' + +services: + test: + class: '%env(FOO)%' + arguments: + - '%env(Bar)%' + - 'foo%bar%baz' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services28.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services28.yml new file mode 100644 index 0000000000000..fd0ce4cf96d9a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services28.yml @@ -0,0 +1,34 @@ +services: + _defaults: + public: false + autowire: true + tags: + - name: foo + + Acme\Foo: ~ + + with_defaults: + class: Foo + + with_null: + class: Foo + public: true + autowire: ~ + + no_defaults: + class: Foo + public: true + autowire: false + tags: [] + + with_defaults_aliased: + alias: with_defaults + + with_defaults_aliased_short: '@with_defaults' + + Acme\WithShortCutArgs: [foo] + + child_def: + parent: with_defaults + public: true + autowire: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31_invalid_tags.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31_invalid_tags.yml new file mode 100644 index 0000000000000..a6061a90e8751 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31_invalid_tags.yml @@ -0,0 +1,6 @@ +services: + _defaults: + tags: ['foo'] + + Foo\Bar: + tags: invalid diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services4.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services4.yml index 8e0987fd03a6c..073f55547330e 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services4.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services4.yml @@ -1,7 +1,8 @@ imports: - - { resource: services2.yml } + - services2.yml - { resource: services3.yml } - { resource: "../php/simple.php" } - { resource: "../ini/parameters.ini", class: Symfony\Component\DependencyInjection\Loader\IniFileLoader } - { resource: "../ini/parameters2.ini" } - { resource: "../xml/services13.xml" } + - { resource: "../xml/xml_with_wrong_ext.php", type: xml } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml index cf1a264525d9a..bfc995d44e450 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services6.yml @@ -21,6 +21,10 @@ services: another_alias_for_foo: alias: foo public: false + request: + class: Request + synthetic: true + lazy: true another_third_alias_for_foo: alias: foo decorator_service: @@ -35,3 +39,5 @@ services: new_factory1: { class: FooBarClass, factory: factory} new_factory2: { class: FooBarClass, factory: ['@baz', getClass]} new_factory3: { class: FooBarClass, factory: [BazClass, getInstance]} + new_factory4: { class: BazClass, factory: [~, getInstance]} + Acme\WithShortCutArgs: [foo, '@baz'] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml index a1fb59035855e..9efdf3e0d4d0a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml @@ -5,3 +5,13 @@ parameters: escape: '@@escapeme' values: [true, false, null, 0, 1000.3, 'true', 'false', 'null'] +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + synthetic: true + Psr\Container\ContainerInterface: + alias: service_container + public: false + Symfony\Component\DependencyInjection\ContainerInterface: + alias: service_container + public: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml index 1afd15c5307d9..a3a95deeb34c7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml @@ -4,6 +4,9 @@ parameters: foo: bar services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + synthetic: true foo: class: Bar\FooClass tags: @@ -28,6 +31,7 @@ services: foo_bar: class: '%foo_class%' shared: false + arguments: ['@deprecated_service'] method_call1: class: Bar\FooClass file: '%path%foo.php' @@ -67,6 +71,13 @@ services: configured_service: class: stdClass configurator: ['@configurator_service', configureStdClass] + configurator_service_simple: + class: ConfClass + public: false + arguments: ['bar'] + configured_service_simple: + class: stdClass + configurator: ['@configurator_service_simple', configureStdClass] decorated: class: stdClass decorator_service: @@ -93,5 +104,32 @@ services: service_from_static_method: class: Bar\FooClass factory: [Bar\FooClass, getInstance] + factory_simple: + class: SimpleFactoryClass + deprecated: The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed. + public: false + arguments: ['foo'] + factory_service_simple: + class: Bar + factory: ['@factory_simple', getInstance] + lazy_context: + class: LazyContext + arguments: [!iterator {'k1': '@foo.baz', 'k2': '@service_container'}, !iterator []] + lazy_context_ignore_invalid_ref: + class: LazyContext + arguments: [!iterator ['@foo.baz', '@?invalid'], !iterator []] + BAR: + class: stdClass + properties: { bar: '@bar' } + bar2: + class: stdClass + BAR2: + class: stdClass alias_for_foo: '@foo' alias_for_alias: '@foo' + Psr\Container\ContainerInterface: + alias: service_container + public: false + Symfony\Component\DependencyInjection\ContainerInterface: + alias: service_container + public: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_autoconfigure.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_autoconfigure.yml new file mode 100644 index 0000000000000..809c9f47ddfda --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_autoconfigure.yml @@ -0,0 +1,9 @@ + +services: + _defaults: + autoconfigure: true + + use_defaults_settings: ~ + + override_defaults_settings_to_false: + autoconfigure: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_autoconfigure_with_parent.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_autoconfigure_with_parent.yml new file mode 100644 index 0000000000000..c6e3080192a7e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_autoconfigure_with_parent.yml @@ -0,0 +1,8 @@ +services: + parent_service: + class: stdClass + + child_service: + class: stdClass + autoconfigure: true + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml new file mode 100644 index 0000000000000..48ad040406219 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml @@ -0,0 +1,16 @@ +services: + _defaults: + bind: + NonExistent: ~ + $quz: quz + $factory: factory + + bar: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar + autowire: true + bind: + Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: '@Symfony\Component\DependencyInjection\Tests\Fixtures\Bar' + $foo: [ ~ ] + + Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: + factory: [ ~, 'create' ] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_case.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_case.yml new file mode 100644 index 0000000000000..cbf158659cfd7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_case.yml @@ -0,0 +1,10 @@ +services: + bar: + class: stdClass + Bar: + class: stdClass + properties: { bar: '@bar' } + BAR: + class: Bar\FooClass + arguments: ['@Bar'] + calls: [[setBar, ['@bar']]] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_configurator_short_syntax.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_configurator_short_syntax.yml new file mode 100644 index 0000000000000..cc5c2be5720a5 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_configurator_short_syntax.yml @@ -0,0 +1,8 @@ +services: + foo_bar: + class: FooBarClass + configurator: foo_bar_configurator:configure + + foo_bar_with_static_call: + class: FooBarClass + configurator: FooBarConfigurator::configureFooBar diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_defaults_with_parent.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_defaults_with_parent.yml new file mode 100644 index 0000000000000..28dec4ce91791 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_defaults_with_parent.yml @@ -0,0 +1,10 @@ +services: + _defaults: + autowire: true + + parent_service: + class: stdClass + + child_service: + class: stdClass + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml index bcf8f31b36115..43b0c7d58a00f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_dump_load.yml @@ -1,4 +1,14 @@ services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + synthetic: true foo: + autoconfigure: true abstract: true + Psr\Container\ContainerInterface: + alias: service_container + public: false + Symfony\Component\DependencyInjection\ContainerInterface: + alias: service_container + public: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_inline.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_inline.yml new file mode 100644 index 0000000000000..14adedf32dde0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_inline.yml @@ -0,0 +1,14 @@ + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + synthetic: true + foo: + class: Class1 + arguments: [!service { class: Class2, arguments: [!service { class: Class2 }] }] + Psr\Container\ContainerInterface: + alias: service_container + public: false + Symfony\Component\DependencyInjection\ContainerInterface: + alias: service_container + public: false diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml new file mode 100644 index 0000000000000..a58cc079e455f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml @@ -0,0 +1,11 @@ +services: + _instanceof: + Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: + autowire: true + lazy: true + tags: + - { name: foo } + - { name: bar } + + Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: ~ + Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: '@Symfony\Component\DependencyInjection\Tests\Fixtures\Bar' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof_with_parent.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof_with_parent.yml new file mode 100644 index 0000000000000..fb21cdf2fb674 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof_with_parent.yml @@ -0,0 +1,11 @@ +services: + _instanceof: + FooInterface: + autowire: true + + parent_service: + class: stdClass + + child_service: + class: stdClass + parent: parent_service diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_named_args.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_named_args.yml new file mode 100644 index 0000000000000..97e310148df04 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_named_args.yml @@ -0,0 +1,10 @@ +services: + Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy: { 0: ~, $apiKey: ABCD } + + another_one: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy + arguments: + $apiKey: ABCD + Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass: ~ + calls: + - ['setApiKey', { $apiKey: '123' }] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml new file mode 100644 index 0000000000000..fb47bcb7e7a52 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_prototype.yml @@ -0,0 +1,4 @@ +services: + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\: + resource: ../Prototype + exclude: '../Prototype/{OtherDir}' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_underscore.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_underscore.yml new file mode 100644 index 0000000000000..0cdbf9032a11c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_underscore.yml @@ -0,0 +1,3 @@ +services: + _foo: + class: Foo diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/tag_name_only.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/tag_name_only.yml new file mode 100644 index 0000000000000..90180b0bb9360 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/tag_name_only.yml @@ -0,0 +1,5 @@ +services: + foo_service: + class: FooClass + tags: + - foo diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/yaml_with_wrong_ext.ini b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/yaml_with_wrong_ext.ini new file mode 100644 index 0000000000000..1395458bf72f0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/yaml_with_wrong_ext.ini @@ -0,0 +1,2 @@ +parameters: + with_wrong_ext: from yaml diff --git a/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/PhpDumper/NullDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/PhpDumper/NullDumperTest.php index cde2c147e752c..b1b9b399c3728 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/PhpDumper/NullDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/LazyProxy/PhpDumper/NullDumperTest.php @@ -28,7 +28,7 @@ public function testNullDumper() $definition = new Definition('stdClass'); $this->assertFalse($dumper->isProxyCandidate($definition)); - $this->assertSame('', $dumper->getProxyFactoryCode($definition, 'foo')); + $this->assertSame('', $dumper->getProxyFactoryCode($definition, 'foo', '(false)')); $this->assertSame('', $dumper->getProxyCode($definition)); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/LegacyContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/LegacyContainerBuilderTest.php deleted file mode 100644 index 1cb8d33b6ed4c..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/LegacyContainerBuilderTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @group legacy - */ -class LegacyContainerBuilderTest extends TestCase -{ - public function testCreateServiceFactoryMethod() - { - $builder = new ContainerBuilder(); - $builder->register('bar', 'stdClass'); - $builder->register('foo1', 'Bar\FooClass')->setFactoryClass('Bar\FooClass')->setFactoryMethod('getInstance')->addArgument(array('foo' => '%value%', '%value%' => 'foo', new Reference('bar'))); - $builder->setParameter('value', 'bar'); - $this->assertTrue($builder->get('foo1')->called, '->createService() calls the factory method to create the service instance'); - $this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', $builder->get('bar')), $builder->get('foo1')->arguments, '->createService() passes the arguments to the factory method'); - } - - public function testCreateServiceFactoryService() - { - $builder = new ContainerBuilder(); - $builder->register('baz_service')->setFactoryService('baz_factory')->setFactoryMethod('getInstance'); - $builder->register('baz_factory', 'BazClass'); - - $this->assertInstanceOf('BazClass', $builder->get('baz_service')); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/LegacyDefinitionTest.php b/src/Symfony/Component/DependencyInjection/Tests/LegacyDefinitionTest.php deleted file mode 100644 index a605729f97ad3..0000000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/LegacyDefinitionTest.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\Definition; - -/** - * @group legacy - */ -class LegacyDefinitionTest extends TestCase -{ - public function testSetGetFactoryClass() - { - $def = new Definition('stdClass'); - $this->assertNull($def->getFactoryClass()); - $this->assertSame($def, $def->setFactoryClass('stdClass2'), '->setFactoryClass() implements a fluent interface.'); - $this->assertEquals('stdClass2', $def->getFactoryClass(), '->getFactoryClass() returns current class to construct this service.'); - } - - public function testSetGetFactoryMethod() - { - $def = new Definition('stdClass'); - $this->assertNull($def->getFactoryMethod()); - $this->assertSame($def, $def->setFactoryMethod('foo'), '->setFactoryMethod() implements a fluent interface'); - $this->assertEquals('foo', $def->getFactoryMethod(), '->getFactoryMethod() returns the factory method name'); - } - - public function testSetGetFactoryService() - { - $def = new Definition('stdClass'); - $this->assertNull($def->getFactoryService()); - $this->assertSame($def, $def->setFactoryService('foo.bar'), '->setFactoryService() implements a fluent interface.'); - $this->assertEquals('foo.bar', $def->getFactoryService(), '->getFactoryService() returns current service to construct this service.'); - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php new file mode 100644 index 0000000000000..6544145c4e0e0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php @@ -0,0 +1,170 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Loader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Loader\FileLoader; +use Symfony\Component\DependencyInjection\Loader\IniFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub\DeeperBaz; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Baz; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar; + +class FileLoaderTest extends TestCase +{ + protected static $fixturesPath; + + public static function setUpBeforeClass() + { + self::$fixturesPath = realpath(__DIR__.'/../'); + } + + public function testImportWithGlobPattern() + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath)); + + $resolver = new LoaderResolver(array( + new IniFileLoader($container, new FileLocator(self::$fixturesPath.'/ini')), + new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')), + new PhpFileLoader($container, new FileLocator(self::$fixturesPath.'/php')), + new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')), + )); + + $loader->setResolver($resolver); + $loader->import('{F}ixtures/{xml,yaml}/services2.{yml,xml}'); + + $actual = $container->getParameterBag()->all(); + $expected = array( + 'a string', + 'foo' => 'bar', + 'values' => array( + 0, + 'integer' => 4, + 100 => null, + 'true', + true, + false, + 'on', + 'off', + 'float' => 1.3, + 1000.3, + 'a string', + array('foo', 'bar'), + ), + 'mixedcase' => array('MixedCaseKey' => 'value'), + 'constant' => PHP_EOL, + 'bar' => '%foo%', + 'escape' => '@escapeme', + 'foo_bar' => new Reference('foo_bar'), + ); + + $this->assertEquals(array_keys($expected), array_keys($actual), '->load() imports and merges imported files'); + } + + public function testRegisterClasses() + { + $container = new ContainerBuilder(); + $container->setParameter('sub_dir', 'Sub'); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\\', 'Prototype/%sub_dir%/*'); + + $this->assertEquals( + array('service_container', Bar::class), + array_keys($container->getDefinitions()) + ); + } + + public function testRegisterClassesWithExclude() + { + $container = new ContainerBuilder(); + $container->setParameter('other_dir', 'OtherDir'); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + $loader->registerClasses( + new Definition(), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', + 'Prototype/*', + // load everything, except OtherDir/AnotherSub & Foo.php + 'Prototype/{%other_dir%/AnotherSub,Foo.php}' + ); + + $this->assertTrue($container->has(Bar::class)); + $this->assertTrue($container->has(Baz::class)); + $this->assertFalse($container->has(Foo::class)); + $this->assertFalse($container->has(DeeperBaz::class)); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /Expected to find class "Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\Prototype\\Bar" in file ".+" while importing services from resource "Prototype\/Sub\/\*", but it was not found\! Check the namespace prefix used with the resource/ + */ + public function testRegisterClassesWithBadPrefix() + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + // the Sub is missing from namespace prefix + $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/Sub/*'); + } + + /** + * @dataProvider getIncompatibleExcludeTests + */ + public function testRegisterClassesWithIncompatibleExclude($resourcePattern, $excludePattern) + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + + try { + $loader->registerClasses( + new Definition(), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', + $resourcePattern, + $excludePattern + ); + } catch (InvalidArgumentException $e) { + $this->assertEquals( + sprintf('Invalid "exclude" pattern when importing classes for "Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s)', $excludePattern, $resourcePattern), + $e->getMessage() + ); + } + } + + public function getIncompatibleExcludeTests() + { + yield array('Prototype/*', 'yaml/*', false); + yield array('Prototype/OtherDir/*', 'Prototype/*', false); + } +} + +class TestFileLoader extends FileLoader +{ + public function load($resource, $type = null) + { + return $resource; + } + + public function supports($resource, $type = null) + { + return false; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php index 70fbf306070c6..84c934c08614f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/IniFileLoaderTest.php @@ -18,20 +18,13 @@ class IniFileLoaderTest extends TestCase { - protected static $fixturesPath; - protected $container; protected $loader; - public static function setUpBeforeClass() - { - self::$fixturesPath = realpath(__DIR__.'/../Fixtures/'); - } - protected function setUp() { $this->container = new ContainerBuilder(); - $this->loader = new IniFileLoader($this->container, new FileLocator(self::$fixturesPath.'/ini')); + $this->loader = new IniFileLoader($this->container, new FileLocator(realpath(__DIR__.'/../Fixtures/').'/ini')); } public function testIniFileCanBeLoaded() @@ -40,6 +33,63 @@ public function testIniFileCanBeLoaded() $this->assertEquals(array('foo' => 'bar', 'bar' => '%foo%'), $this->container->getParameterBag()->all(), '->load() takes a single file name as its first argument'); } + /** + * @dataProvider getTypeConversions + */ + public function testTypeConversions($key, $value, $supported) + { + $this->loader->load('types.ini'); + $parameters = $this->container->getParameterBag()->all(); + $this->assertSame($value, $parameters[$key], '->load() converts values to PHP types'); + } + + /** + * @dataProvider getTypeConversions + * This test illustrates where our conversions differs from INI_SCANNER_TYPED introduced in PHP 5.6.1 + */ + public function testTypeConversionsWithNativePhp($key, $value, $supported) + { + if (!$supported) { + $this->markTestSkipped(sprintf('Converting the value "%s" to "%s" is not supported by the IniFileLoader.', $key, $value)); + } + + $this->loader->load('types.ini'); + $expected = parse_ini_file(__DIR__.'/../Fixtures/ini/types.ini', true, INI_SCANNER_TYPED); + $this->assertSame($value, $expected['parameters'][$key], '->load() converts values to PHP types'); + } + + public function getTypeConversions() + { + return array( + array('true_comment', true, true), + array('true', true, true), + array('false', false, true), + array('on', true, true), + array('off', false, true), + array('yes', true, true), + array('no', false, true), + array('none', false, true), + array('null', null, true), + array('constant', PHP_VERSION, true), + array('12', 12, true), + array('12_string', '12', true), + array('12_comment', 12, true), + array('12_string_comment', '12', true), + array('12_string_comment_again', '12', true), + array('-12', -12, true), + array('1', 1, true), + array('0', 0, true), + array('0b0110', bindec('0b0110'), false), // not supported by INI_SCANNER_TYPED + array('11112222333344445555', '1111,2222,3333,4444,5555', true), + array('0777', 0777, false), // not supported by INI_SCANNER_TYPED + array('255', 0xFF, false), // not supported by INI_SCANNER_TYPED + array('100.0', 1e2, false), // not supported by INI_SCANNER_TYPED + array('-120.0', -1.2E2, false), // not supported by INI_SCANNER_TYPED + array('-10100.1', -10100.1, false), // not supported by INI_SCANNER_TYPED + array('-10,100.1', '-10,100.1', true), + ); + } + /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage The file "foo.ini" does not exist (in: @@ -50,7 +100,7 @@ public function testExceptionIsRaisedWhenIniFileDoesNotExist() } /** - * @expectedException \InvalidArgumentException + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException * @expectedExceptionMessage The "nonvalid.ini" file is not valid. */ public function testExceptionIsRaisedWhenIniFileCannotBeParsed() @@ -58,11 +108,21 @@ public function testExceptionIsRaisedWhenIniFileCannotBeParsed() @$this->loader->load('nonvalid.ini'); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage The "almostvalid.ini" file is not valid. + */ + public function testExceptionIsRaisedWhenIniFileIsAlmostValid() + { + @$this->loader->load('almostvalid.ini'); + } + public function testSupports() { $loader = new IniFileLoader(new ContainerBuilder(), new FileLocator()); $this->assertTrue($loader->supports('foo.ini'), '->supports() returns true if the resource is loadable'); - $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable'); + $this->assertFalse($loader->supports('foo.foo'), '->supports() returns false if the resource is not loadable'); + $this->assertTrue($loader->supports('with_wrong_ext.yml', 'ini'), '->supports() returns true if the resource with forced type is loadable'); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/LoaderResolverTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/LoaderResolverTest.php new file mode 100644 index 0000000000000..cb2d6ddcc248f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/LoaderResolverTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Loader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\ClosureLoader; +use Symfony\Component\DependencyInjection\Loader\IniFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; + +class LoaderResolverTest extends TestCase +{ + private static $fixturesPath; + + /** @var LoaderResolver */ + private $resolver; + + protected function setUp() + { + self::$fixturesPath = realpath(__DIR__.'/../Fixtures/'); + + $container = new ContainerBuilder(); + $this->resolver = new LoaderResolver(array( + new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')), + new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')), + new IniFileLoader($container, new FileLocator(self::$fixturesPath.'/ini')), + new PhpFileLoader($container, new FileLocator(self::$fixturesPath.'/php')), + new ClosureLoader($container), + )); + } + + public function provideResourcesToLoad() + { + return array( + array('ini_with_wrong_ext.xml', 'ini', IniFileLoader::class), + array('xml_with_wrong_ext.php', 'xml', XmlFileLoader::class), + array('php_with_wrong_ext.yml', 'php', PhpFileLoader::class), + array('yaml_with_wrong_ext.ini', 'yaml', YamlFileLoader::class), + ); + } + + /** + * @dataProvider provideResourcesToLoad + */ + public function testResolvesForcedType($resource, $type, $expectedClass) + { + $this->assertInstanceOf($expectedClass, $this->resolver->resolve($resource, $type)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index b936bf5e63ce4..4e01e6fae9c25 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -23,7 +23,8 @@ public function testSupports() $loader = new PhpFileLoader(new ContainerBuilder(), new FileLocator()); $this->assertTrue($loader->supports('foo.php'), '->supports() returns true if the resource is loadable'); - $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable'); + $this->assertFalse($loader->supports('foo.foo'), '->supports() returns false if the resource is not loadable'); + $this->assertTrue($loader->supports('with_wrong_ext.yml', 'php'), '->supports() returns true if the resource with forced type is loadable'); } public function testLoad() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 81023d28901ba..fed070e703da6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Loader; use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -20,6 +20,13 @@ use Symfony\Component\DependencyInjection\Loader\IniFileLoader; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\GlobResource; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar; +use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; +use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; use Symfony\Component\ExpressionLanguage\Expression; class XmlFileLoaderTest extends TestCase @@ -162,9 +169,11 @@ public function testLoadImports() 'bar' => '%foo%', 'imported_from_ini' => true, 'imported_from_yaml' => true, + 'with_wrong_ext' => 'from yaml', ); $this->assertEquals(array_keys($expected), array_keys($actual), '->load() imports and merges imported files'); + $this->assertTrue($actual['imported_from_ini']); // Bad import throws no exception due to ignore_errors value. $loader->load('services4_bad_import.xml'); @@ -205,16 +214,6 @@ public function testLoadAnonymousServices() $this->assertEquals('BuzClass', $inner->getClass(), '->load() uses the same configuration as for the anonymous ones'); $this->assertFalse($inner->isPublic()); - // "wild" service - $service = $container->findTaggedServiceIds('biz_tag'); - $this->assertCount(1, $service); - - foreach ($service as $id => $tag) { - $service = $container->getDefinition($id); - } - $this->assertEquals('BizClass', $service->getClass(), '->load() uses the same configuration as for the anonymous ones'); - $this->assertTrue($service->isPublic()); - // anonymous services are shared when using decoration definitions $container->compile(); $services = $container->getDefinitions(); @@ -224,26 +223,25 @@ public function testLoadAnonymousServices() } /** - * @group legacy + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Top-level services must have "id" attribute, none found in */ - public function testLegacyLoadServices() + public function testLoadAnonymousServicesWithoutId() { $container = new ContainerBuilder(); $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); - $loader->load('legacy-services6.xml'); - $services = $container->getDefinitions(); - $this->assertEquals('FooClass', $services['constructor']->getClass()); - $this->assertEquals('getInstance', $services['constructor']->getFactoryMethod()); - $this->assertNull($services['factory_service']->getClass()); - $this->assertEquals('baz_factory', $services['factory_service']->getFactoryService()); - $this->assertEquals('getInstance', $services['factory_service']->getFactoryMethod()); - $this->assertEquals('container', $services['scope.container']->getScope()); - $this->assertEquals('custom', $services['scope.custom']->getScope()); - $this->assertEquals('prototype', $services['scope.prototype']->getScope()); - $this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag'); - $this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag'); - $this->assertTrue($services['request']->isLazy(), '->load() parses the lazy flag'); - $this->assertNull($services['request']->getDecoratedService()); + $loader->load('services_without_id.xml'); + } + + public function testLoadAnonymousNestedServices() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('nested_service_without_id.xml'); + + $this->assertTrue($container->hasDefinition('FooClass')); + $arguments = $container->getDefinition('FooClass')->getArguments(); + $this->assertInstanceOf(Reference::class, array_shift($arguments)); } public function testLoadServices() @@ -259,13 +257,14 @@ public function testLoadServices() $this->assertEquals('%path%/foo.php', $services['file']->getFile(), '->load() parses the file tag'); $this->assertEquals(array('foo', new Reference('foo'), array(true, false)), $services['arguments']->getArguments(), '->load() parses the argument tags'); $this->assertEquals('sc_configure', $services['configurator1']->getConfigurator(), '->load() parses the configurator tag'); - $this->assertEquals(array(new Reference('baz', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag'); + $this->assertEquals(array(new Reference('baz'), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag'); $this->assertEquals(array('BazClass', 'configureStatic'), $services['configurator3']->getConfigurator(), '->load() parses the configurator tag'); $this->assertEquals(array(array('setBar', array()), array('setBar', array(new Expression('service("foo").foo() ~ (container.hasParameter("foo") ? parameter("foo") : "default")')))), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag'); $this->assertEquals(array(array('setBar', array('foo', new Reference('foo'), array(true, false)))), $services['method_call2']->getMethodCalls(), '->load() parses the method_call tag'); $this->assertEquals('factory', $services['new_factory1']->getFactory(), '->load() parses the factory tag'); - $this->assertEquals(array(new Reference('baz', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false), 'getClass'), $services['new_factory2']->getFactory(), '->load() parses the factory tag'); + $this->assertEquals(array(new Reference('baz'), 'getClass'), $services['new_factory2']->getFactory(), '->load() parses the factory tag'); $this->assertEquals(array('BazClass', 'getInstance'), $services['new_factory3']->getFactory(), '->load() parses the factory tag'); + $this->assertSame(array(null, 'getInstance'), $services['new_factory4']->getFactory(), '->load() accepts factory tag without class'); $aliases = $container->getAliases(); $this->assertTrue(isset($aliases['alias_for_foo']), '->load() parses elements'); @@ -280,6 +279,17 @@ public function testLoadServices() $this->assertEquals(array('decorated', 'decorated.pif-pouf', 5), $services['decorator_service_with_name_and_priority']->getDecoratedService()); } + public function testParsesIteratorArgument() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services9.xml'); + + $lazyDefinition = $container->getDefinition('lazy_context'); + + $this->assertEquals(array(new IteratorArgument(array('k1' => new Reference('foo.baz'), 'k2' => new Reference('service_container'))), new IteratorArgument(array())), $lazyDefinition->getArguments(), '->load() parses lazy arguments'); + } + public function testParsesTags() { $container = new ContainerBuilder(); @@ -466,7 +476,8 @@ public function testSupports() $loader = new XmlFileLoader(new ContainerBuilder(), new FileLocator()); $this->assertTrue($loader->supports('foo.xml'), '->supports() returns true if the resource is loadable'); - $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable'); + $this->assertFalse($loader->supports('foo.foo'), '->supports() returns false if the resource is not loadable'); + $this->assertTrue($loader->supports('with_wrong_ext.yml', 'xml'), '->supports() returns true if the resource with forced type is loadable'); } public function testNoNamingConflictsForAnonymousServices() @@ -476,11 +487,11 @@ public function testNoNamingConflictsForAnonymousServices() $loader1 = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml/extension1')); $loader1->load('services.xml'); $services = $container->getDefinitions(); - $this->assertCount(2, $services, '->load() attributes unique ids to anonymous services'); + $this->assertCount(3, $services, '->load() attributes unique ids to anonymous services'); $loader2 = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml/extension2')); $loader2->load('services.xml'); $services = $container->getDefinitions(); - $this->assertCount(4, $services, '->load() attributes unique ids to anonymous services'); + $this->assertCount(5, $services, '->load() attributes unique ids to anonymous services'); $services = $container->getDefinitions(); $args1 = $services['extension1.foo']->getArguments(); @@ -541,42 +552,82 @@ public function testLoadInlinedServices() $foo = $container->getDefinition('foo'); $fooFactory = $foo->getFactory(); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Definition', $fooFactory[0]); - $this->assertSame('FooFactory', $fooFactory[0]->getClass()); + $this->assertInstanceOf(Reference::class, $fooFactory[0]); + $this->assertTrue($container->has((string) $fooFactory[0])); + $fooFactoryDefinition = $container->getDefinition((string) $fooFactory[0]); + $this->assertSame('FooFactory', $fooFactoryDefinition->getClass()); $this->assertSame('createFoo', $fooFactory[1]); - $fooFactoryFactory = $fooFactory[0]->getFactory(); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Definition', $fooFactoryFactory[0]); - $this->assertSame('Foobar', $fooFactoryFactory[0]->getClass()); + $fooFactoryFactory = $fooFactoryDefinition->getFactory(); + $this->assertInstanceOf(Reference::class, $fooFactoryFactory[0]); + $this->assertTrue($container->has((string) $fooFactoryFactory[0])); + $this->assertSame('Foobar', $container->getDefinition((string) $fooFactoryFactory[0])->getClass()); $this->assertSame('createFooFactory', $fooFactoryFactory[1]); $fooConfigurator = $foo->getConfigurator(); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Definition', $fooConfigurator[0]); - $this->assertSame('Bar', $fooConfigurator[0]->getClass()); + $this->assertInstanceOf(Reference::class, $fooConfigurator[0]); + $this->assertTrue($container->has((string) $fooConfigurator[0])); + $fooConfiguratorDefinition = $container->getDefinition((string) $fooConfigurator[0]); + $this->assertSame('Bar', $fooConfiguratorDefinition->getClass()); $this->assertSame('configureFoo', $fooConfigurator[1]); - $barConfigurator = $fooConfigurator[0]->getConfigurator(); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Definition', $barConfigurator[0]); - $this->assertSame('Baz', $barConfigurator[0]->getClass()); + $barConfigurator = $fooConfiguratorDefinition->getConfigurator(); + $this->assertInstanceOf(Reference::class, $barConfigurator[0]); + $this->assertSame('Baz', $container->getDefinition((string) $barConfigurator[0])->getClass()); $this->assertSame('configureBar', $barConfigurator[1]); } - public function testType() + public function testAutowire() { $container = new ContainerBuilder(); $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); - $loader->load('services22.xml'); + $loader->load('services23.xml'); - $this->assertEquals(array('Bar', 'Baz'), $container->getDefinition('foo')->getAutowiringTypes()); + $this->assertTrue($container->getDefinition('bar')->isAutowired()); } - public function testAutowire() + public function testClassFromId() { $container = new ContainerBuilder(); $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); - $loader->load('services23.xml'); + $loader->load('class_from_id.xml'); + $container->compile(); - $this->assertTrue($container->getDefinition('bar')->isAutowired()); + $this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass()); + } + + public function testPrototype() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_prototype.xml'); + + $ids = array_keys($container->getDefinitions()); + sort($ids); + $this->assertSame(array(Prototype\Foo::class, Prototype\Sub\Bar::class, 'service_container'), $ids); + + $resources = $container->getResources(); + + $fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR; + $this->assertTrue(false !== array_search(new FileResource($fixturesDir.'xml'.DIRECTORY_SEPARATOR.'services_prototype.xml'), $resources)); + $this->assertTrue(false !== array_search(new GlobResource($fixturesDir.'Prototype', '/*', true), $resources)); + $resources = array_map('strval', $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo', $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar', $resources); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid attribute "class" defined for alias "bar" in + */ + public function testAliasDefinitionContainsUnsupportedElements() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + + $loader->load('invalid_alias_definition.xml'); + + $this->assertTrue($container->has('bar')); } public function testArgumentWithKeyOutsideCollection() @@ -587,4 +638,161 @@ public function testArgumentWithKeyOutsideCollection() $this->assertSame(array('type' => 'foo', 'bar'), $container->getDefinition('foo')->getArguments()); } + + public function testDefaults() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services28.xml'); + + $this->assertFalse($container->getDefinition('with_defaults')->isPublic()); + $this->assertSame(array('foo' => array(array())), $container->getDefinition('with_defaults')->getTags()); + $this->assertTrue($container->getDefinition('with_defaults')->isAutowired()); + $this->assertArrayNotHasKey('public', $container->getDefinition('with_defaults')->getChanges()); + $this->assertArrayNotHasKey('autowire', $container->getDefinition('with_defaults')->getChanges()); + + $container->compile(); + + $this->assertTrue($container->getDefinition('no_defaults')->isPublic()); + + $this->assertSame(array('foo' => array(array())), $container->getDefinition('no_defaults')->getTags()); + + $this->assertFalse($container->getDefinition('no_defaults')->isAutowired()); + + $this->assertTrue($container->getDefinition('child_def')->isPublic()); + $this->assertSame(array('foo' => array(array())), $container->getDefinition('child_def')->getTags()); + $this->assertFalse($container->getDefinition('child_def')->isAutowired()); + + $definitions = $container->getDefinitions(); + $this->assertSame('service_container', key($definitions)); + + array_shift($definitions); + $anonymous = current($definitions); + $this->assertSame('bar', key($definitions)); + $this->assertTrue($anonymous->isPublic()); + $this->assertTrue($anonymous->isAutowired()); + $this->assertSame(array('foo' => array(array())), $anonymous->getTags()); + } + + public function testNamedArguments() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_named_args.xml'); + + $this->assertEquals(array('$apiKey' => 'ABCD', CaseSensitiveClass::class => null), $container->getDefinition(NamedArgumentsDummy::class)->getArguments()); + + $container->compile(); + + $this->assertEquals(array(null, 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments()); + $this->assertEquals(array(array('setApiKey', array('123'))), $container->getDefinition(NamedArgumentsDummy::class)->getMethodCalls()); + } + + public function testInstanceof() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_instanceof.xml'); + $container->compile(); + + $definition = $container->getDefinition(Bar::class); + $this->assertTrue($definition->isAutowired()); + $this->assertTrue($definition->isLazy()); + $this->assertSame(array('foo' => array(array()), 'bar' => array(array())), $definition->getTags()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage The service "child_service" cannot use the "parent" option in the same file where "instanceof" configuration is defined as using both is not supported. Move your child definitions to a separate file. + */ + public function testInstanceOfAndChildDefinitionNotAllowed() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_instanceof_with_parent.xml'); + $container->compile(); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage The service "child_service" cannot have a "parent" and also have "autoconfigure". Try setting autoconfigure="false" for the service. + */ + public function testAutoConfigureAndChildDefinitionNotAllowed() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_autoconfigure_with_parent.xml'); + $container->compile(); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Attribute "autowire" on service "child_service" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly. + */ + public function testDefaultsAndChildDefinitionNotAllowed() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_defaults_with_parent.xml'); + $container->compile(); + } + + public function testAutoConfigureInstanceof() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_autoconfigure.xml'); + + $this->assertTrue($container->getDefinition('use_defaults_settings')->isAutoconfigured()); + $this->assertFalse($container->getDefinition('override_defaults_settings_to_false')->isAutoconfigured()); + } + + public function testCaseSensitivity() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_case.xml'); + $container->compile(); + + $this->assertTrue($container->has('bar')); + $this->assertTrue($container->has('BAR')); + $this->assertFalse($container->has('baR')); + $this->assertNotSame($container->get('BAR'), $container->get('bar')); + $this->assertSame($container->get('BAR')->arguments->bar, $container->get('bar')); + $this->assertSame($container->get('BAR')->bar, $container->get('bar')); + } + + public function testBindings() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_bindings.xml'); + $container->compile(); + + $definition = $container->getDefinition('bar'); + $this->assertEquals(array( + 'NonExistent' => null, + BarInterface::class => new Reference(Bar::class), + '$foo' => array(null), + '$quz' => 'quz', + '$factory' => 'factory', + ), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings())); + $this->assertEquals(array( + 'quz', + null, + new Reference(Bar::class), + array(null), + ), $definition->getArguments()); + + $definition = $container->getDefinition(Bar::class); + $this->assertEquals(array( + null, + 'factory', + ), $definition->getArguments()); + $this->assertEquals(array( + 'NonExistent' => null, + '$quz' => 'quz', + '$factory' => 'factory', + ), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings())); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 236bd4da01884..b026eb3a340f8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Loader; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -20,6 +21,13 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Resource\GlobResource; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar; +use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; +use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; use Symfony\Component\ExpressionLanguage\Expression; class YamlFileLoaderTest extends TestCase @@ -93,7 +101,7 @@ public function testLoadParameters() $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); $loader->load('services2.yml'); - $this->assertEquals(array('foo' => 'bar', 'mixedcase' => array('MixedCaseKey' => 'value'), 'values' => array(true, false, 0, 1000.3), 'bar' => 'foo', 'escape' => '@escapeme', 'foo_bar' => new Reference('foo_bar')), $container->getParameterBag()->all(), '->load() converts YAML keys to lowercase'); + $this->assertEquals(array('foo' => 'bar', 'mixedcase' => array('MixedCaseKey' => 'value'), 'values' => array(true, false, 0, 1000.3, PHP_INT_MAX), 'bar' => 'foo', 'escape' => '@escapeme', 'foo_bar' => new Reference('foo_bar')), $container->getParameterBag()->all(), '->load() converts YAML keys to lowercase'); } public function testLoadImports() @@ -109,36 +117,24 @@ public function testLoadImports() $loader->load('services4.yml'); $actual = $container->getParameterBag()->all(); - $expected = array('foo' => 'bar', 'values' => array(true, false), 'bar' => '%foo%', 'escape' => '@escapeme', 'foo_bar' => new Reference('foo_bar'), 'mixedcase' => array('MixedCaseKey' => 'value'), 'imported_from_ini' => true, 'imported_from_xml' => true); + $expected = array( + 'foo' => 'bar', + 'values' => array(true, false, PHP_INT_MAX), + 'bar' => '%foo%', + 'escape' => '@escapeme', + 'foo_bar' => new Reference('foo_bar'), + 'mixedcase' => array('MixedCaseKey' => 'value'), + 'imported_from_ini' => true, + 'imported_from_xml' => true, + 'with_wrong_ext' => 'from yaml', + ); $this->assertEquals(array_keys($expected), array_keys($actual), '->load() imports and merges imported files'); + $this->assertTrue($actual['imported_from_ini']); // Bad import throws no exception due to ignore_errors value. $loader->load('services4_bad_import.yml'); } - /** - * @group legacy - */ - public function testLegacyLoadServices() - { - $container = new ContainerBuilder(); - $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('legacy-services6.yml'); - $services = $container->getDefinitions(); - $this->assertEquals('FooClass', $services['constructor']->getClass()); - $this->assertEquals('getInstance', $services['constructor']->getFactoryMethod()); - $this->assertEquals('BazClass', $services['factory_service']->getClass()); - $this->assertEquals('baz_factory', $services['factory_service']->getFactoryService()); - $this->assertEquals('getInstance', $services['factory_service']->getFactoryMethod()); - $this->assertEquals('container', $services['scope.container']->getScope()); - $this->assertEquals('custom', $services['scope.custom']->getScope()); - $this->assertEquals('prototype', $services['scope.prototype']->getScope()); - $this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag'); - $this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag'); - $this->assertTrue($services['request']->isLazy(), '->load() parses the lazy flag'); - $this->assertNull($services['request']->getDecoratedService()); - } - public function testLoadServices() { $container = new ContainerBuilder(); @@ -159,6 +155,8 @@ public function testLoadServices() $this->assertEquals('factory', $services['new_factory1']->getFactory(), '->load() parses the factory tag'); $this->assertEquals(array(new Reference('baz'), 'getClass'), $services['new_factory2']->getFactory(), '->load() parses the factory tag'); $this->assertEquals(array('BazClass', 'getInstance'), $services['new_factory3']->getFactory(), '->load() parses the factory tag'); + $this->assertSame(array(null, 'getInstance'), $services['new_factory4']->getFactory(), '->load() accepts factory tag without class'); + $this->assertEquals(array('foo', new Reference('baz')), $services['Acme\WithShortCutArgs']->getArguments(), '->load() parses short service definition'); $aliases = $container->getAliases(); $this->assertTrue(isset($aliases['alias_for_foo']), '->load() parses aliases'); @@ -167,6 +165,9 @@ public function testLoadServices() $this->assertTrue(isset($aliases['another_alias_for_foo'])); $this->assertEquals('foo', (string) $aliases['another_alias_for_foo']); $this->assertFalse($aliases['another_alias_for_foo']->isPublic()); + $this->assertTrue(isset($aliases['another_third_alias_for_foo'])); + $this->assertEquals('foo', (string) $aliases['another_third_alias_for_foo']); + $this->assertTrue($aliases['another_third_alias_for_foo']->isPublic()); $this->assertEquals(array('decorated', null, 0), $services['decorator_service']->getDecoratedService()); $this->assertEquals(array('decorated', 'decorated.pif-pouf', 0), $services['decorator_service_with_name']->getDecoratedService()); @@ -184,6 +185,17 @@ public function testLoadFactoryShortSyntax() $this->assertEquals(array('FooBacFactory', 'createFooBar'), $services['factory_with_static_call']->getFactory(), '->load() parses the factory tag with Class::method'); } + public function testLoadConfiguratorShortSyntax() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_configurator_short_syntax.yml'); + $services = $container->getDefinitions(); + + $this->assertEquals(array(new Reference('foo_bar_configurator'), 'configure'), $services['foo_bar']->getConfigurator(), '->load() parses the configurator tag with service:method'); + $this->assertEquals(array('FooBarConfigurator', 'configureFooBar'), $services['foo_bar_with_static_call']->getConfigurator(), '->load() parses the configurator tag with Class::method'); + } + public function testExtensions() { $container = new ContainerBuilder(); @@ -215,7 +227,9 @@ public function testSupports() $this->assertTrue($loader->supports('foo.yml'), '->supports() returns true if the resource is loadable'); $this->assertTrue($loader->supports('foo.yaml'), '->supports() returns true if the resource is loadable'); - $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable'); + $this->assertFalse($loader->supports('foo.foo'), '->supports() returns false if the resource is not loadable'); + $this->assertTrue($loader->supports('with_wrong_ext.xml', 'yml'), '->supports() returns true if the resource with forced type is loadable'); + $this->assertTrue($loader->supports('with_wrong_ext.xml', 'yaml'), '->supports() returns true if the resource with forced type is loadable'); } public function testNonArrayTagsThrowsException() @@ -230,16 +244,6 @@ public function testNonArrayTagsThrowsException() } } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage A "tags" entry must be an array for service - */ - public function testNonArrayTagThrowsException() - { - $loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('badtag4.yml'); - } - public function testTagWithoutNameThrowsException() { $loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml')); @@ -252,6 +256,15 @@ public function testTagWithoutNameThrowsException() } } + public function testNameOnlyTagsAreAllowedAsString() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('tag_name_only.yml'); + + $this->assertCount(1, $container->getDefinition('foo_service')->getTag('foo')); + } + public function testTagWithAttributeArrayThrowsException() { $loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml')); @@ -296,50 +309,383 @@ public function testTagWithNonStringNameThrowsException() $loader->load('tag_name_no_string.yml'); } + public function testParsesIteratorArgument() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services9.yml'); + + $lazyDefinition = $container->getDefinition('lazy_context'); + + $this->assertEquals(array(new IteratorArgument(array('k1' => new Reference('foo.baz'), 'k2' => new Reference('service_container'))), new IteratorArgument(array())), $lazyDefinition->getArguments(), '->load() parses lazy arguments'); + } + + public function testAutowire() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services23.yml'); + + $this->assertTrue($container->getDefinition('bar_service')->isAutowired()); + } + + public function testClassFromId() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('class_from_id.yml'); + $container->compile(); + + $this->assertEquals(CaseSensitiveClass::class, $container->getDefinition(CaseSensitiveClass::class)->getClass()); + } + + public function testPrototype() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_prototype.yml'); + + $ids = array_keys($container->getDefinitions()); + sort($ids); + $this->assertSame(array(Prototype\Foo::class, Prototype\Sub\Bar::class, 'service_container'), $ids); + + $resources = $container->getResources(); + + $fixturesDir = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR; + $this->assertTrue(false !== array_search(new FileResource($fixturesDir.'yaml'.DIRECTORY_SEPARATOR.'services_prototype.yml'), $resources)); + $this->assertTrue(false !== array_search(new GlobResource($fixturesDir.'Prototype', '', true), $resources)); + $resources = array_map('strval', $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo', $resources); + $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar', $resources); + } + + public function testDefaults() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services28.yml'); + + $this->assertFalse($container->getDefinition('with_defaults')->isPublic()); + $this->assertSame(array('foo' => array(array())), $container->getDefinition('with_defaults')->getTags()); + $this->assertTrue($container->getDefinition('with_defaults')->isAutowired()); + $this->assertArrayNotHasKey('public', $container->getDefinition('with_defaults')->getChanges()); + $this->assertArrayNotHasKey('autowire', $container->getDefinition('with_defaults')->getChanges()); + + $this->assertFalse($container->getAlias('with_defaults_aliased')->isPublic()); + $this->assertFalse($container->getAlias('with_defaults_aliased_short')->isPublic()); + + $this->assertFalse($container->getDefinition('Acme\WithShortCutArgs')->isPublic()); + $this->assertSame(array('foo' => array(array())), $container->getDefinition('Acme\WithShortCutArgs')->getTags()); + $this->assertTrue($container->getDefinition('Acme\WithShortCutArgs')->isAutowired()); + + $container->compile(); + + $this->assertTrue($container->getDefinition('with_null')->isPublic()); + $this->assertTrue($container->getDefinition('no_defaults')->isPublic()); + + // foo tag is inherited from defaults + $this->assertSame(array('foo' => array(array())), $container->getDefinition('with_null')->getTags()); + $this->assertSame(array('foo' => array(array())), $container->getDefinition('no_defaults')->getTags()); + + $this->assertTrue($container->getDefinition('with_null')->isAutowired()); + $this->assertFalse($container->getDefinition('no_defaults')->isAutowired()); + + $this->assertTrue($container->getDefinition('child_def')->isPublic()); + $this->assertSame(array('foo' => array(array())), $container->getDefinition('child_def')->getTags()); + $this->assertFalse($container->getDefinition('child_def')->isAutowired()); + } + + public function testNamedArguments() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_named_args.yml'); + + $this->assertEquals(array(null, '$apiKey' => 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments()); + $this->assertEquals(array('$apiKey' => 'ABCD', CaseSensitiveClass::class => null), $container->getDefinition('another_one')->getArguments()); + + $container->compile(); + + $this->assertEquals(array(null, 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments()); + $this->assertEquals(array(null, 'ABCD'), $container->getDefinition('another_one')->getArguments()); + $this->assertEquals(array(array('setApiKey', array('123'))), $container->getDefinition('another_one')->getMethodCalls()); + } + + public function testInstanceof() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_instanceof.yml'); + $container->compile(); + + $definition = $container->getDefinition(Bar::class); + $this->assertTrue($definition->isAutowired()); + $this->assertTrue($definition->isLazy()); + $this->assertSame(array('foo' => array(array()), 'bar' => array(array())), $definition->getTags()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage The service "child_service" cannot use the "parent" option in the same file where "_instanceof" configuration is defined as using both is not supported. Move your child definitions to a separate file. + */ + public function testInstanceOfAndChildDefinitionNotAllowed() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_instanceof_with_parent.yml'); + $container->compile(); + } + /** * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage The service "child_service" cannot have a "parent" and also have "autoconfigure". Try setting "autoconfigure: false" for the service. */ - public function testTypesNotArray() + public function testAutoConfigureAndChildDefinitionNotAllowed() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_autoconfigure_with_parent.yml'); + $container->compile(); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Attribute "autowire" on service "child_service" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly. + */ + public function testDefaultsAndChildDefinitionNotAllowed() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_defaults_with_parent.yml'); + $container->compile(); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage The value of the "decorates" option for the "bar" service must be the id of the service without the "@" prefix (replace "@foo" with "foo"). + */ + public function testDecoratedServicesWithWrongSyntaxThrowsException() { $loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('bad_types1.yml'); + $loader->load('bad_decorates.yml'); } /** * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /Parameter "tags" must be an array for service "Foo\\Bar" in .+services31_invalid_tags\.yml\. Check your YAML syntax./ */ - public function testTypeNotString() + public function testInvalidTagsWithDefaults() { $loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('bad_types2.yml'); + $loader->load('services31_invalid_tags.yml'); } - public function testTypes() + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessage Service names that start with an underscore are reserved. Rename the "_foo" service or define it in XML instead. + */ + public function testUnderscoreServiceId() { $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('services22.yml'); + $loader->load('services_underscore.yml'); + } - $this->assertEquals(array('Foo', 'Bar'), $container->getDefinition('foo_service')->getAutowiringTypes()); - $this->assertEquals(array('Foo'), $container->getDefinition('baz_service')->getAutowiringTypes()); + public function testAnonymousServices() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('anonymous_services.yml'); + + $definition = $container->getDefinition('Foo'); + $this->assertTrue($definition->isAutowired()); + + // Anonymous service in an argument + $args = $definition->getArguments(); + $this->assertCount(1, $args); + $this->assertInstanceOf(Reference::class, $args[0]); + $this->assertTrue($container->has((string) $args[0])); + $this->assertRegExp('/^\d+_Bar[._A-Za-z0-9]{7}$/', (string) $args[0]); + + $anonymous = $container->getDefinition((string) $args[0]); + $this->assertEquals('Bar', $anonymous->getClass()); + $this->assertFalse($anonymous->isPublic()); + $this->assertTrue($anonymous->isAutowired()); + + // Anonymous service in a callable + $factory = $definition->getFactory(); + $this->assertInternalType('array', $factory); + $this->assertInstanceOf(Reference::class, $factory[0]); + $this->assertTrue($container->has((string) $factory[0])); + $this->assertRegExp('/^\d+_Quz[._A-Za-z0-9]{7}$/', (string) $factory[0]); + $this->assertEquals('constructFoo', $factory[1]); + + $anonymous = $container->getDefinition((string) $factory[0]); + $this->assertEquals('Quz', $anonymous->getClass()); + $this->assertFalse($anonymous->isPublic()); + $this->assertFalse($anonymous->isAutowired()); } - public function testAutowire() + public function testAnonymousServicesInDifferentFilesWithSameNameDoNotConflict() + { + $container = new ContainerBuilder(); + + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml/foo')); + $loader->load('services.yml'); + + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml/bar')); + $loader->load('services.yml'); + + $this->assertCount(5, $container->getDefinitions()); + } + + public function testAnonymousServicesInInstanceof() { $container = new ContainerBuilder(); $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('services23.yml'); + $loader->load('anonymous_services_in_instanceof.yml'); - $this->assertTrue($container->getDefinition('bar_service')->isAutowired()); + $definition = $container->getDefinition('Dummy'); + + $instanceof = $definition->getInstanceofConditionals(); + $this->assertCount(3, $instanceof); + $this->assertArrayHasKey('DummyInterface', $instanceof); + + $args = $instanceof['DummyInterface']->getProperties(); + $this->assertCount(1, $args); + $this->assertInstanceOf(Reference::class, $args['foo']); + $this->assertTrue($container->has((string) $args['foo'])); + + $anonymous = $container->getDefinition((string) $args['foo']); + $this->assertEquals('Anonymous', $anonymous->getClass()); + $this->assertFalse($anonymous->isPublic()); + $this->assertEmpty($anonymous->getInstanceofConditionals()); + + $this->assertFalse($container->has('Bar')); } /** * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage The value of the "decorates" option for the "bar" service must be the id of the service without the "@" prefix (replace "@foo" with "foo"). + * @expectedExceptionMessageRegExp /Creating an alias using the tag "!service" is not allowed in ".+anonymous_services_alias\.yml"\./ */ - public function testDecoratedServicesWithWrongSyntaxThrowsException() + public function testAnonymousServicesWithAliases() { - $loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml')); - $loader->load('bad_decorates.yml'); + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('anonymous_services_alias.yml'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /Using an anonymous service in a parameter is not allowed in ".+anonymous_services_in_parameters\.yml"\./ + */ + public function testAnonymousServicesInParameters() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('anonymous_services_in_parameters.yml'); + } + + public function testAutoConfigureInstanceof() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_autoconfigure.yml'); + + $this->assertTrue($container->getDefinition('use_defaults_settings')->isAutoconfigured()); + $this->assertFalse($container->getDefinition('override_defaults_settings_to_false')->isAutoconfigured()); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /Service "_defaults" key must be an array, "NULL" given in ".+bad_empty_defaults\.yml"\./ + */ + public function testEmptyDefaultsThrowsClearException() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('bad_empty_defaults.yml'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /Service "_instanceof" key must be an array, "NULL" given in ".+bad_empty_instanceof\.yml"\./ + */ + public function testEmptyInstanceofThrowsClearException() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('bad_empty_instanceof.yml'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /^The configuration key "private" is unsupported for definition "bar"/ + */ + public function testUnsupportedKeywordThrowsException() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('bad_keyword.yml'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + * @expectedExceptionMessageRegExp /^The configuration key "calls" is unsupported for the service "bar" which is defined as an alias/ + */ + public function testUnsupportedKeywordInServiceAliasThrowsException() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('bad_alias.yml'); + } + + public function testCaseSensitivity() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_case.yml'); + $container->compile(); + + $this->assertTrue($container->has('bar')); + $this->assertTrue($container->has('BAR')); + $this->assertFalse($container->has('baR')); + $this->assertNotSame($container->get('BAR'), $container->get('bar')); + $this->assertSame($container->get('BAR')->arguments->bar, $container->get('bar')); + $this->assertSame($container->get('BAR')->bar, $container->get('bar')); + } + + public function testBindings() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_bindings.yml'); + $container->compile(); + + $definition = $container->getDefinition('bar'); + $this->assertEquals(array( + 'NonExistent' => null, + BarInterface::class => new Reference(Bar::class), + '$foo' => array(null), + '$quz' => 'quz', + '$factory' => 'factory', + ), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings())); + $this->assertEquals(array( + 'quz', + null, + new Reference(Bar::class), + array(null), + ), $definition->getArguments()); + + $definition = $container->getDefinition(Bar::class); + $this->assertEquals(array( + null, + 'factory', + ), $definition->getArguments()); + $this->assertEquals(array( + 'NonExistent' => null, + '$quz' => 'quz', + '$factory' => 'factory', + ), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings())); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/EnvPlaceholderParameterBagTest.php b/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/EnvPlaceholderParameterBagTest.php new file mode 100644 index 0000000000000..01fcd2c3ef10b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/EnvPlaceholderParameterBagTest.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\ParameterBag; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; + +class EnvPlaceholderParameterBagTest extends TestCase +{ + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function testGetThrowsInvalidArgumentExceptionIfEnvNameContainsNonWordCharacters() + { + $bag = new EnvPlaceholderParameterBag(); + $bag->get('env(%foo%)'); + } + + public function testMergeWillNotDuplicateIdenticalParameters() + { + $envVariableName = 'DB_HOST'; + $parameter = sprintf('env(%s)', $envVariableName); + $firstBag = new EnvPlaceholderParameterBag(); + + // initialize placeholders + $firstBag->get($parameter); + $secondBag = clone $firstBag; + + $firstBag->mergeEnvPlaceholders($secondBag); + $mergedPlaceholders = $firstBag->getEnvPlaceholders(); + + $placeholderForVariable = $mergedPlaceholders[$envVariableName]; + $placeholder = array_values($placeholderForVariable)[0]; + + $this->assertCount(1, $placeholderForVariable); + $this->assertInternalType('string', $placeholder); + $this->assertContains($envVariableName, $placeholder); + } + + public function testMergeWhereFirstBagIsEmptyWillWork() + { + $envVariableName = 'DB_HOST'; + $parameter = sprintf('env(%s)', $envVariableName); + $firstBag = new EnvPlaceholderParameterBag(); + $secondBag = new EnvPlaceholderParameterBag(); + + // initialize placeholder only in second bag + $secondBag->get($parameter); + + $this->assertEmpty($firstBag->getEnvPlaceholders()); + + $firstBag->mergeEnvPlaceholders($secondBag); + $mergedPlaceholders = $firstBag->getEnvPlaceholders(); + + $placeholderForVariable = $mergedPlaceholders[$envVariableName]; + $placeholder = array_values($placeholderForVariable)[0]; + + $this->assertCount(1, $placeholderForVariable); + $this->assertInternalType('string', $placeholder); + $this->assertContains($envVariableName, $placeholder); + } + + public function testMergeWherePlaceholderOnlyExistsInSecond() + { + $uniqueEnvName = 'DB_HOST'; + $commonEnvName = 'DB_USER'; + + $uniqueParamName = sprintf('env(%s)', $uniqueEnvName); + $commonParamName = sprintf('env(%s)', $commonEnvName); + + $firstBag = new EnvPlaceholderParameterBag(); + // initialize common placeholder + $firstBag->get($commonParamName); + $secondBag = clone $firstBag; + + // initialize unique placeholder + $secondBag->get($uniqueParamName); + + $firstBag->mergeEnvPlaceholders($secondBag); + $merged = $firstBag->getEnvPlaceholders(); + + $this->assertCount(1, $merged[$uniqueEnvName]); + // second bag has same placeholder for commonEnvName + $this->assertCount(1, $merged[$commonEnvName]); + } + + public function testMergeWithDifferentIdentifiersForPlaceholders() + { + $envName = 'DB_USER'; + $paramName = sprintf('env(%s)', $envName); + + $firstBag = new EnvPlaceholderParameterBag(); + $secondBag = new EnvPlaceholderParameterBag(); + // initialize placeholders + $firstPlaceholder = $firstBag->get($paramName); + $secondPlaceholder = $secondBag->get($paramName); + + $firstBag->mergeEnvPlaceholders($secondBag); + $merged = $firstBag->getEnvPlaceholders(); + + $this->assertNotEquals($firstPlaceholder, $secondPlaceholder); + $this->assertCount(2, $merged[$envName]); + } + + public function testResolveEnvCastsIntToString() + { + $bag = new EnvPlaceholderParameterBag(); + $bag->get('env(INT_VAR)'); + $bag->set('env(Int_Var)', 2); + $bag->resolve(); + $this->assertSame('2', $bag->all()['env(int_var)']); + } + + public function testResolveEnvAllowsNull() + { + $bag = new EnvPlaceholderParameterBag(); + $bag->get('env(NULL_VAR)'); + $bag->set('env(Null_Var)', null); + $bag->resolve(); + $this->assertNull($bag->all()['env(null_var)']); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage The default value of env parameter "ARRAY_VAR" must be scalar or null, array given. + */ + public function testResolveThrowsOnBadDefaultValue() + { + $bag = new EnvPlaceholderParameterBag(); + $bag->get('env(ARRAY_VAR)'); + $bag->set('env(Array_Var)', array()); + $bag->resolve(); + } + + public function testGetEnvAllowsNull() + { + $bag = new EnvPlaceholderParameterBag(); + $bag->set('env(NULL_VAR)', null); + $bag->get('env(NULL_VAR)'); + $bag->resolve(); + + $this->assertNull($bag->all()['env(null_var)']); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage The default value of an env() parameter must be scalar or null, but "array" given to "env(ARRAY_VAR)". + */ + public function testGetThrowsOnBadDefaultValue() + { + $bag = new EnvPlaceholderParameterBag(); + $bag->set('env(ARRAY_VAR)', array()); + $bag->get('env(ARRAY_VAR)'); + $bag->resolve(); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php b/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php index ae6b1990954ba..391e0eb3ff7de 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ParameterBag/ParameterBagTest.php @@ -72,37 +72,37 @@ public function testGetSet() } } - public function testGetThrowParameterNotFoundException() + /** + * @dataProvider provideGetThrowParameterNotFoundExceptionData + */ + public function testGetThrowParameterNotFoundException($parameterKey, $exceptionMessage) { $bag = new ParameterBag(array( 'foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz', + 'fiz' => array('bar' => array('boo' => 12)), )); - try { - $bag->get('foo1'); - $this->fail('->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException if the key does not exist'); - } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException', $e, '->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException if the key does not exist'); - $this->assertEquals('You have requested a non-existent parameter "foo1". Did you mean this: "foo"?', $e->getMessage(), '->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException with some advices'); + if (method_exists($this, 'expectException')) { + $this->expectException(ParameterNotFoundException::class); + $this->expectExceptionMessage($exceptionMessage); + } else { + $this->setExpectedException(ParameterNotFoundException::class, $exceptionMessage); } - try { - $bag->get('bag'); - $this->fail('->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException if the key does not exist'); - } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException', $e, '->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException if the key does not exist'); - $this->assertEquals('You have requested a non-existent parameter "bag". Did you mean one of these: "bar", "baz"?', $e->getMessage(), '->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException with some advices'); - } + $bag->get($parameterKey); + } - try { - $bag->get(''); - $this->fail('->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException if the key does not exist'); - } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException', $e, '->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException if the key does not exist'); - $this->assertEquals('You have requested a non-existent parameter "".', $e->getMessage(), '->get() throws an Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException with some advices'); - } + public function provideGetThrowParameterNotFoundExceptionData() + { + return array( + array('foo1', 'You have requested a non-existent parameter "foo1". Did you mean this: "foo"?'), + array('bag', 'You have requested a non-existent parameter "bag". Did you mean one of these: "bar", "baz"?'), + array('', 'You have requested a non-existent parameter "".'), + + array('fiz.bar.boo', 'You have requested a non-existent parameter "fiz.bar.boo". You cannot access nested array items, do you want to inject "fiz" instead?'), + ); } public function testHas() diff --git a/src/Symfony/Component/DependencyInjection/Tests/ReferenceTest.php b/src/Symfony/Component/DependencyInjection/Tests/ReferenceTest.php index ec0803fa3b8b2..1fc274a2922e7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ReferenceTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ReferenceTest.php @@ -21,10 +21,4 @@ public function testConstructor() $ref = new Reference('foo'); $this->assertEquals('foo', (string) $ref, '__construct() sets the id of the reference, which is used for the __toString() method'); } - - public function testCaseInsensitive() - { - $ref = new Reference('FooBar'); - $this->assertEquals('foobar', (string) $ref, 'the id is lowercased as the container is case insensitive'); - } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php b/src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php new file mode 100644 index 0000000000000..6900fd7ea4490 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ServiceLocator; + +class ServiceLocatorTest extends TestCase +{ + public function testHas() + { + $locator = new ServiceLocator(array( + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + function () { return 'dummy'; }, + )); + + $this->assertTrue($locator->has('foo')); + $this->assertTrue($locator->has('bar')); + $this->assertFalse($locator->has('dummy')); + } + + public function testGet() + { + $locator = new ServiceLocator(array( + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + )); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('baz', $locator->get('bar')); + } + + public function testGetDoesNotMemoize() + { + $i = 0; + $locator = new ServiceLocator(array( + 'foo' => function () use (&$i) { + ++$i; + + return 'bar'; + }, + )); + + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame('bar', $locator->get('foo')); + $this->assertSame(2, $i); + } + + /** + * @expectedException \Psr\Container\NotFoundExceptionInterface + * @expectedExceptionMessage You have requested a non-existent service "dummy" + */ + public function testGetThrowsOnUndefinedService() + { + $locator = new ServiceLocator(array( + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + )); + + $locator->get('dummy'); + } + + public function testInvoke() + { + $locator = new ServiceLocator(array( + 'foo' => function () { return 'bar'; }, + 'bar' => function () { return 'baz'; }, + )); + + $this->assertSame('bar', $locator('foo')); + $this->assertSame('baz', $locator('bar')); + $this->assertNull($locator('dummy'), '->__invoke() should return null on invalid service'); + } +} diff --git a/src/Symfony/Component/DependencyInjection/TypedReference.php b/src/Symfony/Component/DependencyInjection/TypedReference.php new file mode 100644 index 0000000000000..aad78e806b6b6 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/TypedReference.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\Component\DependencyInjection; + +/** + * Represents a PHP type-hinted service reference. + * + * @author Nicolas Grekas + */ +class TypedReference extends Reference +{ + private $type; + private $requiringClass; + + /** + * @param string $id The service identifier + * @param string $type The PHP type of the identified service + * @param string $requiringClass The class of the service that requires the referenced type + * @param int $invalidBehavior The behavior when the service does not exist + */ + public function __construct($id, $type, $requiringClass = '', $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE) + { + parent::__construct($id, $invalidBehavior); + $this->type = $type; + $this->requiringClass = $requiringClass; + } + + public function getType() + { + return $this->type; + } + + public function getRequiringClass() + { + return $this->requiringClass; + } + + public function canBeAutoregistered() + { + return $this->requiringClass && (false !== $i = strpos($this->type, '\\')) && 0 === strncasecmp($this->type, $this->requiringClass, 1 + $i); + } +} diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 2506690ca6997..1a2fef0879810 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -16,22 +16,30 @@ } ], "require": { - "php": ">=5.3.9" + "php": "^7.1.3", + "psr/container": "^1.0" }, "require-dev": { - "symfony/yaml": "~2.3.42|~2.7.14|~2.8.7|~3.0.7", - "symfony/config": "~2.2|~3.0.0", - "symfony/expression-language": "~2.6|~3.0.0" - }, - "conflict": { - "symfony/expression-language": "<2.6" + "symfony/yaml": "~3.4|~4.0", + "symfony/config": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0" }, "suggest": { "symfony/yaml": "", "symfony/config": "", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", "symfony/expression-language": "For using expressions in service container configuration", "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them" }, + "conflict": { + "symfony/config": "<3.4", + "symfony/finder": "<3.4", + "symfony/proxy-manager-bridge": "<3.4", + "symfony/yaml": "<3.4" + }, + "provide": { + "psr/container-implementation": "1.0" + }, "autoload": { "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" }, "exclude-from-classmap": [ @@ -41,7 +49,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "4.0-dev" } } } diff --git a/src/Symfony/Component/DomCrawler/AbstractUriElement.php b/src/Symfony/Component/DomCrawler/AbstractUriElement.php new file mode 100644 index 0000000000000..d602d6f3316bf --- /dev/null +++ b/src/Symfony/Component/DomCrawler/AbstractUriElement.php @@ -0,0 +1,212 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +/** + * Any HTML element that can link to an URI. + * + * @author Fabien Potencier + */ +abstract class AbstractUriElement +{ + /** + * @var \DOMElement + */ + protected $node; + + /** + * @var string The method to use for the element + */ + protected $method; + + /** + * @var string The URI of the page where the element is embedded (or the base href) + */ + protected $currentUri; + + /** + * @param \DOMElement $node A \DOMElement instance + * @param string $currentUri The URI of the page where the link is embedded (or the base href) + * @param string $method The method to use for the link (get by default) + * + * @throws \InvalidArgumentException if the node is not a link + */ + public function __construct(\DOMElement $node, $currentUri, $method = 'GET') + { + if (!in_array(strtolower(substr($currentUri, 0, 4)), array('http', 'file'))) { + throw new \InvalidArgumentException(sprintf('Current URI must be an absolute URL ("%s").', $currentUri)); + } + + $this->setNode($node); + $this->method = $method ? strtoupper($method) : null; + $this->currentUri = $currentUri; + } + + /** + * Gets the node associated with this link. + * + * @return \DOMElement A \DOMElement instance + */ + public function getNode() + { + return $this->node; + } + + /** + * Gets the method associated with this link. + * + * @return string The method + */ + public function getMethod() + { + return $this->method; + } + + /** + * Gets the URI associated with this link. + * + * @return string The URI + */ + public function getUri() + { + $uri = trim($this->getRawUri()); + + // absolute URL? + if (null !== parse_url($uri, PHP_URL_SCHEME)) { + return $uri; + } + + // empty URI + if (!$uri) { + return $this->currentUri; + } + + // an anchor + if ('#' === $uri[0]) { + return $this->cleanupAnchor($this->currentUri).$uri; + } + + $baseUri = $this->cleanupUri($this->currentUri); + + if ('?' === $uri[0]) { + return $baseUri.$uri; + } + + // absolute URL with relative schema + if (0 === strpos($uri, '//')) { + return preg_replace('#^([^/]*)//.*$#', '$1', $baseUri).$uri; + } + + $baseUri = preg_replace('#^(.*?//[^/]*)(?:\/.*)?$#', '$1', $baseUri); + + // absolute path + if ('/' === $uri[0]) { + return $baseUri.$uri; + } + + // relative path + $path = parse_url(substr($this->currentUri, strlen($baseUri)), PHP_URL_PATH); + $path = $this->canonicalizePath(substr($path, 0, strrpos($path, '/')).'/'.$uri); + + return $baseUri.('' === $path || '/' !== $path[0] ? '/' : '').$path; + } + + /** + * Returns raw URI data. + * + * @return string + */ + abstract protected function getRawUri(); + + /** + * Returns the canonicalized URI path (see RFC 3986, section 5.2.4). + * + * @param string $path URI path + * + * @return string + */ + protected function canonicalizePath($path) + { + if ('' === $path || '/' === $path) { + return $path; + } + + if ('.' === substr($path, -1)) { + $path .= '/'; + } + + $output = array(); + + foreach (explode('/', $path) as $segment) { + if ('..' === $segment) { + array_pop($output); + } elseif ('.' !== $segment) { + $output[] = $segment; + } + } + + return implode('/', $output); + } + + /** + * Sets current \DOMElement instance. + * + * @param \DOMElement $node A \DOMElement instance + * + * @throws \LogicException If given node is not an anchor + */ + abstract protected function setNode(\DOMElement $node); + + /** + * Removes the query string and the anchor from the given uri. + * + * @param string $uri The uri to clean + * + * @return string + */ + private function cleanupUri($uri) + { + return $this->cleanupQuery($this->cleanupAnchor($uri)); + } + + /** + * Remove the query string from the uri. + * + * @param string $uri + * + * @return string + */ + private function cleanupQuery($uri) + { + if (false !== $pos = strpos($uri, '?')) { + return substr($uri, 0, $pos); + } + + return $uri; + } + + /** + * Remove the anchor from the uri. + * + * @param string $uri + * + * @return string + */ + private function cleanupAnchor($uri) + { + if (false !== $pos = strpos($uri, '#')) { + return substr($uri, 0, $pos); + } + + return $uri; + } +} diff --git a/src/Symfony/Component/DomCrawler/CHANGELOG.md b/src/Symfony/Component/DomCrawler/CHANGELOG.md index 48fd323f8202c..e65176f5ac0b4 100644 --- a/src/Symfony/Component/DomCrawler/CHANGELOG.md +++ b/src/Symfony/Component/DomCrawler/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +3.1.0 +----- + +* All the URI parsing logic have been abstracted in the `AbstractUriElement` class. + The `Link` class is now a child of `AbstractUriElement`. +* Added an `Image` class to crawl images and parse their `src` attribute, + and `selectImage`, `image`, `images` methods in the `Crawler` (the image version of the equivalent `link` methods). + 2.5.0 ----- diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 094075bfc903a..d6dbe1e7a9768 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -18,7 +18,7 @@ * * @author Fabien Potencier */ -class Crawler extends \SplObjectStorage +class Crawler implements \Countable, \IteratorAggregate { /** * @var string The current URI @@ -45,6 +45,11 @@ class Crawler extends \SplObjectStorage */ private $document; + /** + * @var \DOMElement[] + */ + private $nodes = array(); + /** * Whether the Crawler contains HTML or XML content (used when converting CSS to XPath). * @@ -53,8 +58,6 @@ class Crawler extends \SplObjectStorage private $isHtml = true; /** - * Constructor. - * * @param mixed $node A Node to use as the base for the crawling * @param string $currentUri The current URI * @param string $baseHref The base href value @@ -67,12 +70,32 @@ public function __construct($node = null, $currentUri = null, $baseHref = null) $this->add($node); } + /** + * Returns the current URI. + * + * @return string + */ + public function getUri() + { + return $this->uri; + } + + /** + * Returns base href. + * + * @return string + */ + public function getBaseHref() + { + return $this->baseHref; + } + /** * Removes all the nodes. */ public function clear() { - parent::removeAll($this); + $this->nodes = array(); $this->document = null; } @@ -294,25 +317,19 @@ public function addNode(\DOMNode $node) } if (null !== $this->document && $this->document !== $node->ownerDocument) { - @trigger_error('Attaching DOM nodes from multiple documents in a Crawler is deprecated as of 2.8 and will be forbidden in 3.0.', E_USER_DEPRECATED); + throw new \InvalidArgumentException('Attaching DOM nodes from multiple documents in the same crawler is forbidden.'); } if (null === $this->document) { $this->document = $node->ownerDocument; } - parent::attach($node); - } - - // Serializing and unserializing a crawler creates DOM objects in a corrupted state. DOM elements are not properly serializable. - public function unserialize($serialized) - { - throw new \BadMethodCallException('A Crawler cannot be serialized.'); - } + // Don't add duplicate nodes in the Crawler + if (in_array($node, $this->nodes, true)) { + return; + } - public function serialize() - { - throw new \BadMethodCallException('A Crawler cannot be serialized.'); + $this->nodes[] = $node; } /** @@ -324,10 +341,8 @@ public function serialize() */ public function eq($position) { - foreach ($this as $i => $node) { - if ($i == $position) { - return $this->createSubCrawler($node); - } + if (isset($this->nodes[$position])) { + return $this->createSubCrawler($this->nodes[$position]); } return $this->createSubCrawler(null); @@ -352,7 +367,7 @@ public function eq($position) public function each(\Closure $closure) { $data = array(); - foreach ($this as $i => $node) { + foreach ($this->nodes as $i => $node) { $data[] = $closure($this->createSubCrawler($node), $i); } @@ -367,9 +382,9 @@ public function each(\Closure $closure) * * @return self */ - public function slice($offset = 0, $length = -1) + public function slice($offset = 0, $length = null) { - return $this->createSubCrawler(iterator_to_array(new \LimitIterator($this, $offset, $length))); + return $this->createSubCrawler(array_slice($this->nodes, $offset, $length)); } /** @@ -384,7 +399,7 @@ public function slice($offset = 0, $length = -1) public function reduce(\Closure $closure) { $nodes = array(); - foreach ($this as $i => $node) { + foreach ($this->nodes as $i => $node) { if (false !== $closure($this->createSubCrawler($node), $i)) { $nodes[] = $node; } @@ -410,7 +425,7 @@ public function first() */ public function last() { - return $this->eq(count($this) - 1); + return $this->eq(count($this->nodes) - 1); } /** @@ -422,7 +437,7 @@ public function last() */ public function siblings() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -438,7 +453,7 @@ public function siblings() */ public function nextAll() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -454,7 +469,7 @@ public function nextAll() */ public function previousAll() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -470,7 +485,7 @@ public function previousAll() */ public function parents() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -495,7 +510,7 @@ public function parents() */ public function children() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -515,7 +530,7 @@ public function children() */ public function attr($attribute) { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -533,7 +548,7 @@ public function attr($attribute) */ public function nodeName() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -549,7 +564,7 @@ public function nodeName() */ public function text() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -565,7 +580,7 @@ public function text() */ public function html() { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -577,6 +592,36 @@ public function html() return $html; } + /** + * Evaluates an XPath expression. + * + * Since an XPath expression might evaluate to either a simple type or a \DOMNodeList, + * this method will return either an array of simple types or a new Crawler instance. + * + * @param string $xpath An XPath expression + * + * @return array|Crawler An array of evaluation results or a new Crawler instance + */ + public function evaluate($xpath) + { + if (null === $this->document) { + throw new \LogicException('Cannot evaluate the expression on an uninitialized crawler.'); + } + + $data = array(); + $domxpath = $this->createDOMXPath($this->document, $this->findNamespacePrefixes($xpath)); + + foreach ($this->nodes as $node) { + $data[] = $domxpath->evaluate($xpath, $node); + } + + if (isset($data[0]) && $data[0] instanceof \DOMNodeList) { + return $this->createSubCrawler($data); + } + + return $data; + } + /** * Extracts information from the list of nodes. * @@ -596,7 +641,7 @@ public function extract($attributes) $count = count($attributes); $data = array(); - foreach ($this as $node) { + foreach ($this->nodes as $node) { $elements = array(); foreach ($attributes as $attribute) { if ('_text' === $attribute) { @@ -674,6 +719,20 @@ public function selectLink($value) return $this->filterRelativeXPath($xpath); } + /** + * Selects images by alt value. + * + * @param string $value The image alt + * + * @return self A new instance of Crawler with the filtered list of nodes + */ + public function selectImage($value) + { + $xpath = sprintf('descendant-or-self::img[contains(normalize-space(string(@alt)), %s)]', static::xpathLiteral($value)); + + return $this->filterRelativeXPath($xpath); + } + /** * Selects a button by name or alt value for images. * @@ -702,7 +761,7 @@ public function selectButton($value) */ public function link($method = 'get') { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -725,7 +784,7 @@ public function link($method = 'get') public function links() { $links = array(); - foreach ($this as $node) { + foreach ($this->nodes as $node) { if (!$node instanceof \DOMElement) { throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_class($node))); } @@ -736,6 +795,47 @@ public function links() return $links; } + /** + * Returns an Image object for the first node in the list. + * + * @return Image An Image instance + * + * @throws \InvalidArgumentException If the current node list is empty + */ + public function image() + { + if (!count($this)) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + + if (!$node instanceof \DOMElement) { + throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_class($node))); + } + + return new Image($node, $this->baseHref); + } + + /** + * Returns an array of Image objects for the nodes in the list. + * + * @return Image[] An array of Image instances + */ + public function images() + { + $images = array(); + foreach ($this as $node) { + if (!$node instanceof \DOMElement) { + throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_class($node))); + } + + $images[] = new Image($node, $this->baseHref); + } + + return $images; + } + /** * Returns a Form object for the first node in the list. * @@ -748,7 +848,7 @@ public function links() */ public function form(array $values = null, $method = null) { - if (!count($this)) { + if (!$this->nodes) { throw new \InvalidArgumentException('The current node list is empty.'); } @@ -833,126 +933,6 @@ public static function xpathLiteral($s) return sprintf('concat(%s)', implode(', ', $parts)); } - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function attach($object, $data = null) - { - $this->triggerDeprecation(__METHOD__); - - parent::attach($object, $data); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function detach($object) - { - $this->triggerDeprecation(__METHOD__); - - parent::detach($object); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function contains($object) - { - $this->triggerDeprecation(__METHOD__); - - return parent::contains($object); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function addAll($storage) - { - $this->triggerDeprecation(__METHOD__); - - parent::addAll($storage); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function removeAll($storage) - { - $this->triggerDeprecation(__METHOD__); - - parent::removeAll($storage); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function removeAllExcept($storage) - { - $this->triggerDeprecation(__METHOD__); - - parent::removeAllExcept($storage); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function getInfo() - { - $this->triggerDeprecation(__METHOD__); - - return parent::getInfo(); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function setInfo($data) - { - $this->triggerDeprecation(__METHOD__); - - parent::setInfo($data); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function offsetExists($object) - { - $this->triggerDeprecation(__METHOD__); - - return parent::offsetExists($object); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function offsetSet($object, $data = null) - { - $this->triggerDeprecation(__METHOD__); - - parent::offsetSet($object, $data); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function offsetUnset($object) - { - $this->triggerDeprecation(__METHOD__); - - parent::offsetUnset($object); - } - - /** - * @deprecated Using the SplObjectStorage API on the Crawler is deprecated as of 2.8 and will be removed in 3.0. - */ - public function offsetGet($object) - { - $this->triggerDeprecation(__METHOD__); - - return parent::offsetGet($object); - } - /** * Filters the list of nodes with an XPath expression. * @@ -968,7 +948,7 @@ private function filterRelativeXPath($xpath) $crawler = $this->createSubCrawler(null); - foreach ($this as $node) { + foreach ($this->nodes as $node) { $domxpath = $this->createDOMXPath($node->ownerDocument, $prefixes); $crawler->add($domxpath->query($xpath, $node)); } @@ -1032,12 +1012,7 @@ private function relativize($xpath) } $expression = rtrim(substr($xpath, $startPosition, $i - $startPosition)); - // BC for Symfony 2.4 and lower were elements were adding in a fake _root parent - if (0 === strpos($expression, '/_root/')) { - @trigger_error('XPath expressions referencing the fake root node are deprecated since version 2.8 and will be unsupported in 3.0. Please use "./" instead of "/_root/".', E_USER_DEPRECATED); - - $expression = './'.substr($expression, 7); - } elseif (0 === strpos($expression, 'self::*/')) { + if (0 === strpos($expression, 'self::*/')) { $expression = './'.substr($expression, 8); } @@ -1052,12 +1027,7 @@ private function relativize($xpath) $expression = 'self::'.substr($expression, 2); } elseif (0 === strpos($expression, 'child::')) { $expression = 'self::'.substr($expression, 7); - } elseif ('/' === $expression[0] || 0 === strpos($expression, 'self::')) { - // the only direct child in Symfony 2.4 and lower is _root, which is already handled previously - // so let's drop the expression entirely - $expression = $nonMatchingExpression; - } elseif ('.' === $expression[0]) { - // '.' is the fake root element in Symfony 2.4 and lower, which is excluded from results + } elseif ('/' === $expression[0] || '.' === $expression[0] || 0 === strpos($expression, 'self::')) { $expression = $nonMatchingExpression; } elseif (0 === strpos($expression, 'descendant::')) { $expression = 'descendant-or-self::'.substr($expression, 12); @@ -1087,13 +1057,27 @@ private function relativize($xpath) */ public function getNode($position) { - foreach ($this as $i => $node) { - if ($i == $position) { - return $node; - } + if (isset($this->nodes[$position])) { + return $this->nodes[$position]; } } + /** + * @return int + */ + public function count() + { + return count($this->nodes); + } + + /** + * @return \ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator($this->nodes); + } + /** * @param \DOMElement $node * @param string $siblingDir @@ -1187,23 +1171,4 @@ private function createSubCrawler($nodes) return $crawler; } - - private function triggerDeprecation($methodName, $useTrace = false) - { - if ($useTrace || defined('HHVM_VERSION')) { - if (\PHP_VERSION_ID >= 50400) { - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); - } else { - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - } - - // The SplObjectStorage class performs calls to its own methods. These - // method calls must not lead to triggered deprecation notices. - if (isset($trace[2]['class']) && 'SplObjectStorage' === $trace[2]['class']) { - return; - } - } - - @trigger_error('The '.$methodName.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); - } } diff --git a/src/Symfony/Component/DomCrawler/Field/FileFormField.php b/src/Symfony/Component/DomCrawler/Field/FileFormField.php index 0e0f94347a5ee..fc21239f01059 100644 --- a/src/Symfony/Component/DomCrawler/Field/FileFormField.php +++ b/src/Symfony/Component/DomCrawler/Field/FileFormField.php @@ -59,7 +59,7 @@ public function setValue($value) $name = $info['basename']; // copy to a tmp location - $tmp = sys_get_temp_dir().'/'.sha1(uniqid(mt_rand(), true)); + $tmp = sys_get_temp_dir().'/'.strtr(substr(base64_encode(hash('sha256', uniqid(mt_rand(), true), true)), 0, 7), '/', '_'); if (array_key_exists('extension', $info)) { $tmp .= '.'.$info['extension']; } diff --git a/src/Symfony/Component/DomCrawler/Field/FormField.php b/src/Symfony/Component/DomCrawler/Field/FormField.php index a6b33ded2d2f3..1fa3e1de5f50e 100644 --- a/src/Symfony/Component/DomCrawler/Field/FormField.php +++ b/src/Symfony/Component/DomCrawler/Field/FormField.php @@ -57,6 +57,30 @@ public function __construct(\DOMElement $node) $this->initialize(); } + /** + * Returns the label tag associated to the field or null if none. + * + * @return \DOMElement|null + */ + public function getLabel() + { + $xpath = new \DOMXPath($this->node->ownerDocument); + + if ($this->node->hasAttribute('id')) { + $labels = $xpath->query(sprintf('descendant::label[@for="%s"]', $this->node->getAttribute('id'))); + if ($labels->length > 0) { + return $labels->item(0); + } + } + + $labels = $xpath->query('ancestor::label[1]', $this->node); + if ($labels->length > 0) { + return $labels->item(0); + } + + return; + } + /** * Returns the name of the field. * diff --git a/src/Symfony/Component/DomCrawler/Form.php b/src/Symfony/Component/DomCrawler/Form.php index bad1b34935d04..db1c6ebb64c42 100644 --- a/src/Symfony/Component/DomCrawler/Form.php +++ b/src/Symfony/Component/DomCrawler/Form.php @@ -211,6 +211,11 @@ public function getUri() protected function getRawUri() { + // If the form was created from a button rather than the form node, check for HTML5 action overrides + if ($this->button !== $this->node && $this->button->getAttribute('formaction')) { + return $this->button->getAttribute('formaction'); + } + return $this->node->getAttribute('action'); } @@ -227,6 +232,11 @@ public function getMethod() return $this->method; } + // If the form was created from a button rather than the form node, check for HTML5 method override + if ($this->button !== $this->node && $this->button->getAttribute('formmethod')) { + return strtoupper($this->button->getAttribute('formmethod')); + } + return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET'; } diff --git a/src/Symfony/Component/DomCrawler/Image.php b/src/Symfony/Component/DomCrawler/Image.php new file mode 100644 index 0000000000000..4d6403258057c --- /dev/null +++ b/src/Symfony/Component/DomCrawler/Image.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +/** + * Image represents an HTML image (an HTML img tag). + */ +class Image extends AbstractUriElement +{ + public function __construct(\DOMElement $node, $currentUri) + { + parent::__construct($node, $currentUri, 'GET'); + } + + protected function getRawUri() + { + return $this->node->getAttribute('src'); + } + + protected function setNode(\DOMElement $node) + { + if ('img' !== $node->nodeName) { + throw new \LogicException(sprintf('Unable to visualize a "%s" tag.', $node->nodeName)); + } + + $this->node = $node; + } +} diff --git a/src/Symfony/Component/DomCrawler/Link.php b/src/Symfony/Component/DomCrawler/Link.php index ede0991e6f36c..80a356e468480 100644 --- a/src/Symfony/Component/DomCrawler/Link.php +++ b/src/Symfony/Component/DomCrawler/Link.php @@ -16,159 +16,13 @@ * * @author Fabien Potencier */ -class Link +class Link extends AbstractUriElement { - /** - * @var \DOMElement - */ - protected $node; - - /** - * @var string The method to use for the link - */ - protected $method; - - /** - * @var string The URI of the page where the link is embedded (or the base href) - */ - protected $currentUri; - - /** - * Constructor. - * - * @param \DOMElement $node A \DOMElement instance - * @param string $currentUri The URI of the page where the link is embedded (or the base href) - * @param string $method The method to use for the link (get by default) - * - * @throws \InvalidArgumentException if the node is not a link - */ - public function __construct(\DOMElement $node, $currentUri, $method = 'GET') - { - if (!in_array(strtolower(substr($currentUri, 0, 4)), array('http', 'file'))) { - throw new \InvalidArgumentException(sprintf('Current URI must be an absolute URL ("%s").', $currentUri)); - } - - $this->setNode($node); - $this->method = $method ? strtoupper($method) : null; - $this->currentUri = $currentUri; - } - - /** - * Gets the node associated with this link. - * - * @return \DOMElement A \DOMElement instance - */ - public function getNode() - { - return $this->node; - } - - /** - * Gets the method associated with this link. - * - * @return string The method - */ - public function getMethod() - { - return $this->method; - } - - /** - * Gets the URI associated with this link. - * - * @return string The URI - */ - public function getUri() - { - $uri = trim($this->getRawUri()); - - // absolute URL? - if (null !== parse_url($uri, PHP_URL_SCHEME)) { - return $uri; - } - - // empty URI - if (!$uri) { - return $this->currentUri; - } - - // an anchor - if ('#' === $uri[0]) { - return $this->cleanupAnchor($this->currentUri).$uri; - } - - $baseUri = $this->cleanupUri($this->currentUri); - - if ('?' === $uri[0]) { - return $baseUri.$uri; - } - - // absolute URL with relative schema - if (0 === strpos($uri, '//')) { - return preg_replace('#^([^/]*)//.*$#', '$1', $baseUri).$uri; - } - - $baseUri = preg_replace('#^(.*?//[^/]*)(?:\/.*)?$#', '$1', $baseUri); - - // absolute path - if ('/' === $uri[0]) { - return $baseUri.$uri; - } - - // relative path - $path = parse_url(substr($this->currentUri, strlen($baseUri)), PHP_URL_PATH); - $path = $this->canonicalizePath(substr($path, 0, strrpos($path, '/')).'/'.$uri); - - return $baseUri.('' === $path || '/' !== $path[0] ? '/' : '').$path; - } - - /** - * Returns raw URI data. - * - * @return string - */ protected function getRawUri() { return $this->node->getAttribute('href'); } - /** - * Returns the canonicalized URI path (see RFC 3986, section 5.2.4). - * - * @param string $path URI path - * - * @return string - */ - protected function canonicalizePath($path) - { - if ('' === $path || '/' === $path) { - return $path; - } - - if ('.' === substr($path, -1)) { - $path .= '/'; - } - - $output = array(); - - foreach (explode('/', $path) as $segment) { - if ('..' === $segment) { - array_pop($output); - } elseif ('.' !== $segment) { - $output[] = $segment; - } - } - - return implode('/', $output); - } - - /** - * Sets current \DOMElement instance. - * - * @param \DOMElement $node A \DOMElement instance - * - * @throws \LogicException If given node is not an anchor - */ protected function setNode(\DOMElement $node) { if ('a' !== $node->nodeName && 'area' !== $node->nodeName && 'link' !== $node->nodeName) { @@ -177,48 +31,4 @@ protected function setNode(\DOMElement $node) $this->node = $node; } - - /** - * Removes the query string and the anchor from the given uri. - * - * @param string $uri The uri to clean - * - * @return string - */ - private function cleanupUri($uri) - { - return $this->cleanupQuery($this->cleanupAnchor($uri)); - } - - /** - * Remove the query string from the uri. - * - * @param string $uri - * - * @return string - */ - private function cleanupQuery($uri) - { - if (false !== $pos = strpos($uri, '?')) { - return substr($uri, 0, $pos); - } - - return $uri; - } - - /** - * Remove the anchor from the uri. - * - * @param string $uri - * - * @return string - */ - private function cleanupAnchor($uri) - { - if (false !== $pos = strpos($uri, '#')) { - return substr($uri, 0, $pos); - } - - return $uri; - } } diff --git a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php index f002627ef5942..f7b0857309ebe 100644 --- a/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/CrawlerTest.php @@ -28,6 +28,20 @@ public function testConstructor() $this->assertCount(1, $crawler, '__construct() takes a node as a first argument'); } + public function testGetUri() + { + $uri = 'http://symfony.com'; + $crawler = new Crawler(null, $uri); + $this->assertEquals($uri, $crawler->getUri()); + } + + public function testGetBaseHref() + { + $baseHref = 'http://symfony.com'; + $crawler = new Crawler(null, null, $baseHref); + $this->assertEquals($baseHref, $crawler->getBaseHref()); + } + public function testAdd() { $crawler = new Crawler(); @@ -64,6 +78,16 @@ public function testAddInvalidType() $crawler->add(1); } + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Attaching DOM nodes from multiple documents in the same crawler is forbidden. + */ + public function testAddMultipleDocumentNode() + { + $crawler = $this->createTestCrawler(); + $crawler->addHtmlContent('
      ', 'UTF-8'); + } + public function testAddHtmlContent() { $crawler = new Crawler(); @@ -492,16 +516,6 @@ public function testFilterXPathWithFakeRoot() $this->assertCount(0, $crawler->filterXPath('self::_root'), '->filterXPath() returns an empty result if the XPath references the fake root node'); } - /** @group legacy */ - public function testLegacyFilterXPathWithFakeRoot() - { - $crawler = $this->createTestCrawler(); - $this->assertCount(0, $crawler->filterXPath('/_root'), '->filterXPath() returns an empty result if the XPath references the fake root node'); - - $crawler = $this->createTestCrawler()->filterXPath('//body'); - $this->assertCount(1, $crawler->filterXPath('/_root/body')); - } - public function testFilterXPathWithAncestorAxis() { $crawler = $this->createTestCrawler()->filterXPath('//form'); @@ -665,6 +679,17 @@ public function testSelectLink() $this->assertCount(4, $crawler->selectLink('Bar'), '->selectLink() selects links by the node values'); } + public function testSelectImage() + { + $crawler = $this->createTestCrawler(); + $this->assertNotSame($crawler, $crawler->selectImage('Bar'), '->selectImage() returns a new instance of a crawler'); + $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Crawler', $crawler, '->selectImage() returns a new instance of a crawler'); + + $this->assertCount(1, $crawler->selectImage('Fabien\'s Bar'), '->selectImage() selects images by alt attribute'); + $this->assertCount(2, $crawler->selectImage('Fabien"s Bar'), '->selectImage() selects images by alt attribute'); + $this->assertCount(1, $crawler->selectImage('\' Fabien"s Bar'), '->selectImage() selects images by alt attribute'); + } + public function testSelectButton() { $crawler = $this->createTestCrawler(); @@ -763,6 +788,19 @@ public function testInvalidLinks() $crawler->filterXPath('//li/text()')->link(); } + public function testImage() + { + $crawler = $this->createTestCrawler('http://example.com/bar/')->selectImage('Bar'); + $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Image', $crawler->image(), '->image() returns an Image instance'); + + try { + $this->createTestCrawler()->filterXPath('//ol')->image(); + $this->fail('->image() throws an \InvalidArgumentException if the node list is empty'); + } catch (\InvalidArgumentException $e) { + $this->assertTrue(true, '->image() throws an \InvalidArgumentException if the node list is empty'); + } + } + public function testSelectLinkAndLinkFiltered() { $html = <<<'HTML' @@ -813,6 +851,18 @@ public function testLinks() $this->assertEquals(array(), $this->createTestCrawler()->filterXPath('//ol')->links(), '->links() returns an empty array if the node selection is empty'); } + public function testImages() + { + $crawler = $this->createTestCrawler('http://example.com/bar/')->selectImage('Bar'); + $this->assertInternalType('array', $crawler->images(), '->images() returns an array'); + + $this->assertCount(4, $crawler->images(), '->images() returns an array'); + $images = $crawler->images(); + $this->assertInstanceOf('Symfony\\Component\\DomCrawler\\Image', $images[0], '->images() returns an array of Image instances'); + + $this->assertEquals(array(), $this->createTestCrawler()->filterXPath('//ol')->links(), '->links() returns an empty array if the node selection is empty'); + } + public function testForm() { $testCrawler = $this->createTestCrawler('http://example.com/bar/'); @@ -1021,6 +1071,51 @@ public function testCountOfNestedElements() $this->assertCount(1, $crawler->filter('li:contains("List item 1")')); } + public function testEvaluateReturnsTypedResultOfXPathExpressionOnADocumentSubset() + { + $crawler = $this->createTestCrawler(); + + $result = $crawler->filterXPath('//form/input')->evaluate('substring-before(@name, "Name")'); + + $this->assertSame(array('Text', 'Foo', 'Bar'), $result); + } + + public function testEvaluateReturnsTypedResultOfNamespacedXPathExpressionOnADocumentSubset() + { + $crawler = $this->createTestXmlCrawler(); + + $result = $crawler->filterXPath('//yt:accessControl/@action')->evaluate('string(.)'); + + $this->assertSame(array('comment', 'videoRespond'), $result); + } + + public function testEvaluateReturnsTypedResultOfNamespacedXPathExpression() + { + $crawler = $this->createTestXmlCrawler(); + $crawler->registerNamespace('youtube', 'http://gdata.youtube.com/schemas/2007'); + + $result = $crawler->evaluate('string(//youtube:accessControl/@action)'); + + $this->assertSame(array('comment'), $result); + } + + public function testEvaluateReturnsACrawlerIfXPathExpressionEvaluatesToANode() + { + $crawler = $this->createTestCrawler()->evaluate('//form/input[1]'); + + $this->assertInstanceOf(Crawler::class, $crawler); + $this->assertCount(1, $crawler); + $this->assertSame('input', $crawler->first()->nodeName()); + } + + /** + * @expectedException \LogicException + */ + public function testEvaluateThrowsAnExceptionIfDocumentIsEmpty() + { + (new Crawler())->evaluate('//form/input[1]'); + } + public function createTestCrawler($uri = null) { $dom = new \DOMDocument(); diff --git a/src/Symfony/Component/DomCrawler/Tests/Field/FormFieldTest.php b/src/Symfony/Component/DomCrawler/Tests/Field/FormFieldTest.php index 510f7628f2428..d150eb3ac73b1 100644 --- a/src/Symfony/Component/DomCrawler/Tests/Field/FormFieldTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/Field/FormFieldTest.php @@ -35,4 +35,38 @@ public function testGetSetHasValue() $this->assertTrue($field->hasValue(), '->hasValue() always returns true'); } + + public function testLabelReturnsNullIfNoneIsDefined() + { + $dom = new \DOMDocument(); + $dom->loadHTML('
      '); + + $field = new InputFormField($dom->getElementById('foo')); + $this->assertNull($field->getLabel(), '->getLabel() returns null if no label is defined'); + } + + public function testLabelIsAssignedByForAttribute() + { + $dom = new \DOMDocument(); + $dom->loadHTML('
      + + + +
      '); + + $field = new InputFormField($dom->getElementById('foo')); + $this->assertEquals('Foo label', $field->getLabel()->textContent, '->getLabel() returns the associated label'); + } + + public function testLabelIsAssignedByParentingRelation() + { + $dom = new \DOMDocument(); + $dom->loadHTML('
      + + +
      '); + + $field = new InputFormField($dom->getElementById('foo')); + $this->assertEquals('Foo label', $field->getLabel()->textContent, '->getLabel() returns the parent label'); + } } diff --git a/src/Symfony/Component/DomCrawler/Tests/FormTest.php b/src/Symfony/Component/DomCrawler/Tests/FormTest.php index 0a4cd7201e1bb..1c7ebe7f7a664 100644 --- a/src/Symfony/Component/DomCrawler/Tests/FormTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/FormTest.php @@ -321,6 +321,12 @@ public function testGetMethod() $this->assertEquals('PATCH', $form->getMethod(), '->getMethod() returns the method defined in the constructor if provided'); } + public function testGetMethodWithOverride() + { + $form = $this->createForm('
      '); + $this->assertEquals('POST', $form->getMethod(), '->getMethod() returns the method attribute value of the form'); + } + public function testGetSetValue() { $form = $this->createForm('
      '); @@ -528,6 +534,12 @@ public function testGetUriWithoutAction() $this->assertEquals('http://localhost/foo/bar', $form->getUri(), '->getUri() returns path if no action defined'); } + public function testGetUriWithActionOverride() + { + $form = $this->createForm('