From e697c7d2721d30884193ed86c5f6fd6f044323ab Mon Sep 17 00:00:00 2001 From: Anton Dyshkant Date: Tue, 24 Apr 2018 14:54:22 +0300 Subject: [PATCH 001/125] [Finder] added "use natural sort" option --- src/Symfony/Component/Finder/CHANGELOG.md | 5 + src/Symfony/Component/Finder/Finder.php | 8 +- .../Finder/Iterator/SortableIterator.php | 5 + .../Component/Finder/Tests/FinderTest.php | 495 ++++++++++++++++-- .../Iterator/DateRangeFilterIteratorTest.php | 18 + .../Iterator/DepthRangeFilterIteratorTest.php | 20 + .../ExcludeDirectoryFilterIteratorTest.php | 27 + .../Iterator/FileTypeFilterIteratorTest.php | 9 + .../Tests/Iterator/RealIteratorTestCase.php | 9 + .../Iterator/SizeRangeFilterIteratorTest.php | 1 + .../Tests/Iterator/SortableIteratorTest.php | 109 +++- 11 files changed, 655 insertions(+), 51 deletions(-) diff --git a/src/Symfony/Component/Finder/CHANGELOG.md b/src/Symfony/Component/Finder/CHANGELOG.md index c795e54cf5a28..13bc98c65dd92 100644 --- a/src/Symfony/Component/Finder/CHANGELOG.md +++ b/src/Symfony/Component/Finder/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + + * added $useNaturalSort option to Finder::sortByName() method + 4.0.0 ----- diff --git a/src/Symfony/Component/Finder/Finder.php b/src/Symfony/Component/Finder/Finder.php index 105acc70ce41a..b7e388d1874ec 100644 --- a/src/Symfony/Component/Finder/Finder.php +++ b/src/Symfony/Component/Finder/Finder.php @@ -397,13 +397,17 @@ public function sort(\Closure $closure) * * This can be slow as all the matching files and directories must be retrieved for comparison. * + * @param bool $useNaturalSort Whether to use natural sort or not, disabled by default + * * @return $this * * @see SortableIterator */ - public function sortByName() + public function sortByName(/* bool $useNaturalSort = false */) { - $this->sort = Iterator\SortableIterator::SORT_BY_NAME; + $useNaturalSort = 0 < func_num_args() && func_get_arg(0); + + $this->sort = $useNaturalSort ? Iterator\SortableIterator::SORT_BY_NAME_NATURAL : Iterator\SortableIterator::SORT_BY_NAME; return $this; } diff --git a/src/Symfony/Component/Finder/Iterator/SortableIterator.php b/src/Symfony/Component/Finder/Iterator/SortableIterator.php index c2f54b937652f..4734a6ebdc74a 100644 --- a/src/Symfony/Component/Finder/Iterator/SortableIterator.php +++ b/src/Symfony/Component/Finder/Iterator/SortableIterator.php @@ -23,6 +23,7 @@ class SortableIterator implements \IteratorAggregate const SORT_BY_ACCESSED_TIME = 3; const SORT_BY_CHANGED_TIME = 4; const SORT_BY_MODIFIED_TIME = 5; + const SORT_BY_NAME_NATURAL = 6; private $iterator; private $sort; @@ -41,6 +42,10 @@ public function __construct(\Traversable $iterator, $sort) $this->sort = function ($a, $b) { return strcmp($a->getRealpath() ?: $a->getPathname(), $b->getRealpath() ?: $b->getPathname()); }; + } elseif (self::SORT_BY_NAME_NATURAL === $sort) { + $this->sort = function ($a, $b) { + return strnatcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); + }; } elseif (self::SORT_BY_TYPE === $sort) { $this->sort = function ($a, $b) { if ($a->isDir() && $b->isFile()) { diff --git a/src/Symfony/Component/Finder/Tests/FinderTest.php b/src/Symfony/Component/Finder/Tests/FinderTest.php index c7908fa86743d..134c93641b67f 100644 --- a/src/Symfony/Component/Finder/Tests/FinderTest.php +++ b/src/Symfony/Component/Finder/Tests/FinderTest.php @@ -24,33 +24,70 @@ public function testDirectories() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->directories()); - $this->assertIterator($this->toAbsolute(array('foo', 'toto')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array('foo', 'qux', 'toto')), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->directories(); $finder->files(); $finder->directories(); - $this->assertIterator($this->toAbsolute(array('foo', 'toto')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array('foo', 'qux', 'toto')), $finder->in(self::$tmpDir)->getIterator()); } public function testFiles() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->files()); - $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', + 'test.php', + 'test.py', + 'foo bar', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->files(); $finder->directories(); $finder->files(); - $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', + 'test.php', + 'test.py', + 'foo bar', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testRemoveTrailingSlash() { $finder = $this->buildFinder(); - $expected = $this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py', 'foo bar')); + $expected = $this->toAbsolute(array( + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'foo bar', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )); $in = self::$tmpDir.'//'; $this->assertIterator($expected, $finder->in($in)->files()->getIterator()); @@ -89,15 +126,43 @@ public function testDepth() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->depth('< 1')); - $this->assertIterator($this->toAbsolute(array('foo', 'test.php', 'test.py', 'toto', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array('foo', + 'test.php', + 'test.py', + 'toto', + 'foo bar', + 'qux', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $this->assertSame($finder, $finder->depth('<= 0')); - $this->assertIterator($this->toAbsolute(array('foo', 'test.php', 'test.py', 'toto', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array('foo', + 'test.php', + 'test.py', + 'toto', + 'foo bar', + 'qux', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $this->assertSame($finder, $finder->depth('>= 1')); - $this->assertIterator($this->toAbsolute(array('foo/bar.tmp')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo/bar.tmp', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->depth('< 1')->depth('>= 1'); @@ -108,7 +173,15 @@ public function testName() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->name('*.php')); - $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'test.php', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->name('test.ph*'); @@ -121,7 +194,15 @@ public function testName() $finder = $this->buildFinder(); $finder->name('~\\.php$~i'); - $this->assertIterator($this->toAbsolute(array('test.php')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'test.php', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->name('test.p{hp,y}'); @@ -132,12 +213,27 @@ public function testNotName() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->notName('*.php')); - $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.py', 'toto', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo/bar.tmp', + 'test.py', + 'toto', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->notName('*.php'); $finder->notName('*.py'); - $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'toto', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo/bar.tmp', + 'toto', + 'foo bar', + 'qux', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->name('test.ph*'); @@ -160,7 +256,10 @@ public function testRegexName($regex) { $finder = $this->buildFinder(); $finder->name($regex); - $this->assertIterator($this->toAbsolute(array('test.py', 'test.php')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'test.py', + 'test.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testSize() @@ -181,79 +280,356 @@ public function testExclude() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->exclude('foo')); - $this->assertIterator($this->toAbsolute(array('test.php', 'test.py', 'toto', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'test.php', + 'test.py', + 'toto', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testIgnoreVCS() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->ignoreVCS(false)->ignoreDotFiles(false)); - $this->assertIterator($this->toAbsolute(array('.git', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', 'toto/.git', '.bar', '.foo', '.foo/.bar', '.foo/bar', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + '.git', + 'foo', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + 'toto/.git', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->ignoreVCS(false)->ignoreVCS(false)->ignoreDotFiles(false); - $this->assertIterator($this->toAbsolute(array('.git', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', 'toto/.git', '.bar', '.foo', '.foo/.bar', '.foo/bar', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + '.git', + 'foo', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + 'toto/.git', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $this->assertSame($finder, $finder->ignoreVCS(true)->ignoreDotFiles(false)); - $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', '.bar', '.foo', '.foo/.bar', '.foo/bar', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testIgnoreDotFiles() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->ignoreDotFiles(false)->ignoreVCS(false)); - $this->assertIterator($this->toAbsolute(array('.git', '.bar', '.foo', '.foo/.bar', '.foo/bar', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', 'toto/.git', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + '.git', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + 'toto/.git', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $finder->ignoreDotFiles(false)->ignoreDotFiles(false)->ignoreVCS(false); - $this->assertIterator($this->toAbsolute(array('.git', '.bar', '.foo', '.foo/.bar', '.foo/bar', 'foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', 'toto/.git', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + '.git', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + 'toto/.git', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); $finder = $this->buildFinder(); $this->assertSame($finder, $finder->ignoreDotFiles(true)->ignoreVCS(false)); - $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testSortByName() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->sortByName()); - $this->assertIterator($this->toAbsolute(array('foo', 'foo bar', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo bar', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testSortByType() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->sortByType()); - $this->assertIterator($this->toAbsolute(array('foo', 'foo bar', 'toto', 'foo/bar.tmp', 'test.php', 'test.py')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo bar', + 'toto', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testSortByAccessedTime() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->sortByAccessedTime()); - $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'toto', 'test.py', 'foo', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo/bar.tmp', + 'test.php', + 'toto', + 'test.py', + 'foo', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testSortByChangedTime() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->sortByChangedTime()); - $this->assertIterator($this->toAbsolute(array('toto', 'test.py', 'test.php', 'foo/bar.tmp', 'foo', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'toto', + 'test.py', + 'test.php', + 'foo/bar.tmp', + 'foo', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testSortByModifiedTime() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->sortByModifiedTime()); - $this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'toto', 'test.py', 'foo', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo/bar.tmp', + 'test.php', + 'toto', + 'test.py', + 'foo', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); + } + + public function testSortByNameNatural() + { + $finder = $this->buildFinder(); + $this->assertSame($finder, $finder->sortByName(true)); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo bar', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + )), $finder->in(self::$tmpDir)->getIterator()); + + $finder = $this->buildFinder(); + $this->assertSame($finder, $finder->sortByName(false)); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo bar', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testSort() { $finder = $this->buildFinder(); $this->assertSame($finder, $finder->sort(function (\SplFileInfo $a, \SplFileInfo $b) { return strcmp($a->getRealPath(), $b->getRealPath()); })); - $this->assertIterator($this->toAbsolute(array('foo', 'foo bar', 'foo/bar.tmp', 'test.php', 'test.py', 'toto')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo bar', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testFilter() @@ -271,7 +647,23 @@ public function testFollowLinks() $finder = $this->buildFinder(); $this->assertSame($finder, $finder->followLinks()); - $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'test.php', 'test.py', 'toto', 'foo bar')), $finder->in(self::$tmpDir)->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo', + 'foo/bar.tmp', + 'test.php', + 'test.py', + 'toto', + 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + )), $finder->in(self::$tmpDir)->getIterator()); } public function testIn() @@ -283,6 +675,12 @@ public function testIn() self::$tmpDir.DIRECTORY_SEPARATOR.'test.php', __DIR__.DIRECTORY_SEPARATOR.'FinderTest.php', __DIR__.DIRECTORY_SEPARATOR.'GlobTest.php', + self::$tmpDir.DIRECTORY_SEPARATOR.'qux_0_1.php', + self::$tmpDir.DIRECTORY_SEPARATOR.'qux_1000_1.php', + self::$tmpDir.DIRECTORY_SEPARATOR.'qux_1002_0.php', + self::$tmpDir.DIRECTORY_SEPARATOR.'qux_10_2.php', + self::$tmpDir.DIRECTORY_SEPARATOR.'qux_12_0.php', + self::$tmpDir.DIRECTORY_SEPARATOR.'qux_2_0.php', ); $this->assertIterator($expected, $iterator); @@ -339,7 +737,7 @@ public function testGetIterator() $dirs[] = (string) $dir; } - $expected = $this->toAbsolute(array('foo', 'toto')); + $expected = $this->toAbsolute(array('foo', 'qux', 'toto')); sort($dirs); sort($expected); @@ -347,7 +745,7 @@ public function testGetIterator() $this->assertEquals($expected, $dirs, 'implements the \IteratorAggregate interface'); $finder = $this->buildFinder(); - $this->assertEquals(2, iterator_count($finder->directories()->in(self::$tmpDir)), 'implements the \IteratorAggregate interface'); + $this->assertEquals(3, iterator_count($finder->directories()->in(self::$tmpDir)), 'implements the \IteratorAggregate interface'); $finder = $this->buildFinder(); $a = iterator_to_array($finder->directories()->in(self::$tmpDir)); @@ -366,7 +764,7 @@ public function testRelativePath() $paths[] = $file->getRelativePath(); } - $ref = array('', '', '', '', 'foo', ''); + $ref = array('', '', '', '', '', '', '', '', '', '', '', 'foo', 'qux', 'qux', ''); sort($ref); sort($paths); @@ -384,7 +782,23 @@ public function testRelativePathname() $paths[] = $file->getRelativePathname(); } - $ref = array('test.php', 'toto', 'test.py', 'foo', 'foo'.DIRECTORY_SEPARATOR.'bar.tmp', 'foo bar'); + $ref = array( + 'test.php', + 'toto', + 'test.py', + 'foo', + 'foo'.DIRECTORY_SEPARATOR.'bar.tmp', + 'foo bar', + 'qux', + 'qux'.DIRECTORY_SEPARATOR.'baz_100_1.py', + 'qux'.DIRECTORY_SEPARATOR.'baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + ); sort($paths); sort($ref); @@ -402,7 +816,7 @@ public function testAppendWithAFinder() $finder = $finder->append($finder1); - $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'toto')), $finder->getIterator()); + $this->assertIterator($this->toAbsolute(array('foo', 'foo/bar.tmp', 'qux', 'toto')), $finder->getIterator()); } public function testAppendWithAnArray() @@ -601,7 +1015,7 @@ public function getContainsTestData() public function getRegexNameTestData() { return array( - array('~.+\\.p.+~i'), + array('~.*t\\.p.+~i'), array('~t.*s~i'), ); } @@ -718,7 +1132,20 @@ public function testIgnoredAccessDeniedException() chmod($testDir, 0333); if (false === ($couldRead = is_readable($testDir))) { - $this->assertIterator($this->toAbsolute(array('foo bar', 'test.php', 'test.py')), $finder->getIterator()); + $this->assertIterator($this->toAbsolute(array( + 'foo bar', + 'test.php', + 'test.py', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + ) + ), $finder->getIterator()); } // restore original permissions diff --git a/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php index 1ec70518278aa..6da91da95c50d 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/DateRangeFilterIteratorTest.php @@ -45,6 +45,15 @@ public function getAcceptData() '.foo/.bar', 'foo bar', '.foo/bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); $since2MonthsAgo = array( @@ -58,6 +67,15 @@ public function getAcceptData() '.foo/.bar', 'foo bar', '.foo/bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); $untilLastMonth = array( diff --git a/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeFilterIteratorTest.php index 2e90140530cd3..3a403cb9559e8 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/DepthRangeFilterIteratorTest.php @@ -41,6 +41,13 @@ public function getAcceptData() '.foo', '.bar', 'foo bar', + 'qux', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); $lessThanOrEqualTo1 = array( @@ -56,6 +63,15 @@ public function getAcceptData() '.bar', 'foo bar', '.foo/bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); $graterThanOrEqualTo1 = array( @@ -63,6 +79,8 @@ public function getAcceptData() 'foo/bar.tmp', '.foo/.bar', '.foo/bar', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', ); $equalTo1 = array( @@ -70,6 +88,8 @@ public function getAcceptData() 'foo/bar.tmp', '.foo/.bar', '.foo/bar', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', ); return array( diff --git a/src/Symfony/Component/Finder/Tests/Iterator/ExcludeDirectoryFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/ExcludeDirectoryFilterIteratorTest.php index fa192c31f4dec..c977b0cfdb383 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/ExcludeDirectoryFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/ExcludeDirectoryFilterIteratorTest.php @@ -41,6 +41,15 @@ public function getAcceptData() 'toto', 'toto/.git', 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); $fo = array( @@ -56,6 +65,15 @@ public function getAcceptData() 'toto', 'toto/.git', 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); $toto = array( @@ -69,6 +87,15 @@ public function getAcceptData() 'foo/bar.tmp', 'test.php', 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); return array( diff --git a/src/Symfony/Component/Finder/Tests/Iterator/FileTypeFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/FileTypeFilterIteratorTest.php index 4350b00ca940a..0ecd8dfe7346e 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/FileTypeFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/FileTypeFilterIteratorTest.php @@ -37,11 +37,20 @@ public function getAcceptData() '.foo/.bar', '.foo/bar', 'foo bar', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ); $onlyDirectories = array( '.git', 'foo', + 'qux', 'toto', 'toto/.git', '.foo', diff --git a/src/Symfony/Component/Finder/Tests/Iterator/RealIteratorTestCase.php b/src/Symfony/Component/Finder/Tests/Iterator/RealIteratorTestCase.php index 94253c7ee7ca2..ea00122f76265 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/RealIteratorTestCase.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/RealIteratorTestCase.php @@ -33,6 +33,15 @@ public static function setUpBeforeClass() 'toto/', 'toto/.git/', 'foo bar', + 'qux_0_1.php', + 'qux_2_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux/', + 'qux/baz_1_2.py', + 'qux/baz_100_1.py', ); self::$files = self::toAbsolute(self::$files); diff --git a/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php index 6d75b0f2f0b18..9bd34855b17c9 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/SizeRangeFilterIteratorTest.php @@ -34,6 +34,7 @@ public function getAcceptData() '.foo', '.git', 'foo', + 'qux', 'test.php', 'toto', 'toto/.git', diff --git a/src/Symfony/Component/Finder/Tests/Iterator/SortableIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/SortableIteratorTest.php index 444654a28fb61..9428013a4f49c 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/SortableIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/SortableIteratorTest.php @@ -82,6 +82,15 @@ public function getAcceptData() 'foo', 'foo bar', 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', 'test.php', 'test.py', 'toto', @@ -92,6 +101,7 @@ public function getAcceptData() '.foo', '.git', 'foo', + 'qux', 'toto', 'toto/.git', '.bar', @@ -99,25 +109,18 @@ public function getAcceptData() '.foo/bar', 'foo bar', 'foo/bar.tmp', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', 'test.php', 'test.py', ); - $customComparison = array( - '.bar', - '.foo', - '.foo/.bar', - '.foo/bar', - '.git', - 'foo', - 'foo bar', - 'foo/bar.tmp', - 'test.php', - 'test.py', - 'toto', - 'toto/.git', - ); - $sortByAccessedTime = array( // For these two files the access time was set to 2005-10-15 array('foo/bar.tmp', 'test.php'), @@ -132,6 +135,15 @@ public function getAcceptData() 'toto', 'toto/.git', 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ), // This file was accessed after sleeping for 1 sec array('.bar'), @@ -149,6 +161,15 @@ public function getAcceptData() 'toto', 'toto/.git', 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ), array('test.php'), array('test.py'), @@ -166,17 +187,75 @@ public function getAcceptData() 'toto', 'toto/.git', 'foo bar', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', ), array('test.php'), array('test.py'), ); + $sortByNameNatural = array( + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + '.git', + 'foo', + 'foo/bar.tmp', + 'foo bar', + 'qux', + 'qux/baz_1_2.py', + 'qux/baz_100_1.py', + 'qux_0_1.php', + 'qux_2_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'test.php', + 'test.py', + 'toto', + 'toto/.git', + ); + + $customComparison = array( + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + '.git', + 'foo', + 'foo bar', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + 'toto/.git', + ); + return array( array(SortableIterator::SORT_BY_NAME, $this->toAbsolute($sortByName)), array(SortableIterator::SORT_BY_TYPE, $this->toAbsolute($sortByType)), array(SortableIterator::SORT_BY_ACCESSED_TIME, $this->toAbsolute($sortByAccessedTime)), array(SortableIterator::SORT_BY_CHANGED_TIME, $this->toAbsolute($sortByChangedTime)), array(SortableIterator::SORT_BY_MODIFIED_TIME, $this->toAbsolute($sortByModifiedTime)), + array(SortableIterator::SORT_BY_NAME_NATURAL, $this->toAbsolute($sortByNameNatural)), array(function (\SplFileInfo $a, \SplFileInfo $b) { return strcmp($a->getRealPath(), $b->getRealPath()); }, $this->toAbsolute($customComparison)), ); } From 32fc58df8b7cc4079ccb6d1571d0b3ca091b4cf4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 4 May 2018 20:14:52 -0700 Subject: [PATCH 002/125] [DI] Allow binding by type+name --- .../Compiler/AutowirePass.php | 2 +- .../Compiler/ResolveBindingsPass.php | 14 +++++++---- .../DependencyInjection/Definition.php | 4 ++++ .../Loader/Configurator/Traits/BindTrait.php | 2 +- .../Compiler/ResolveBindingsPassTest.php | 24 +++++++++++++++++++ .../Tests/Fixtures/NamedArgumentsDummy.php | 4 ++++ ...RegisterControllerArgumentLocatorsPass.php | 7 +++--- ...sterControllerArgumentLocatorsPassTest.php | 21 ++++++++++++---- 8 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index f94284d2e0722..419d955d3c333 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -208,7 +208,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a continue; } $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, false); - $type = $type ? sprintf('is type-hinted "%s"', $type) : 'has no type-hint'; + $type = $type ? sprintf('is type-hinted "%s"', ltrim($type, '\\')) : 'has no type-hint'; throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type)); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php index c82a974360674..5ab1a0138dc10 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php @@ -74,7 +74,7 @@ protected function processValue($value, $isRoot = false) $this->unusedBindings[$bindingId] = array($key, $this->currentId); } - if (isset($key[0]) && '$' === $key[0]) { + if (preg_match('/^(?:(?:array|bool|float|int|string) )?\$/', $key)) { continue; } @@ -113,15 +113,21 @@ protected function processValue($value, $isRoot = false) continue; } + $typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter); + + if (array_key_exists($k = ltrim($typeHint, '\\').' $'.$parameter->name, $bindings)) { + $arguments[$key] = $this->getBindingValue($bindings[$k]); + + 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])) { + if (!$typeHint || '\\' !== $typeHint[0] || !isset($bindings[$typeHint = substr($typeHint, 1)])) { continue; } diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index b6ddff186aedf..807ce13a07edd 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -859,6 +859,10 @@ public function getBindings() public function setBindings(array $bindings) { foreach ($bindings as $key => $binding) { + if (0 < strpos($key, '$') && $key !== $k = preg_replace('/[ \t]*\$/', ' $', $key)) { + unset($bindings[$key]); + $bindings[$key = $k] = $binding; + } if (!$binding instanceof BoundArgument) { $bindings[$key] = new BoundArgument($binding); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php index 4511ed659d4e5..5d6c57cb3b9b2 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php @@ -31,7 +31,7 @@ trait BindTrait final public function bind($nameOrFqcn, $valueOrRef) { $valueOrRef = static::processValue($valueOrRef, true); - if (isset($nameOrFqcn[0]) && '$' !== $nameOrFqcn[0] && !$valueOrRef instanceof Reference) { + if (!preg_match('/^(?:(?:array|bool|float|int|string)[ \t]*+)?\$/', $nameOrFqcn) && !$valueOrRef instanceof Reference) { throw new InvalidArgumentException(sprintf('Invalid binding for service "%s": named arguments must start with a "$", and FQCN must map to references. Neither applies to binding "%s".', $this->id, $nameOrFqcn)); } $bindings = $this->definition->getBindings(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index 16e486afafe2e..cf68841ca8974 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -95,4 +95,28 @@ public function testScalarSetter() $this->assertEquals(array(array('setDefaultLocale', array('fr'))), $definition->getMethodCalls()); } + + public function testTupleBinding() + { + $container = new ContainerBuilder(); + + $bindings = array( + '$c' => new BoundArgument(new Reference('bar')), + CaseSensitiveClass::class.'$c' => new BoundArgument(new Reference('foo')), + ); + + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); + $definition->addMethodCall('setSensitiveClass'); + $definition->addMethodCall('setAnotherC'); + $definition->setBindings($bindings); + + $pass = new ResolveBindingsPass(); + $pass->process($container); + + $expected = array( + array('setSensitiveClass', array(new Reference('foo'))), + array('setAnotherC', array(new Reference('bar'))), + ); + $this->assertEquals($expected, $definition->getMethodCalls()); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php index 09d907dfae769..802ba842715bf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php @@ -18,4 +18,8 @@ public function setApiKey($apiKey) public function setSensitiveClass(CaseSensitiveClass $c) { } + + public function setAnotherC($c) + { + } } diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index cd76e56caab83..f8f924ab01698 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -120,7 +120,7 @@ public function process(ContainerBuilder $container) $args = array(); foreach ($parameters as $p) { /** @var \ReflectionParameter $p */ - $type = $target = ProxyHelper::getTypeHint($r, $p, true); + $type = ltrim($target = ProxyHelper::getTypeHint($r, $p), '\\'); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; if (isset($arguments[$r->name][$p->name])) { @@ -132,7 +132,7 @@ public function process(ContainerBuilder $container) } elseif ($p->allowsNull() && !$p->isOptional()) { $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; } - } elseif (isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) { + } elseif (isset($bindings[$bindingName = $type.' $'.$p->name]) || isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) { $binding = $bindings[$bindingName]; list($bindingValue, $bindingId) = $binding->getValues(); @@ -148,7 +148,7 @@ public function process(ContainerBuilder $container) } continue; - } elseif (!$type || !$autowire) { + } elseif (!$type || !$autowire || '\\' !== $target[0]) { continue; } elseif (!$p->allowsNull()) { $invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE; @@ -169,6 +169,7 @@ public function process(ContainerBuilder $container) throw new InvalidArgumentException($message); } + $target = ltrim($target, '\\'); $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior) : new Reference($target, $invalidBehavior); } // register the maps as a per-method service-locators diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 98c77f4aa32d1..189515496ca81 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -308,16 +308,23 @@ public function testBindings($bindingName) public function provideBindings() { - return array(array(ControllerDummy::class), array('$bar')); + return array( + array(ControllerDummy::class.'$bar'), + array(ControllerDummy::class), + array('$bar'), + ); } - public function testBindScalarValueToControllerArgument() + /** + * @dataProvider provideBindScalarValueToControllerArgument + */ + public function testBindScalarValueToControllerArgument($bindingKey) { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument(array()); $container->register('foo', ArgumentWithoutTypeController::class) - ->setBindings(array('$someArg' => '%foo%')) + ->setBindings(array($bindingKey => '%foo%')) ->addTag('controller.service_arguments'); $container->setParameter('foo', 'foo_val'); @@ -339,6 +346,12 @@ public function testBindScalarValueToControllerArgument() $this->assertTrue($container->has((string) $reference)); $this->assertSame('foo_val', $container->get((string) $reference)); } + + public function provideBindScalarValueToControllerArgument() + { + yield array('$someArg'); + yield array('string $someArg'); + } } class RegisterTestController @@ -396,7 +409,7 @@ public function barAction(NonExistentClass $nonExistent = null, $bar) class ArgumentWithoutTypeController { - public function fooAction($someArg) + public function fooAction(string $someArg) { } } From f1e9aa4ed84e424248b1dca52572e1b90b187dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 3 May 2018 15:47:55 +0200 Subject: [PATCH 003/125] [HttpKernel] Better exception page when the controller returns nothing --- ...ntrollerDoesNotReturnResponseException.php | 78 +++++++++++++++++++ .../Component/HttpKernel/HttpKernel.php | 4 +- .../HttpKernel/Tests/HttpKernelTest.php | 17 +++- 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Exception/ControllerDoesNotReturnResponseException.php diff --git a/src/Symfony/Component/HttpKernel/Exception/ControllerDoesNotReturnResponseException.php b/src/Symfony/Component/HttpKernel/Exception/ControllerDoesNotReturnResponseException.php new file mode 100644 index 0000000000000..2abc4069cf1be --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Exception/ControllerDoesNotReturnResponseException.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\HttpKernel\Exception; + +/** + * @author Grégoire Pineau + */ +class ControllerDoesNotReturnResponseException extends \LogicException +{ + public function __construct(string $message, callable $controller, string $file, int $line) + { + parent::__construct($message); + + if (!$controllerDefinition = $this->parseControllerDefinition($controller)) { + return; + } + + $this->file = $controllerDefinition['file']; + $this->line = $controllerDefinition['line']; + $r = new \ReflectionProperty(\Exception::class, 'trace'); + $r->setAccessible(true); + $r->setValue($this, array_merge(array( + array( + 'line' => $line, + 'file' => $file, + ), + ), $this->getTrace())); + } + + private function parseControllerDefinition(callable $controller): ?array + { + if (is_string($controller) && false !== strpos($controller, '::')) { + $controller = explode('::', $controller); + } + + if (is_array($controller)) { + try { + $r = new \ReflectionMethod($controller[0], $controller[1]); + + return array( + 'file' => $r->getFileName(), + 'line' => $r->getEndLine(), + ); + } catch (\ReflectionException $e) { + return null; + } + } + + if ($controller instanceof \Closure) { + $r = new \ReflectionFunction($controller); + + return array( + 'file' => $r->getFileName(), + 'line' => $r->getEndLine(), + ); + } + + if (is_object($controller)) { + $r = new \ReflectionClass($controller); + + return array( + 'file' => $r->getFileName(), + 'line' => $r->getEndLine(), + ); + } + + return null; + } +} diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index d4ef09e8478ff..9ebf5847b437e 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\ControllerDoesNotReturnResponseException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Event\FilterControllerEvent; @@ -162,7 +163,8 @@ private function handleRaw(Request $request, int $type = self::MASTER_REQUEST) if (null === $response) { $msg .= ' Did you forget to add a return statement somewhere in your controller?'; } - throw new \LogicException($msg); + + throw new ControllerDoesNotReturnResponseException($msg, $controller, __FILE__, __LINE__ - 17); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index 0adef984c6f5c..bb19c496cce89 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -23,6 +23,7 @@ use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\ControllerDoesNotReturnResponseException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -211,15 +212,23 @@ public function testHandleWhenTheControllerIsAStaticArray() $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } - /** - * @expectedException \LogicException - */ public function testHandleWhenTheControllerDoesNotReturnAResponse() { $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, function () { return 'foo'; }); - $kernel->handle(new Request()); + try { + $kernel->handle(new Request()); + + $this->fail('The kernel should throw an exception.'); + } catch (ControllerDoesNotReturnResponseException $e) { + } + + $first = $e->getTrace()[0]; + + // `file` index the array starting at 0, and __FILE__ starts at 1 + $line = file($first['file'])[$first['line'] - 1]; + $this->assertContains('call_user_func_array', $line); } public function testHandleWhenTheControllerDoesNotReturnAResponseButAViewIsRegistered() From 016d5562628af6935bbf76e2eb3fb99683f7cf7b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 7 May 2018 16:51:25 +0200 Subject: [PATCH 004/125] updated version to 4.2 --- composer.json | 2 +- src/Symfony/Bridge/Doctrine/composer.json | 2 +- src/Symfony/Bridge/Monolog/composer.json | 2 +- src/Symfony/Bridge/PhpUnit/composer.json | 2 +- src/Symfony/Bridge/ProxyManager/composer.json | 2 +- src/Symfony/Bridge/Twig/composer.json | 2 +- src/Symfony/Bundle/DebugBundle/composer.json | 2 +- src/Symfony/Bundle/FrameworkBundle/composer.json | 2 +- src/Symfony/Bundle/SecurityBundle/composer.json | 2 +- src/Symfony/Bundle/TwigBundle/composer.json | 2 +- src/Symfony/Bundle/WebProfilerBundle/composer.json | 2 +- src/Symfony/Bundle/WebServerBundle/composer.json | 2 +- src/Symfony/Component/Asset/composer.json | 2 +- src/Symfony/Component/BrowserKit/composer.json | 2 +- src/Symfony/Component/Cache/composer.json | 2 +- src/Symfony/Component/Config/composer.json | 2 +- src/Symfony/Component/Console/composer.json | 2 +- src/Symfony/Component/CssSelector/composer.json | 2 +- src/Symfony/Component/Debug/composer.json | 2 +- .../Component/DependencyInjection/composer.json | 2 +- src/Symfony/Component/DomCrawler/composer.json | 2 +- src/Symfony/Component/Dotenv/composer.json | 2 +- src/Symfony/Component/EventDispatcher/composer.json | 2 +- src/Symfony/Component/ExpressionLanguage/composer.json | 2 +- src/Symfony/Component/Filesystem/composer.json | 2 +- src/Symfony/Component/Finder/composer.json | 2 +- src/Symfony/Component/Form/composer.json | 2 +- src/Symfony/Component/HttpFoundation/composer.json | 2 +- src/Symfony/Component/HttpKernel/Kernel.php | 10 +++++----- src/Symfony/Component/HttpKernel/composer.json | 2 +- src/Symfony/Component/Inflector/composer.json | 2 +- src/Symfony/Component/Intl/composer.json | 2 +- src/Symfony/Component/Ldap/composer.json | 2 +- src/Symfony/Component/Lock/composer.json | 2 +- src/Symfony/Component/Messenger/composer.json | 2 +- src/Symfony/Component/OptionsResolver/composer.json | 2 +- src/Symfony/Component/Process/composer.json | 2 +- src/Symfony/Component/PropertyAccess/composer.json | 2 +- src/Symfony/Component/PropertyInfo/composer.json | 2 +- src/Symfony/Component/Routing/composer.json | 2 +- src/Symfony/Component/Security/Core/composer.json | 2 +- src/Symfony/Component/Security/Csrf/composer.json | 2 +- src/Symfony/Component/Security/Guard/composer.json | 2 +- src/Symfony/Component/Security/Http/composer.json | 2 +- src/Symfony/Component/Security/composer.json | 2 +- src/Symfony/Component/Serializer/composer.json | 2 +- src/Symfony/Component/Stopwatch/composer.json | 2 +- src/Symfony/Component/Templating/composer.json | 2 +- src/Symfony/Component/Translation/composer.json | 2 +- src/Symfony/Component/Validator/composer.json | 2 +- src/Symfony/Component/VarDumper/composer.json | 2 +- src/Symfony/Component/WebLink/composer.json | 2 +- src/Symfony/Component/Workflow/composer.json | 2 +- src/Symfony/Component/Yaml/composer.json | 2 +- 54 files changed, 58 insertions(+), 58 deletions(-) diff --git a/composer.json b/composer.json index cd86573f80797..3a861887ce60d 100644 --- a/composer.json +++ b/composer.json @@ -133,7 +133,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index f36c2d647c563..4f6e21f74b784 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -58,7 +58,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index a4d3e984100c9..9d0d6acf85f7d 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -44,7 +44,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json index 7338fca00db74..4d72603eb452f 100644 --- a/src/Symfony/Bridge/PhpUnit/composer.json +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -40,7 +40,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" }, "thanks": { "name": "phpunit/phpunit", diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 90f000c828618..a224d5a8a90ad 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 14c80b21a33e2..81bab7092dc45 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -68,7 +68,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json index 7897621b54e10..ec781784640d7 100644 --- a/src/Symfony/Bundle/DebugBundle/composer.json +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -43,7 +43,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 8c1c32194fdea..0e3f16fb8effc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -93,7 +93,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 58e34ac55ae6e..e9dc497220fea 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -58,7 +58,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index ab5c539735e25..bd5fdc6ef8292 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -52,7 +52,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index df801f033d592..4e3783d695dcb 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -44,7 +44,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Bundle/WebServerBundle/composer.json b/src/Symfony/Bundle/WebServerBundle/composer.json index 81eaaad4488fe..4a981ea93e419 100644 --- a/src/Symfony/Bundle/WebServerBundle/composer.json +++ b/src/Symfony/Bundle/WebServerBundle/composer.json @@ -37,7 +37,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json index bbd74fe239346..bcf70fc18318f 100644 --- a/src/Symfony/Component/Asset/composer.json +++ b/src/Symfony/Component/Asset/composer.json @@ -34,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/BrowserKit/composer.json b/src/Symfony/Component/BrowserKit/composer.json index 6c226bed79c55..0607378c4d367 100644 --- a/src/Symfony/Component/BrowserKit/composer.json +++ b/src/Symfony/Component/BrowserKit/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index 9005eeb1462e2..869a9bbd5ab06 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -43,7 +43,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Config/composer.json b/src/Symfony/Component/Config/composer.json index eb5bdbb51054d..35fbffdb69a41 100644 --- a/src/Symfony/Component/Config/composer.json +++ b/src/Symfony/Component/Config/composer.json @@ -41,7 +41,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index d3aadfbc1d2f6..48da2819ff232 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -46,7 +46,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/CssSelector/composer.json b/src/Symfony/Component/CssSelector/composer.json index e2ed078e364af..ebe7d0d5c1eea 100644 --- a/src/Symfony/Component/CssSelector/composer.json +++ b/src/Symfony/Component/CssSelector/composer.json @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Debug/composer.json b/src/Symfony/Component/Debug/composer.json index 9b2deebe6864a..45799e2e60f67 100644 --- a/src/Symfony/Component/Debug/composer.json +++ b/src/Symfony/Component/Debug/composer.json @@ -34,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 8230395645c7c..4578af6ffd3dd 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -49,7 +49,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/DomCrawler/composer.json b/src/Symfony/Component/DomCrawler/composer.json index afb44bd7843c0..167d0fdfe50c9 100644 --- a/src/Symfony/Component/DomCrawler/composer.json +++ b/src/Symfony/Component/DomCrawler/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Dotenv/composer.json b/src/Symfony/Component/Dotenv/composer.json index 90ffb8f5f5ca7..6590da03e5169 100644 --- a/src/Symfony/Component/Dotenv/composer.json +++ b/src/Symfony/Component/Dotenv/composer.json @@ -30,7 +30,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json index 12ee53270edb9..faad1204a87cb 100644 --- a/src/Symfony/Component/EventDispatcher/composer.json +++ b/src/Symfony/Component/EventDispatcher/composer.json @@ -41,7 +41,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/ExpressionLanguage/composer.json b/src/Symfony/Component/ExpressionLanguage/composer.json index 3c453b8f49f12..489612428c59b 100644 --- a/src/Symfony/Component/ExpressionLanguage/composer.json +++ b/src/Symfony/Component/ExpressionLanguage/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Filesystem/composer.json b/src/Symfony/Component/Filesystem/composer.json index 77f62b75b04c6..ee8a319a7d1c5 100644 --- a/src/Symfony/Component/Filesystem/composer.json +++ b/src/Symfony/Component/Filesystem/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Finder/composer.json b/src/Symfony/Component/Finder/composer.json index dd37f2e0ee1f5..37d34a5e51d76 100644 --- a/src/Symfony/Component/Finder/composer.json +++ b/src/Symfony/Component/Finder/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 7700d75b54874..4e4f65ebf3d3c 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -59,7 +59,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index ef4bf826e5454..76381a7c46a05 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index cfaef24ac3a85..cd90b037d8224 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -63,15 +63,15 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '4.1.0-DEV'; - const VERSION_ID = 40100; + const VERSION = '4.2.0-DEV'; + const VERSION_ID = 40200; const MAJOR_VERSION = 4; - const MINOR_VERSION = 1; + const MINOR_VERSION = 2; const RELEASE_VERSION = 0; const EXTRA_VERSION = 'DEV'; - const END_OF_MAINTENANCE = '01/2019'; - const END_OF_LIFE = '07/2019'; + const END_OF_MAINTENANCE = '07/2019'; + const END_OF_LIFE = '01/2020'; public function __construct(string $environment, bool $debug) { diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index e610f27418777..816af1222faf5 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -65,7 +65,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Inflector/composer.json b/src/Symfony/Component/Inflector/composer.json index 36b9b77d81a9e..b4312cd037dfe 100644 --- a/src/Symfony/Component/Inflector/composer.json +++ b/src/Symfony/Component/Inflector/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Intl/composer.json b/src/Symfony/Component/Intl/composer.json index 874be36cb3bb6..b0f3e3b6d53c7 100644 --- a/src/Symfony/Component/Intl/composer.json +++ b/src/Symfony/Component/Intl/composer.json @@ -43,7 +43,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Ldap/composer.json b/src/Symfony/Component/Ldap/composer.json index 10bb67cf3fa89..2c69b7dfe0ce6 100644 --- a/src/Symfony/Component/Ldap/composer.json +++ b/src/Symfony/Component/Ldap/composer.json @@ -29,7 +29,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Lock/composer.json b/src/Symfony/Component/Lock/composer.json index 1ca9a4da40220..8aaf0eab94d25 100644 --- a/src/Symfony/Component/Lock/composer.json +++ b/src/Symfony/Component/Lock/composer.json @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index 1767f4dab967e..5bb25399574dd 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -40,7 +40,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/OptionsResolver/composer.json b/src/Symfony/Component/OptionsResolver/composer.json index e78b66f25c105..1c819495bf89d 100644 --- a/src/Symfony/Component/OptionsResolver/composer.json +++ b/src/Symfony/Component/OptionsResolver/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Process/composer.json b/src/Symfony/Component/Process/composer.json index 58f4005b594b7..44bad06b597b9 100644 --- a/src/Symfony/Component/Process/composer.json +++ b/src/Symfony/Component/Process/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json index bacba378a4434..33f9b188d0d1c 100644 --- a/src/Symfony/Component/PropertyAccess/composer.json +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -34,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json index 0350fb411f315..a7531a3cc4c76 100644 --- a/src/Symfony/Component/PropertyInfo/composer.json +++ b/src/Symfony/Component/PropertyInfo/composer.json @@ -53,7 +53,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index a558a1d749f3f..df999a11f7f0b 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -50,7 +50,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index 30c42bb80f6b0..60603bff8b4ca 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -44,7 +44,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Security/Csrf/composer.json b/src/Symfony/Component/Security/Csrf/composer.json index f13de8fefb2d1..cdc19ade22f15 100644 --- a/src/Symfony/Component/Security/Csrf/composer.json +++ b/src/Symfony/Component/Security/Csrf/composer.json @@ -37,7 +37,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Security/Guard/composer.json b/src/Symfony/Component/Security/Guard/composer.json index e1faa18d81f47..982b16f012a32 100644 --- a/src/Symfony/Component/Security/Guard/composer.json +++ b/src/Symfony/Component/Security/Guard/composer.json @@ -32,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 7bf03b6e874d4..3eb368c078fef 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -41,7 +41,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index 965f881bdfdf4..bf04e86730388 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -55,7 +55,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index a11c952f6a578..52961bb7c0528 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -58,7 +58,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Stopwatch/composer.json b/src/Symfony/Component/Stopwatch/composer.json index cb1f823cc49f7..26c6c3e922bd9 100644 --- a/src/Symfony/Component/Stopwatch/composer.json +++ b/src/Symfony/Component/Stopwatch/composer.json @@ -27,7 +27,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Templating/composer.json b/src/Symfony/Component/Templating/composer.json index 9e7f3e1bb815e..9e56b0247cc88 100644 --- a/src/Symfony/Component/Templating/composer.json +++ b/src/Symfony/Component/Templating/composer.json @@ -34,7 +34,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 64ab46b31b732..2991949987471 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -47,7 +47,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index c9b201d81c5f6..531ffd9d57101 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -64,7 +64,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json index 59e9201720589..5cf48475a78e1 100644 --- a/src/Symfony/Component/VarDumper/composer.json +++ b/src/Symfony/Component/VarDumper/composer.json @@ -47,7 +47,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/WebLink/composer.json b/src/Symfony/Component/WebLink/composer.json index 1211d16ea3d05..2c09787390037 100644 --- a/src/Symfony/Component/WebLink/composer.json +++ b/src/Symfony/Component/WebLink/composer.json @@ -37,7 +37,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Workflow/composer.json b/src/Symfony/Component/Workflow/composer.json index 643f762e4a27c..17500cbb2576f 100644 --- a/src/Symfony/Component/Workflow/composer.json +++ b/src/Symfony/Component/Workflow/composer.json @@ -37,7 +37,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } diff --git a/src/Symfony/Component/Yaml/composer.json b/src/Symfony/Component/Yaml/composer.json index 6537e42b2b073..c8b7123f239d4 100644 --- a/src/Symfony/Component/Yaml/composer.json +++ b/src/Symfony/Component/Yaml/composer.json @@ -37,7 +37,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } } } From a7c96963829667928cd84931ceaa935f5e9a1f26 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 4 May 2018 15:23:26 +0200 Subject: [PATCH 005/125] Added new templates for GitHub issues --- .github/ISSUE_TEMPLATE.md | 13 ---------- .github/ISSUE_TEMPLATE/Bug_report.md | 26 +++++++++++++++++++ .github/ISSUE_TEMPLATE/Documentation_issue.md | 15 +++++++++++ .github/ISSUE_TEMPLATE/Feature_request.md | 17 ++++++++++++ .github/ISSUE_TEMPLATE/Security_issue.md | 16 ++++++++++++ .github/ISSUE_TEMPLATE/Support_question.md | 16 ++++++++++++ 6 files changed, 90 insertions(+), 13 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/Bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/Documentation_issue.md create mode 100644 .github/ISSUE_TEMPLATE/Feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/Security_issue.md create mode 100644 .github/ISSUE_TEMPLATE/Support_question.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 6eaec7c81da9a..0000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ -| Q | A -| ---------------- | ----- -| Bug report? | yes/no -| Feature request? | yes/no -| BC Break report? | yes/no -| RFC? | yes/no -| Symfony version | x.y.z - - diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000000000..42173b6d49faa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug Report +about: Report errors and problems + +--- + + + +**Symfony version(s) affected**: x.y.z + +**Description** + + +**How to reproduce** + + +**Possible Solution** + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/Documentation_issue.md b/.github/ISSUE_TEMPLATE/Documentation_issue.md new file mode 100644 index 0000000000000..eed9470b6878f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Documentation_issue.md @@ -0,0 +1,15 @@ +--- +name: Documentation Issue +about: Anything related to Symfony Documentation + +--- + + + +Symfony Documentation has its own dedicated repository. Please open your +documentation-related issue at https://github.com/symfony/symfony-docs/issues + +Thanks! diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000000000..3422cc1353edc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature Request +about: RFC and ideas for new features and improvements + +--- + + + +**Description** + + +**Example** + diff --git a/.github/ISSUE_TEMPLATE/Security_issue.md b/.github/ISSUE_TEMPLATE/Security_issue.md new file mode 100644 index 0000000000000..29981939d043b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Security_issue.md @@ -0,0 +1,16 @@ +--- +name: Security Issue +about: Report security-related errors + +--- + + + +If you have found a security issue in Symfony, please send the details to +security [at] symfony.com and don't disclose it publicly until we can provide a +fix for it. + +More information: https://symfony.com/security diff --git a/.github/ISSUE_TEMPLATE/Support_question.md b/.github/ISSUE_TEMPLATE/Support_question.md new file mode 100644 index 0000000000000..40a9277035404 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Support_question.md @@ -0,0 +1,16 @@ +--- +name: Support Question +about: Questions about using Symfony and its components + +--- + + + +We use GitHub issues only to discuss about Symfony bugs and new features. For +this kind of questions about using Symfony or third-party bundles, please use +any of the support alternatives shown in https://symfony.com/support + +Thanks! From 3ae3a03f41c34d6224cd9787c8ca111f7a8a71a4 Mon Sep 17 00:00:00 2001 From: Baptiste Lafontaine Date: Fri, 27 Apr 2018 15:39:41 +0200 Subject: [PATCH 006/125] [DI][DX] Allow exclude to be an array of patterns (taking #24428 over) --- .../Configurator/PrototypeConfigurator.php | 12 +++++---- .../DependencyInjection/Loader/FileLoader.php | 16 ++++++------ .../Loader/XmlFileLoader.php | 9 ++++++- .../schema/dic/services/services-1.0.xsd | 1 + .../config/prototype_array.expected.yml | 25 +++++++++++++++++++ .../Tests/Fixtures/config/prototype_array.php | 22 ++++++++++++++++ .../Fixtures/xml/services_prototype_array.xml | 9 +++++++ .../Tests/Loader/FileLoaderTest.php | 19 ++++++++++++++ .../Tests/Loader/XmlFileLoaderTest.php | 20 +++++++++++++++ 9 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php index 573dcc51e8204..45e88a0bfaef0 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/PrototypeConfigurator.php @@ -39,7 +39,7 @@ class PrototypeConfigurator extends AbstractServiceConfigurator private $loader; private $resource; - private $exclude; + private $excludes; private $allowParent; public function __construct(ServicesConfigurator $parent, PhpFileLoader $loader, Definition $defaults, string $namespace, string $resource, bool $allowParent) @@ -63,19 +63,21 @@ public function __destruct() parent::__destruct(); if ($this->loader) { - $this->loader->registerClasses($this->definition, $this->id, $this->resource, $this->exclude); + $this->loader->registerClasses($this->definition, $this->id, $this->resource, $this->excludes); } $this->loader = null; } /** - * Excludes files from registration using a glob pattern. + * Excludes files from registration using glob patterns. + * + * @param string[]|string $excludes * * @return $this */ - final public function exclude(string $exclude) + final public function exclude($excludes) { - $this->exclude = $exclude; + $this->excludes = (array) $excludes; return $this; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 83a3f4f87ca45..b193354a6eae0 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -40,10 +40,10 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l /** * 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 + * @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|string[]|null $exclude A globbed path of files to exclude or an array of globbed paths of files to exclude */ public function registerClasses(Definition $prototype, $namespace, $resource, $exclude = null) { @@ -54,7 +54,7 @@ public function registerClasses(Definition $prototype, $namespace, $resource, $e throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: %s.', $namespace)); } - $classes = $this->findClasses($namespace, $resource, $exclude); + $classes = $this->findClasses($namespace, $resource, (array) $exclude); // prepare for deep cloning $serializedPrototype = serialize($prototype); $interfaces = array(); @@ -101,14 +101,14 @@ protected function setDefinition($id, Definition $definition) } } - private function findClasses($namespace, $pattern, $excludePattern) + private function findClasses($namespace, $pattern, array $excludePatterns) { $parameterBag = $this->container->getParameterBag(); $excludePaths = array(); $excludePrefix = null; - if ($excludePattern) { - $excludePattern = $parameterBag->unescapeValue($parameterBag->resolveValue($excludePattern)); + $excludePatterns = $parameterBag->unescapeValue($parameterBag->resolveValue($excludePatterns)); + foreach ($excludePatterns as $excludePattern) { foreach ($this->glob($excludePattern, true, $resource) as $path => $info) { if (null === $excludePrefix) { $excludePrefix = $resource->getPrefix(); diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 3b1c3548f6d55..9fbff021d680c 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -146,7 +146,14 @@ private function parseDefinitions(\DOMDocument $xml, $file, $defaults) foreach ($services as $service) { 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')); + $excludes = array_column($this->getChildren($service, 'exclude'), 'nodeValue'); + if ($service->hasAttribute('exclude')) { + if (count($excludes) > 0) { + throw new InvalidArgumentException('You cannot use both the attribute "exclude" and tags at the same time.'); + } + $excludes = array($service->getAttribute('exclude')); + } + $this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'), $excludes); } else { $this->setDefinition((string) $service->getAttribute('id'), $definition); } 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 60a01bd666aed..015f81f9536e8 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 @@ -160,6 +160,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml new file mode 100644 index 0000000000000..e8a03691c95c6 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.expected.yml @@ -0,0 +1,25 @@ + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo + public: true + tags: + - { name: foo } + - { name: baz } + deprecated: "%service_id%" + arguments: [1] + factory: f + Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar + public: true + tags: + - { name: foo } + - { name: baz } + deprecated: "%service_id%" + lazy: true + arguments: [1] + factory: f diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php new file mode 100644 index 0000000000000..4d5625c245aa7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/prototype_array.php @@ -0,0 +1,22 @@ +services()->defaults() + ->tag('baz'); + $di->load(Prototype::class.'\\', '../Prototype') + ->autoconfigure() + ->exclude(array('../Prototype/OtherDir', '../Prototype/BadClasses')) + ->factory('f') + ->deprecate('%service_id%') + ->args(array(0)) + ->args(array(1)) + ->autoconfigure(false) + ->tag('foo') + ->parent('foo'); + $di->set('foo')->lazy()->abstract(); + $di->get(Prototype\Foo::class)->lazy(false); +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml new file mode 100644 index 0000000000000..892e0a7e8c95d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_prototype_array.xml @@ -0,0 +1,9 @@ + + + + + ../Prototype/OtherDir + ../Prototype/BadClasses + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php index 8a271a818a475..a15b1b0315108 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php @@ -136,6 +136,25 @@ public function testRegisterClassesWithExclude() ); } + public function testRegisterClassesWithExcludeAsArray() + { + $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\\', + 'Prototype/*', array( + 'Prototype/%sub_dir%', + 'Prototype/OtherDir/AnotherSub/DeeperBaz.php', + ) + ); + $this->assertTrue($container->has(Foo::class)); + $this->assertTrue($container->has(Baz::class)); + $this->assertFalse($container->has(Bar::class)); + $this->assertFalse($container->has(DeeperBaz::class)); + } + public function testNestedRegisterClasses() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 7e0ce38719097..2e87291bf2c15 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -617,6 +617,26 @@ public function testPrototype() $this->assertContains('reflection.Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar', $resources); } + public function testPrototypeExcludeWithArray() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_prototype_array.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_array.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 From a05ae9b9cdd3977a1fde8dd146317f5de41c4fc1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 7 May 2018 16:41:05 -0700 Subject: [PATCH 007/125] Improve github issue templates --- .github/ISSUE_TEMPLATE/{Bug_report.md => 1_Bug_report.md} | 2 +- .../{Feature_request.md => 2_Feature_request.md} | 2 +- .../{Support_question.md => 3_Support_question.md} | 4 ++-- .../{Documentation_issue.md => 4_Documentation_issue.md} | 4 ++-- .../{Security_issue.md => 5_Security_issue.md} | 6 ++++-- 5 files changed, 10 insertions(+), 8 deletions(-) rename .github/ISSUE_TEMPLATE/{Bug_report.md => 1_Bug_report.md} (97%) rename .github/ISSUE_TEMPLATE/{Feature_request.md => 2_Feature_request.md} (95%) rename .github/ISSUE_TEMPLATE/{Support_question.md => 3_Support_question.md} (77%) rename .github/ISSUE_TEMPLATE/{Documentation_issue.md => 4_Documentation_issue.md} (75%) rename .github/ISSUE_TEMPLATE/{Security_issue.md => 5_Security_issue.md} (69%) diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md similarity index 97% rename from .github/ISSUE_TEMPLATE/Bug_report.md rename to .github/ISSUE_TEMPLATE/1_Bug_report.md index 42173b6d49faa..77210d5df7e20 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -1,5 +1,5 @@ --- -name: Bug Report +name: 🐛 Bug Report about: Report errors and problems --- diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md similarity index 95% rename from .github/ISSUE_TEMPLATE/Feature_request.md rename to .github/ISSUE_TEMPLATE/2_Feature_request.md index 3422cc1353edc..5e791f9f08b07 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature Request +name: 🚀 Feature Request about: RFC and ideas for new features and improvements --- diff --git a/.github/ISSUE_TEMPLATE/Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md similarity index 77% rename from .github/ISSUE_TEMPLATE/Support_question.md rename to .github/ISSUE_TEMPLATE/3_Support_question.md index 40a9277035404..4975ea242c8f1 100644 --- a/.github/ISSUE_TEMPLATE/Support_question.md +++ b/.github/ISSUE_TEMPLATE/3_Support_question.md @@ -1,6 +1,6 @@ --- -name: Support Question -about: Questions about using Symfony and its components +name: ⛔ Support Question +about: See https://symfony.com/support for questions about using Symfony and its components --- diff --git a/.github/ISSUE_TEMPLATE/Documentation_issue.md b/.github/ISSUE_TEMPLATE/4_Documentation_issue.md similarity index 75% rename from .github/ISSUE_TEMPLATE/Documentation_issue.md rename to .github/ISSUE_TEMPLATE/4_Documentation_issue.md index eed9470b6878f..8570d0c6e44a0 100644 --- a/.github/ISSUE_TEMPLATE/Documentation_issue.md +++ b/.github/ISSUE_TEMPLATE/4_Documentation_issue.md @@ -1,6 +1,6 @@ --- -name: Documentation Issue -about: Anything related to Symfony Documentation +name: ⛔ Documentation Issue +about: See https://github.com/symfony/symfony-docs/issues for documentation issues --- diff --git a/.github/ISSUE_TEMPLATE/Security_issue.md b/.github/ISSUE_TEMPLATE/5_Security_issue.md similarity index 69% rename from .github/ISSUE_TEMPLATE/Security_issue.md rename to .github/ISSUE_TEMPLATE/5_Security_issue.md index 29981939d043b..e3283c73969b8 100644 --- a/.github/ISSUE_TEMPLATE/Security_issue.md +++ b/.github/ISSUE_TEMPLATE/5_Security_issue.md @@ -1,9 +1,11 @@ --- -name: Security Issue -about: Report security-related errors +name: ⛔ Security Issue +about: See https://symfony.com/security to report security-related issues --- +⚠ PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW. + - **Symfony version(s) affected**: x.y.z **Description** diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md index 5e791f9f08b07..335321e413607 100644 --- a/.github/ISSUE_TEMPLATE/2_Feature_request.md +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.md @@ -4,11 +4,6 @@ about: RFC and ideas for new features and improvements --- - - **Description** diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md index 4975ea242c8f1..9480710c15655 100644 --- a/.github/ISSUE_TEMPLATE/3_Support_question.md +++ b/.github/ISSUE_TEMPLATE/3_Support_question.md @@ -4,11 +4,6 @@ about: See https://symfony.com/support for questions about using Symfony and its --- - - We use GitHub issues only to discuss about Symfony bugs and new features. For this kind of questions about using Symfony or third-party bundles, please use any of the support alternatives shown in https://symfony.com/support diff --git a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md b/.github/ISSUE_TEMPLATE/4_Documentation_issue.md index 8570d0c6e44a0..0855c3c5f1e12 100644 --- a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md +++ b/.github/ISSUE_TEMPLATE/4_Documentation_issue.md @@ -4,11 +4,6 @@ about: See https://github.com/symfony/symfony-docs/issues for documentation issu --- - - Symfony Documentation has its own dedicated repository. Please open your documentation-related issue at https://github.com/symfony/symfony-docs/issues diff --git a/.github/ISSUE_TEMPLATE/5_Security_issue.md b/.github/ISSUE_TEMPLATE/5_Security_issue.md index e3283c73969b8..9b3165eb1db47 100644 --- a/.github/ISSUE_TEMPLATE/5_Security_issue.md +++ b/.github/ISSUE_TEMPLATE/5_Security_issue.md @@ -6,11 +6,6 @@ about: See https://symfony.com/security to report security-related issues ⚠ PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW. - - If you have found a security issue in Symfony, please send the details to security [at] symfony.com and don't disclose it publicly until we can provide a fix for it. From 88ecd0dc9ac2a369a477dfb5558bc5229dc204f5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 13 May 2018 23:24:43 +0200 Subject: [PATCH 010/125] [DI] fine tune dumped factories --- .../DependencyInjection/Dumper/PhpDumper.php | 30 ++++++++++++++++--- .../Tests/Fixtures/php/services9_as_files.txt | 16 ++-------- .../Tests/Fixtures/php/services9_compiled.php | 6 ++-- .../php/services_errored_definition.php | 6 ++-- .../php/services_private_in_expression.php | 2 +- 5 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 8491f806b4b17..b12d885325723 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Variable; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; +use Symfony\Component\DependencyInjection\Compiler\ServiceReferenceGraphNode; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -68,6 +69,7 @@ class PhpDumper extends Dumper private $inlineRequires; private $inlinedRequires = array(); private $circularReferences = array(); + private $singleUsePrivateIds = array(); /** * @var ProxyDumper @@ -141,10 +143,14 @@ public function dump(array $options = array()) (new AnalyzeServiceReferencesPass())->process($this->container); $this->circularReferences = array(); + $this->singleUsePrivateIds = array(); $checkedNodes = array(); foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) { $currentPath = array($id => $id); $this->analyzeCircularReferences($node->getOutEdges(), $checkedNodes, $currentPath); + if ($this->isSingleUsePrivateNode($node)) { + $this->singleUsePrivateIds[$id] = $id; + } } $this->container->getCompiler()->getServiceReferenceGraph()->clear(); @@ -526,7 +532,7 @@ private function addServiceInstance(string $id, Definition $definition, string $ $isProxyCandidate = $this->getProxyDumper()->isProxyCandidate($definition); $instantiation = ''; - if (!$isProxyCandidate && $definition->isShared()) { + if (!$isProxyCandidate && $definition->isShared() && !isset($this->singleUsePrivateIds[$id])) { $instantiation = sprintf('$this->%s[\'%s\'] = %s', $this->container->getDefinition($id)->isPublic() ? 'services' : 'privates', $id, $isSimpleInstance ? '' : '$instance'); } elseif (!$isSimpleInstance) { $instantiation = '$instance'; @@ -819,7 +825,7 @@ private function generateServiceFiles() $definitions = $this->container->getDefinitions(); ksort($definitions); foreach ($definitions as $id => $definition) { - if (!$definition->isSynthetic() && !$this->isHotPath($definition)) { + if (!$definition->isSynthetic() && !$this->isHotPath($definition) && ($definition->isPublic() || !$this->isTrivialInstance($definition))) { $code = $this->addService($id, $definition, $file); if (!$definition->isShared()) { @@ -1662,7 +1668,7 @@ private function getServiceCall(string $id, Reference $reference = null): string $code = 'null'; } elseif ($this->isTrivialInstance($definition)) { $code = substr($this->addNewInstance($definition, '', '', $id), 8, -2); - if ($definition->isShared()) { + if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) { $code = sprintf('$this->%s[\'%s\'] = %s', $definition->isPublic() ? 'services' : 'privates', $id, $code); } } elseif ($this->asFiles && !$this->isHotPath($definition)) { @@ -1674,7 +1680,7 @@ private function getServiceCall(string $id, Reference $reference = null): string } else { $code = sprintf('$this->%s()', $this->generateMethodName($id)); } - if ($definition->isShared()) { + if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) { $code = sprintf('($this->%s[\'%s\'] ?? %s)', $definition->isPublic() ? 'services' : 'privates', $id, $code); } @@ -1798,6 +1804,22 @@ private function isHotPath(Definition $definition) return $this->hotPathTag && $definition->hasTag($this->hotPathTag) && !$definition->isDeprecated(); } + private function isSingleUsePrivateNode(ServiceReferenceGraphNode $node): bool + { + if ($node->getValue()->isPublic()) { + return false; + } + $ids = array(); + foreach ($node->getInEdges() as $edge) { + if ($edge->isLazy() || !$edge->getSourceNode()->getValue()->isShared()) { + return false; + } + $ids[$edge->getSourceNode()->getId()] = true; + } + + return 1 === \count($ids); + } + private function export($value) { if (null !== $this->targetDirRegex && is_string($value) && preg_match($this->targetDirRegex, $value, $matches, PREG_OFFSET_CAPTURE)) { 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 index 2b92c5838ba15..3a092f7306d70 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt @@ -155,7 +155,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; // This file has been auto-generated by the Symfony Dependency Injection Component for internal use. // Returns the public 'factory_service_simple' shared service. -return $this->services['factory_service_simple'] = ($this->privates['factory_simple'] ?? $this->load('getFactorySimpleService.php'))->getInstance(); +return $this->services['factory_service_simple'] = $this->load('getFactorySimpleService.php')->getInstance(); [Container%s/getFactorySimpleService.php] => privates['factory_simple'] = new \SimpleFactoryClass('foo'); +return new \SimpleFactoryClass('foo'); [Container%s/getFooService.php] => services['runtime_error'] = new \stdClass(($this->privates['errored_definition'] ?? $this->load('getErroredDefinitionService.php'))); +return $this->services['runtime_error'] = new \stdClass($this->load('getErroredDefinitionService.php')); [Container%s/getServiceFromStaticMethodService.php] => services['tagged_iterator'] = new \Bar(new RewindableGenerator(fun yield 1 => ($this->privates['tagged_iterator_foo'] ?? $this->privates['tagged_iterator_foo'] = new \Bar()); }, 2)); - [Container%s/getTaggedIteratorFooService.php] => privates['tagged_iterator_foo'] = new \Bar(); - [Container%s/ProjectServiceContainer.php] => services['factory_service_simple'] = ($this->privates['factory_simple'] ?? $this->getFactorySimpleService())->getInstance(); + return $this->services['factory_service_simple'] = $this->getFactorySimpleService()->getInstance(); } /** @@ -381,7 +381,7 @@ protected function getNewFactoryServiceService() */ protected function getRuntimeErrorService() { - return $this->services['runtime_error'] = new \stdClass(($this->privates['errored_definition'] ?? $this->getErroredDefinitionService())); + return $this->services['runtime_error'] = new \stdClass($this->getErroredDefinitionService()); } /** @@ -428,7 +428,7 @@ protected function getFactorySimpleService() { @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'); + return new \SimpleFactoryClass('foo'); } public function getParameter($name) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php index 34a38dfc40274..9ff22bf4d43d8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php @@ -243,7 +243,7 @@ protected function getFactoryServiceService() */ protected function getFactoryServiceSimpleService() { - return $this->services['factory_service_simple'] = ($this->privates['factory_simple'] ?? $this->getFactorySimpleService())->getInstance(); + return $this->services['factory_service_simple'] = $this->getFactorySimpleService()->getInstance(); } /** @@ -381,7 +381,7 @@ protected function getNewFactoryServiceService() */ protected function getRuntimeErrorService() { - return $this->services['runtime_error'] = new \stdClass(($this->privates['errored_definition'] ?? $this->getErroredDefinitionService())); + return $this->services['runtime_error'] = new \stdClass($this->getErroredDefinitionService()); } /** @@ -428,7 +428,7 @@ protected function getFactorySimpleService() { @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'); + return new \SimpleFactoryClass('foo'); } public function getParameter($name) 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 index 5caf9104dd34d..2ae36458036d9 100644 --- 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 @@ -67,6 +67,6 @@ public function getRemovedIds() */ protected function getPublicFooService() { - return $this->services['public_foo'] = new \stdClass(($this->privates['private_foo'] ?? $this->privates['private_foo'] = new \stdClass())); + return $this->services['public_foo'] = new \stdClass(new \stdClass()); } } From 1c64c8267ddd9332d6434f87badfecc96146fd7d Mon Sep 17 00:00:00 2001 From: Jonathan Hedstrom Date: Tue, 1 May 2018 16:16:06 -0700 Subject: [PATCH 011/125] [BrowserKit] Adds support for meta refresh --- src/Symfony/Component/BrowserKit/Client.php | 35 ++++++++++++++++++- .../Component/BrowserKit/Tests/ClientTest.php | 33 +++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/BrowserKit/Client.php b/src/Symfony/Component/BrowserKit/Client.php index 799b3579f0f69..ddd99a1ce2622 100644 --- a/src/Symfony/Component/BrowserKit/Client.php +++ b/src/Symfony/Component/BrowserKit/Client.php @@ -40,6 +40,7 @@ abstract class Client protected $insulated = false; protected $redirect; protected $followRedirects = true; + protected $followMetaRefresh = false; private $maxRedirects = -1; private $redirectCount = 0; @@ -68,6 +69,14 @@ public function followRedirects($followRedirect = true) $this->followRedirects = (bool) $followRedirect; } + /** + * Sets whether to automatically follow meta refresh redirects or not. + */ + public function followMetaRefresh(bool $followMetaRefresh = true) + { + $this->followMetaRefresh = $followMetaRefresh; + } + /** * Returns whether client automatically follows redirects or not. * @@ -367,7 +376,16 @@ public function request(string $method, string $uri, array $parameters = array() return $this->crawler = $this->followRedirect(); } - return $this->crawler = $this->createCrawlerFromContent($this->internalRequest->getUri(), $this->internalResponse->getContent(), $this->internalResponse->getHeader('Content-Type')); + $this->crawler = $this->createCrawlerFromContent($this->internalRequest->getUri(), $this->internalResponse->getContent(), $this->internalResponse->getHeader('Content-Type')); + + // Check for meta refresh redirect + if ($this->followMetaRefresh && null !== $redirect = $this->getMetaRefreshUrl()) { + $this->redirect = $redirect; + $this->redirects[serialize($this->history->current())] = true; + $this->crawler = $this->followRedirect(); + } + + return $this->crawler; } /** @@ -563,6 +581,21 @@ public function followRedirect() return $response; } + /** + * @see https://dev.w3.org/html5/spec-preview/the-meta-element.html#attr-meta-http-equiv-refresh + */ + private function getMetaRefreshUrl(): ?string + { + $metaRefresh = $this->getCrawler()->filter('head meta[http-equiv="refresh"]'); + foreach ($metaRefresh->extract(array('content')) as $content) { + if (preg_match('/^\s*0\s*;\s*URL\s*=\s*(?|\'([^\']++)|"([^"]++)|([^\'"].*))/i', $content, $m)) { + return str_replace("\t\r\n", '', rtrim($m[1])); + } + } + + return null; + } + /** * Restarts the client. * diff --git a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php index 909bf3c67c245..020fa07526e03 100644 --- a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php @@ -594,6 +594,39 @@ public function testFollowRedirectDropPostMethod() } } + /** + * @dataProvider getTestsForMetaRefresh + */ + public function testFollowMetaRefresh(string $content, string $expectedEndingUrl, bool $followMetaRefresh = true) + { + $client = new TestClient(); + $client->followMetaRefresh($followMetaRefresh); + $client->setNextResponse(new Response($content)); + $client->request('GET', 'http://www.example.com/foo/foobar'); + $this->assertEquals($expectedEndingUrl, $client->getRequest()->getUri()); + } + + public function getTestsForMetaRefresh() + { + return array( + array('', 'http://www.example.com/redirected'), + array('', 'http://www.example.com/redirected'), + array('', 'http://www.example.com/redirected'), + array('', 'http://www.example.com/redirected'), + array('', 'http://www.example.com/redirected'), + array('', 'http://www.example.com/redirected'), + array('', 'http://www.example.com/redirected'), + array('', 'http://www.example.com/redirected'), + // Non-zero timeout should not result in a redirect. + array('', 'http://www.example.com/foo/foobar'), + array('', 'http://www.example.com/foo/foobar'), + // Invalid meta tag placement should not result in a redirect. + array('', 'http://www.example.com/foo/foobar'), + // Valid meta refresh should not be followed if disabled. + array('', 'http://www.example.com/foo/foobar', false), + ); + } + public function testBack() { $client = new TestClient(); From 07cfa93fc11f155b5961c97bddd7b1cbe71bb97f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 18 May 2018 08:08:04 +0200 Subject: [PATCH 012/125] removed unneeded private methods --- .../Ldap/Adapter/ExtLdap/UpdateOperation.php | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/UpdateOperation.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/UpdateOperation.php index 87bd451eb7229..bb5d146b9e05d 100644 --- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/UpdateOperation.php +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/UpdateOperation.php @@ -34,8 +34,12 @@ class UpdateOperation */ public function __construct(int $operationType, string $attribute, ?array $values) { - $this->assertValidOperationType($operationType); - $this->assertNullValuesOnRemoveAll($operationType, $values); + if (!in_array($operationType, $this->validOperationTypes, true)) { + throw new UpdateOperationException(sprintf('"%s" is not a valid modification type.', $operationType)); + } + if (LDAP_MODIFY_BATCH_REMOVE_ALL === $operationType && null !== $values) { + throw new UpdateOperationException(sprintf('$values must be null for LDAP_MODIFY_BATCH_REMOVE_ALL operation, "%s" given.', gettype($values))); + } $this->operationType = $operationType; $this->attribute = $attribute; @@ -50,27 +54,4 @@ public function toArray(): array 'values' => $this->values, ); } - - /** - * @param int $operationType - */ - private function assertValidOperationType(int $operationType): void - { - if (!in_array($operationType, $this->validOperationTypes, true)) { - throw new UpdateOperationException(sprintf('"%s" is not a valid modification type.', $operationType)); - } - } - - /** - * @param int $operationType - * @param array|null $values - * - * @throws \Symfony\Component\Ldap\Exception\UpdateOperationException - */ - private function assertNullValuesOnRemoveAll(int $operationType, ?array $values): void - { - if (LDAP_MODIFY_BATCH_REMOVE_ALL === $operationType && null !== $values) { - throw new UpdateOperationException(sprintf('$values must be null for LDAP_MODIFY_BATCH_REMOVE_ALL operation, "%s" given.', gettype($values))); - } - } } From 2ae7ad9fe195f7a9ec2b3c37c19bd6da50dd98a1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 18 May 2018 08:10:24 +0200 Subject: [PATCH 013/125] updated CHANGELOG --- src/Symfony/Component/Ldap/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Symfony/Component/Ldap/CHANGELOG.md b/src/Symfony/Component/Ldap/CHANGELOG.md index 014c487eed2cf..b3f367d9ef445 100644 --- a/src/Symfony/Component/Ldap/CHANGELOG.md +++ b/src/Symfony/Component/Ldap/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + + * added `EntryManager::applyOperations` + 4.1.0 ----- From 589ff697f4a09b6aa391389cc64d5504b4ba1eb1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 14 Apr 2018 19:21:41 -0500 Subject: [PATCH 014/125] [Cache] Add [Taggable]CacheInterface, the easiest way to use a cache --- .../Resources/config/cache.xml | 6 +++ .../Cache/Adapter/AbstractAdapter.php | 5 ++- .../Component/Cache/Adapter/ArrayAdapter.php | 5 ++- .../Component/Cache/Adapter/ChainAdapter.php | 35 ++++++++++++++- .../Component/Cache/Adapter/NullAdapter.php | 6 ++- .../Cache/Adapter/PhpArrayAdapter.php | 30 ++++++++++++- .../Component/Cache/Adapter/ProxyAdapter.php | 19 +++++++- .../Cache/Adapter/TagAwareAdapter.php | 6 ++- .../Cache/Adapter/TraceableAdapter.php | 36 +++++++++++++++- .../Adapter/TraceableTagAwareAdapter.php | 4 +- src/Symfony/Component/Cache/CHANGELOG.md | 6 +++ .../Component/Cache/CacheInterface.php | 37 ++++++++++++++++ src/Symfony/Component/Cache/CacheItem.php | 7 ++- .../DataCollector/CacheDataCollector.php | 10 ++++- .../Cache/Exception/LogicException.php | 19 ++++++++ .../Cache/TaggableCacheInterface.php | 35 +++++++++++++++ .../Cache/Tests/Adapter/AdapterTestCase.php | 21 +++++++++ .../Tests/Adapter/PhpArrayAdapterTest.php | 1 + .../Component/Cache/Tests/CacheItemTest.php | 21 +++++++++ .../Component/Cache/Traits/GetTrait.php | 43 +++++++++++++++++++ 20 files changed, 341 insertions(+), 11 deletions(-) create mode 100644 src/Symfony/Component/Cache/CacheInterface.php create mode 100644 src/Symfony/Component/Cache/Exception/LogicException.php create mode 100644 src/Symfony/Component/Cache/TaggableCacheInterface.php create mode 100644 src/Symfony/Component/Cache/Traits/GetTrait.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index f7162adb1c701..d0e596ab83338 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -15,6 +15,10 @@ + + + + @@ -122,7 +126,9 @@ + + diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 3afc982089018..1e246b8790cb4 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -15,17 +15,20 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\AbstractTrait; +use Symfony\Component\Cache\Traits\GetTrait; /** * @author Nicolas Grekas */ -abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface, ResettableInterface +abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface { use AbstractTrait; + use GetTrait; private static $apcuSupported; private static $phpFilesSupported; diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index fee7ed6d906d5..17f2beaf0fe75 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -13,16 +13,19 @@ use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerAwareInterface; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\ArrayTrait; +use Symfony\Component\Cache\Traits\GetTrait; /** * @author Nicolas Grekas */ -class ArrayAdapter implements AdapterInterface, LoggerAwareInterface, ResettableInterface +class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface { use ArrayTrait; + use GetTrait; private $createCacheItem; diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php index 98b0cc24693b4..ea0af87d9f238 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -13,10 +13,12 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\GetTrait; /** * Chains several adapters together. @@ -26,8 +28,10 @@ * * @author Kévin Dunglas */ -class ChainAdapter implements AdapterInterface, PruneableInterface, ResettableInterface +class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface { + use GetTrait; + private $adapters = array(); private $adapterCount; private $syncItem; @@ -61,6 +65,8 @@ function ($sourceItem, $item) use ($defaultLifetime) { $item->expiry = $sourceItem->expiry; $item->isHit = $sourceItem->isHit; + $sourceItem->isTaggable = false; + if (0 < $sourceItem->defaultLifetime && $sourceItem->defaultLifetime < $defaultLifetime) { $defaultLifetime = $sourceItem->defaultLifetime; } @@ -75,6 +81,33 @@ function ($sourceItem, $item) use ($defaultLifetime) { ); } + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback) + { + $lastItem = null; + $i = 0; + $wrap = function (CacheItem $item = null) use ($key, $callback, &$wrap, &$i, &$lastItem) { + $adapter = $this->adapters[$i]; + if (isset($this->adapters[++$i])) { + $callback = $wrap; + } + if ($adapter instanceof CacheInterface) { + $value = $adapter->get($key, $callback); + } else { + $value = $this->doGet($adapter, $key, $callback); + } + if (null !== $item) { + ($this->syncItem)($lastItem = $lastItem ?? $item, $item); + } + + return $value; + }; + + return $wrap(); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Adapter/NullAdapter.php b/src/Symfony/Component/Cache/Adapter/NullAdapter.php index f58f81e5b8960..44929f9e76aeb 100644 --- a/src/Symfony/Component/Cache/Adapter/NullAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/NullAdapter.php @@ -12,13 +12,17 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Traits\GetTrait; /** * @author Titouan Galopin */ -class NullAdapter implements AdapterInterface +class NullAdapter implements AdapterInterface, CacheInterface { + use GetTrait; + private $createCacheItem; public function __construct() diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index ca5ef743d2285..bcd322fede1ba 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -13,10 +13,12 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\GetTrait; use Symfony\Component\Cache\Traits\PhpArrayTrait; /** @@ -26,9 +28,10 @@ * @author Titouan Galopin * @author Nicolas Grekas */ -class PhpArrayAdapter implements AdapterInterface, PruneableInterface, ResettableInterface +class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface { use PhpArrayTrait; + use GetTrait; private $createCacheItem; @@ -77,6 +80,31 @@ public static function create($file, CacheItemPoolInterface $fallbackPool) return $fallbackPool; } + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback) + { + if (null === $this->values) { + $this->initialize(); + } + if (null === $value = $this->values[$key] ?? null) { + if ($this->pool instanceof CacheInterface) { + return $this->pool->get($key, $callback); + } + + return $this->doGet($this->pool, $key, $callback); + } + if ('N;' === $value) { + return null; + } + if (\is_string($value) && isset($value[2]) && ':' === $value[1]) { + return unserialize($value); + } + + return $value; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index da286dbf173f7..b9981f5e64c0c 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -13,17 +13,20 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\GetTrait; use Symfony\Component\Cache\Traits\ProxyTrait; /** * @author Nicolas Grekas */ -class ProxyAdapter implements AdapterInterface, PruneableInterface, ResettableInterface +class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface { use ProxyTrait; + use GetTrait; private $namespace; private $namespaceLen; @@ -54,6 +57,20 @@ function ($key, $innerItem) use ($defaultLifetime, $poolHash) { ); } + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback) + { + if (!$this->pool instanceof CacheInterface) { + return $this->doGet($this->pool, $key, $callback); + } + + return $this->pool->get($this->getId($key), function ($innerItem) use ($key, $callback) { + return $callback(($this->createCacheItem)($key, $innerItem)); + }); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index 62f815e0171ad..257e404e1c43e 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -16,16 +16,19 @@ use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\TaggableCacheInterface; +use Symfony\Component\Cache\Traits\GetTrait; use Symfony\Component\Cache\Traits\ProxyTrait; /** * @author Nicolas Grekas */ -class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface, ResettableInterface +class TagAwareAdapter implements TagAwareAdapterInterface, TaggableCacheInterface, PruneableInterface, ResettableInterface { const TAGS_PREFIX = "\0tags\0"; use ProxyTrait; + use GetTrait; private $deferred = array(); private $createCacheItem; @@ -58,6 +61,7 @@ function ($key, $value, CacheItem $protoItem) { ); $this->setCacheItemTags = \Closure::bind( function (CacheItem $item, $key, array &$itemTags) { + $item->isTaggable = true; if (!$item->isHit) { return $item; } diff --git a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php index 98d0e526933b9..a0df682d92b69 100644 --- a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Cache\Adapter; use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheInterface; +use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; @@ -22,7 +24,7 @@ * @author Tobias Nyholm * @author Nicolas Grekas */ -class TraceableAdapter implements AdapterInterface, PruneableInterface, ResettableInterface +class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface { protected $pool; private $calls = array(); @@ -32,6 +34,38 @@ public function __construct(AdapterInterface $pool) $this->pool = $pool; } + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback) + { + if (!$this->pool instanceof CacheInterface) { + throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_class($this->pool), CacheInterface::class)); + } + + $isHit = true; + $callback = function (CacheItem $item) use ($callback, &$isHit) { + $isHit = $item->isHit(); + + return $callback($item); + }; + + $event = $this->start(__FUNCTION__); + try { + $value = $this->pool->get($key, $callback); + $event->result[$key] = \is_object($value) ? \get_class($value) : gettype($value); + } finally { + $event->end = microtime(true); + } + if ($isHit) { + ++$event->hits; + } else { + ++$event->misses; + } + + return $value; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php index de68955d8e56d..2fda8b360240e 100644 --- a/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php @@ -11,10 +11,12 @@ namespace Symfony\Component\Cache\Adapter; +use Symfony\Component\Cache\TaggableCacheInterface; + /** * @author Robin Chalas */ -class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface +class TraceableTagAwareAdapter extends TraceableAdapter implements TaggableCacheInterface, TagAwareAdapterInterface { public function __construct(TagAwareAdapterInterface $pool) { diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 11c1b9364ebd5..d21b2cbda4df1 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +4.2.0 +----- + + * added `CacheInterface` and `TaggableCacheInterface` + * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool + 3.4.0 ----- diff --git a/src/Symfony/Component/Cache/CacheInterface.php b/src/Symfony/Component/Cache/CacheInterface.php new file mode 100644 index 0000000000000..c5c877ddbd746 --- /dev/null +++ b/src/Symfony/Component/Cache/CacheInterface.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; + +use Psr\Cache\CacheItemInterface; + +/** + * Gets and stores items from a cache. + * + * On cache misses, a callback is called that should return the missing value. + * It is given two arguments: + * - the missing cache key + * - the corresponding PSR-6 CacheItemInterface object, + * allowing time-based expiration control. + * + * If you need tag-based invalidation, use TaggableCacheInterface instead. + * + * @author Nicolas Grekas + */ +interface CacheInterface +{ + /** + * @param callable(CacheItemInterface):mixed $callback Should return the computed value for the given key/item + * + * @return mixed The value corresponding to the provided key + */ + public function get(string $key, callable $callback); +} diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index cecaa126d9129..82ad9df68262c 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -14,6 +14,7 @@ use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Exception\LogicException; /** * @author Nicolas Grekas @@ -29,6 +30,7 @@ final class CacheItem implements CacheItemInterface protected $prevTags = array(); protected $innerItem; protected $poolHash; + protected $isTaggable = false; /** * {@inheritdoc} @@ -109,7 +111,10 @@ public function expiresAfter($time) */ public function tag($tags) { - if (!\is_array($tags)) { + if (!$this->isTaggable) { + throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key)); + } + if (!\is_iterable($tags)) { $tags = array($tags); } foreach ($tags as $tag) { diff --git a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php index 91763e5a9f33b..5f29bfe5f2769 100644 --- a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php +++ b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php @@ -121,7 +121,15 @@ private function calculateStatistics(): array foreach ($calls as $call) { ++$statistics[$name]['calls']; $statistics[$name]['time'] += $call->end - $call->start; - if ('getItem' === $call->name) { + if ('get' === $call->name) { + ++$statistics[$name]['reads']; + if ($call->hits) { + ++$statistics[$name]['hits']; + } else { + ++$statistics[$name]['misses']; + ++$statistics[$name]['writes']; + } + } elseif ('getItem' === $call->name) { ++$statistics[$name]['reads']; if ($call->hits) { ++$statistics[$name]['hits']; diff --git a/src/Symfony/Component/Cache/Exception/LogicException.php b/src/Symfony/Component/Cache/Exception/LogicException.php new file mode 100644 index 0000000000000..042f73e6a5040 --- /dev/null +++ b/src/Symfony/Component/Cache/Exception/LogicException.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 LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface +{ +} diff --git a/src/Symfony/Component/Cache/TaggableCacheInterface.php b/src/Symfony/Component/Cache/TaggableCacheInterface.php new file mode 100644 index 0000000000000..c112e72586d74 --- /dev/null +++ b/src/Symfony/Component/Cache/TaggableCacheInterface.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; + +/** + * Gets and stores items from a tag-aware cache. + * + * On cache misses, a callback is called that should return the missing value. + * It is given two arguments: + * - the missing cache key + * - the corresponding Symfony CacheItem object, + * allowing time-based *and* tags-based expiration control + * + * If you don't need tags-based invalidation, use CacheInterface instead. + * + * @author Nicolas Grekas + */ +interface TaggableCacheInterface extends CacheInterface +{ + /** + * @param callable(CacheItem):mixed $callback Should return the computed value for the given key/item + * + * @return mixed The value corresponding to the provided key + */ + public function get(string $key, callable $callback); +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index 018d149467482..3c96b731cc565 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -13,6 +13,7 @@ use Cache\IntegrationTests\CachePoolTest; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\PruneableInterface; abstract class AdapterTestCase extends CachePoolTest @@ -26,6 +27,26 @@ protected function setUp() } } + public function testGet() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = $this->createCachePool(); + + $value = mt_rand(); + + $this->assertSame($value, $cache->get('foo', function (CacheItem $item) use ($value) { + $this->assertSame('foo', $item->getKey()); + + return $value; + })); + + $item = $cache->getItem('foo'); + $this->assertSame($value, $item->get()); + } + public function testDefaultLifeTime() { if (isset($this->skippedTests[__FUNCTION__])) { diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php index 14b61263c5892..8630b52cf30c9 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -21,6 +21,7 @@ class PhpArrayAdapterTest extends AdapterTestCase { protected $skippedTests = array( + 'testGet' => 'PhpArrayAdapter is read-only.', 'testBasicUsage' => 'PhpArrayAdapter is read-only.', 'testBasicUsageWithLongKey' => 'PhpArrayAdapter is read-only.', 'testClear' => 'PhpArrayAdapter is read-only.', diff --git a/src/Symfony/Component/Cache/Tests/CacheItemTest.php b/src/Symfony/Component/Cache/Tests/CacheItemTest.php index daca925fd5b78..3a0ea098ad7c1 100644 --- a/src/Symfony/Component/Cache/Tests/CacheItemTest.php +++ b/src/Symfony/Component/Cache/Tests/CacheItemTest.php @@ -55,6 +55,9 @@ public function provideInvalidKey() public function testTag() { $item = new CacheItem(); + $r = new \ReflectionProperty($item, 'isTaggable'); + $r->setAccessible(true); + $r->setValue($item, true); $this->assertSame($item, $item->tag('foo')); $this->assertSame($item, $item->tag(array('bar', 'baz'))); @@ -72,6 +75,24 @@ public function testTag() public function testInvalidTag($tag) { $item = new CacheItem(); + $r = new \ReflectionProperty($item, 'isTaggable'); + $r->setAccessible(true); + $r->setValue($item, true); + $item->tag($tag); } + + /** + * @expectedException \Symfony\Component\Cache\Exception\LogicException + * @expectedExceptionMessage Cache item "foo" comes from a non tag-aware pool: you cannot tag it. + */ + public function testNonTaggableItem() + { + $item = new CacheItem(); + $r = new \ReflectionProperty($item, 'key'); + $r->setAccessible(true); + $r->setValue($item, 'foo'); + + $item->tag(array()); + } } diff --git a/src/Symfony/Component/Cache/Traits/GetTrait.php b/src/Symfony/Component/Cache/Traits/GetTrait.php new file mode 100644 index 0000000000000..d2a5f92da2e53 --- /dev/null +++ b/src/Symfony/Component/Cache/Traits/GetTrait.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\Cache\Traits; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait GetTrait +{ + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback) + { + return $this->doGet($this, $key, $callback); + } + + private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback) + { + $item = $pool->getItem($key); + + if ($item->isHit()) { + return $item->get(); + } + + $pool->save($item->set($value = $callback($item))); + + return $value; + } +} From 9dbf39924781092ad6e6809ff4b4d70323a02cdc Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 18 May 2018 12:12:47 +0200 Subject: [PATCH 015/125] [Security/Core] Add "is_granted()" to security expressions, deprecate "has_role()" --- UPGRADE-4.2.md | 7 +++ .../DependencyInjection/Configuration.php | 2 +- .../Resources/config/security.xml | 1 + .../app/StandardFormLogin/config.yml | 2 +- .../Bundle/SecurityBundle/composer.json | 2 +- src/Symfony/Component/Security/CHANGELOG.md | 6 +++ .../ExpressionLanguageProvider.php | 10 ++++ .../Authorization/Voter/ExpressionVoter.php | 19 ++++++- .../Authorization/ExpressionLanguageTest.php | 50 ++++++++++++++++--- .../Voter/ExpressionVoterTest.php | 7 +-- 10 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 UPGRADE-4.2.md diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md new file mode 100644 index 0000000000000..23982ef159b36 --- /dev/null +++ b/UPGRADE-4.2.md @@ -0,0 +1,7 @@ +UPGRADE FROM 4.1 to 4.2 +======================= + +Security +-------- + + * Using the `has_role()` function in security expressions is deprecated, use the `is_granted()` function instead. diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index ce65f52bef128..9f0e07fd149d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -370,7 +370,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->scalarNode('guard') ->cannotBeEmpty() ->info('An expression to block the transition') - ->example('is_fully_authenticated() and has_role(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'') + ->example('is_fully_authenticated() and is_granted(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'') ->end() ->arrayNode('from') ->beforeNormalization() diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 9cbdd2061e119..65c48e0855cb7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -119,6 +119,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml index 19b9d8952ec5e..d7c73aa0b6dc0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml @@ -40,5 +40,5 @@ security: - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/highly_protected_resource$, roles: IS_ADMIN } - - { path: ^/protected-via-expression$, allow_if: "(is_anonymous() and request.headers.get('user-agent') matches '/Firefox/i') or has_role('ROLE_USER')" } + - { path: ^/protected-via-expression$, allow_if: "(is_anonymous() and request.headers.get('user-agent') matches '/Firefox/i') or is_granted('ROLE_USER')" } - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 5dd550ec3b84d..e95f3430a94b7 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1.3", "ext-xml": "*", - "symfony/security": "~4.1", + "symfony/security": "~4.2", "symfony/dependency-injection": "^3.4.3|^4.0.3", "symfony/http-kernel": "^4.1" }, diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index ec33a7af97a4b..87939e3a26388 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +4.2.0 +----- + +* added the `is_granted()` function in security expressions +* deprecated the `has_role()` function in security expressions, use `is_granted()` instead + 4.1.0 ----- diff --git a/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguageProvider.php b/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguageProvider.php index 9293ba7e39d4e..9e551115837cb 100644 --- a/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguageProvider.php +++ b/src/Symfony/Component/Security/Core/Authorization/ExpressionLanguageProvider.php @@ -42,6 +42,12 @@ public function getFunctions() return $variables['trust_resolver']->isFullFledged($variables['token']); }), + new ExpressionFunction('is_granted', function ($attributes, $object = 'null') { + return sprintf('$auth_checker->isGranted(%s, %s)', $attributes, $object); + }, function (array $variables, $attributes, $object = null) { + return $variables['auth_checker']->isGranted($attributes, $object); + }), + new ExpressionFunction('is_remember_me', function () { return '$trust_resolver->isRememberMe($token)'; }, function (array $variables) { @@ -49,8 +55,12 @@ public function getFunctions() }), new ExpressionFunction('has_role', function ($role) { + @trigger_error('Using the "has_role()" function in security expressions is deprecated since Symfony 4.2, use "is_granted()" instead.', E_USER_DEPRECATED); + return sprintf('in_array(%s, $roles)', $role); }, function (array $variables, $role) { + @trigger_error('Using the "has_role()" function in security expressions is deprecated since Symfony 4.2, use "is_granted()" instead.', E_USER_DEPRECATED); + return in_array($role, $variables['roles']); }), ); diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php index cbee938667789..726313c69d910 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php @@ -13,6 +13,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; @@ -28,12 +29,27 @@ class ExpressionVoter implements VoterInterface { private $expressionLanguage; private $trustResolver; + private $authChecker; private $roleHierarchy; - public function __construct(ExpressionLanguage $expressionLanguage, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null) + /** + * @param AuthorizationCheckerInterface $authChecker + */ + public function __construct(ExpressionLanguage $expressionLanguage, AuthenticationTrustResolverInterface $trustResolver, $authChecker = null, RoleHierarchyInterface $roleHierarchy = null) { + if ($authChecker instanceof RoleHierarchyInterface) { + @trigger_error(sprintf('Passing a RoleHierarchyInterface to "%s()" is deprecated since Symfony 4.2. Pass an AuthorizationCheckerInterface instead.', __METHOD__), E_USER_DEPRECATED); + $roleHierarchy = $authChecker; + $authChecker = null; + } elseif (null === $authChecker) { + @trigger_error(sprintf('Argument 3 passed to "%s()" should be an instanceof AuthorizationCheckerInterface, not passing it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + } elseif (!$authChecker instanceof AuthorizationCheckerInterface) { + throw new \InvalidArgumentException(sprintf('Argument 3 passed to %s() must be an instance of %s or null, %s given.', __METHOD__, AuthorizationCheckerInterface::class, is_object($authChecker) ? get_class($authChecker) : gettype($authChecker))); + } + $this->expressionLanguage = $expressionLanguage; $this->trustResolver = $trustResolver; + $this->authChecker = $authChecker; $this->roleHierarchy = $roleHierarchy; } @@ -87,6 +103,7 @@ private function getVariables(TokenInterface $token, $subject) 'subject' => $subject, 'roles' => array_map(function ($role) { return $role->getRole(); }, $roles), 'trust_resolver' => $this->trustResolver, + 'auth_checker' => $this->authChecker, ); // this is mainly to propose a better experience when the expression is used diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php index 1565d1c865256..6c05ecfab506a 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php @@ -12,10 +12,15 @@ namespace Symfony\Component\Security\Core\Tests\Authorization; use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\User; @@ -24,17 +29,21 @@ class ExpressionLanguageTest extends TestCase /** * @dataProvider provider */ - public function testIsAuthenticated($token, $expression, $result, array $roles = array()) + public function testIsAuthenticated($token, $expression, $result) { $anonymousTokenClass = 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken'; $rememberMeTokenClass = 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\RememberMeToken'; $expressionLanguage = new ExpressionLanguage(); $trustResolver = new AuthenticationTrustResolver($anonymousTokenClass, $rememberMeTokenClass); + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken($token); + $accessDecisionManager = new AccessDecisionManager(array(new RoleVoter())); + $authChecker = new AuthorizationChecker($tokenStorage, $this->getMockBuilder(AuthenticationManagerInterface::class)->getMock(), $accessDecisionManager); $context = array(); $context['trust_resolver'] = $trustResolver; + $context['auth_checker'] = $authChecker; $context['token'] = $token; - $context['roles'] = $roles; $this->assertEquals($result, $expressionLanguage->evaluate($expression, $context)); } @@ -54,27 +63,52 @@ public function provider() array($noToken, 'is_authenticated()', false), array($noToken, 'is_fully_authenticated()', false), array($noToken, 'is_remember_me()', false), - array($noToken, "has_role('ROLE_USER')", false), array($anonymousToken, 'is_anonymous()', true), array($anonymousToken, 'is_authenticated()', false), array($anonymousToken, 'is_fully_authenticated()', false), array($anonymousToken, 'is_remember_me()', false), - array($anonymousToken, "has_role('ROLE_USER')", false), + array($anonymousToken, "is_granted('ROLE_USER')", false), array($rememberMeToken, 'is_anonymous()', false), array($rememberMeToken, 'is_authenticated()', true), array($rememberMeToken, 'is_fully_authenticated()', false), array($rememberMeToken, 'is_remember_me()', true), - array($rememberMeToken, "has_role('ROLE_FOO')", false, $roles), - array($rememberMeToken, "has_role('ROLE_USER')", true, $roles), + array($rememberMeToken, "is_granted('ROLE_FOO')", false), + array($rememberMeToken, "is_granted('ROLE_USER')", true), array($usernamePasswordToken, 'is_anonymous()', false), array($usernamePasswordToken, 'is_authenticated()', true), array($usernamePasswordToken, 'is_fully_authenticated()', true), array($usernamePasswordToken, 'is_remember_me()', false), - array($usernamePasswordToken, "has_role('ROLE_FOO')", false, $roles), - array($usernamePasswordToken, "has_role('ROLE_USER')", true, $roles), + array($usernamePasswordToken, "is_granted('ROLE_FOO')", false), + array($usernamePasswordToken, "is_granted('ROLE_USER')", true), + ); + } + + /** + * @dataProvider provideLegacyHasRole + * @group legacy + */ + public function testLegacyHasRole($expression, $result, $roles = array()) + { + $expressionLanguage = new ExpressionLanguage(); + $context = array('roles' => $roles); + + $this->assertEquals($result, $expressionLanguage->evaluate($expression, $context)); + } + + public function provideLegacyHasRole() + { + $roles = array('ROLE_USER', 'ROLE_ADMIN'); + + return array( + array("has_role('ROLE_FOO')", false), + array("has_role('ROLE_USER')", false), + array("has_role('ROLE_ADMIN')", false), + array("has_role('ROLE_FOO')", false, $roles), + array("has_role('ROLE_USER')", true, $roles), + array("has_role('ROLE_ADMIN')", true, $roles), ); } } diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php index 79626835264ab..9b7bf67709ce0 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Tests\Authorization\Voter; use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Role\Role; @@ -23,7 +24,7 @@ class ExpressionVoterTest extends TestCase */ public function testVote($roles, $attributes, $expected, $tokenExpectsGetRoles = true, $expressionLanguageExpectsEvaluate = true) { - $voter = new ExpressionVoter($this->createExpressionLanguage($expressionLanguageExpectsEvaluate), $this->createTrustResolver()); + $voter = new ExpressionVoter($this->createExpressionLanguage($expressionLanguageExpectsEvaluate), $this->createTrustResolver(), $this->createAuthorizationChecker()); $this->assertSame($expected, $voter->vote($this->getToken($roles, $tokenExpectsGetRoles), null, $attributes)); } @@ -75,9 +76,9 @@ protected function createTrustResolver() return $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface')->getMock(); } - protected function createRoleHierarchy() + protected function createAuthorizationChecker() { - return $this->getMockBuilder('Symfony\Component\Security\Core\Role\RoleHierarchyInterface')->getMock(); + return $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock(); } protected function createExpression() From 5551e0c09105a303d7e2822e3f206c2214589ab9 Mon Sep 17 00:00:00 2001 From: kiler129 Date: Sat, 19 May 2018 16:51:12 -0500 Subject: [PATCH 016/125] feature #26824 add exception chain breadcrumbs navigation --- .../TwigBundle/Resources/views/Exception/exception.html.twig | 4 ++-- .../TwigBundle/Resources/views/Exception/traces.html.twig | 2 +- .../Bundle/TwigBundle/Resources/views/exception.css.twig | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig index 2f29571ae3278..00b1988ec3e67 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig @@ -3,10 +3,10 @@

{% for previousException in exception.allPrevious|reverse %} - {{ previousException.class|abbr_class }} + {{ previousException.class|abbr_class }} {{ include('@Twig/images/chevron-right.svg') }} {% endfor %} - {{ exception.class|abbr_class }} + {{ exception.class|abbr_class }}

HTTP {{ status_code }} {{ status_text }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig index 29952e2cb75b5..01f3796079c0d 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig @@ -1,4 +1,4 @@ -
+
diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig index 8cd6e6d07c0c6..e252281975e31 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig @@ -73,7 +73,7 @@ header .container { display: flex; justify-content: space-between; } .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: 15px; } .exception-metadata { background: rgba(0, 0, 0, 0.1); padding: 7px 0; } .exception-metadata .container { display: flex; flex-direction: row; justify-content: space-between; } -.exception-metadata h2 { color: rgba(255, 255, 255, 0.8); font-size: 13px; font-weight: 400; margin: 0; } +.exception-metadata h2, .exception-metadata h2 > a { color: rgba(255, 255, 255, 0.8); font-size: 13px; font-weight: 400; margin: 0; } .exception-http small { font-size: 13px; opacity: .7; } .exception-hierarchy { flex: 1; } .exception-hierarchy .icon { margin: 0 3px; opacity: .7; } From 6e43838c5ddca05755f77869c46b86c95d65c27e Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Wed, 16 May 2018 10:25:36 +0200 Subject: [PATCH 017/125] [Messenger] Activation middleware decorator --- src/Symfony/Component/Messenger/Envelope.php | 11 +++ .../ActivationMiddlewareDecorator.php | 48 ++++++++++ .../ActivationMiddlewareDecoratorTest.php | 92 +++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/Symfony/Component/Messenger/Middleware/Enhancers/ActivationMiddlewareDecorator.php create mode 100644 src/Symfony/Component/Messenger/Tests/Middleware/Enhancers/ActivationMiddlewareDecoratorTest.php diff --git a/src/Symfony/Component/Messenger/Envelope.php b/src/Symfony/Component/Messenger/Envelope.php index 4d5f7a02a9d5d..6ac9bc7f3c6d1 100644 --- a/src/Symfony/Component/Messenger/Envelope.php +++ b/src/Symfony/Component/Messenger/Envelope.php @@ -86,4 +86,15 @@ public function getMessage() { return $this->message; } + + /** + * @param object $target + * + * @return Envelope|object The original message or the envelope if the target supports it + * (i.e implements {@link EnvelopeAwareInterface}). + */ + public function getMessageFor($target) + { + return $target instanceof EnvelopeAwareInterface ? $this : $this->message; + } } diff --git a/src/Symfony/Component/Messenger/Middleware/Enhancers/ActivationMiddlewareDecorator.php b/src/Symfony/Component/Messenger/Middleware/Enhancers/ActivationMiddlewareDecorator.php new file mode 100644 index 0000000000000..1c812ccc12153 --- /dev/null +++ b/src/Symfony/Component/Messenger/Middleware/Enhancers/ActivationMiddlewareDecorator.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\Messenger\Middleware\Enhancers; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\EnvelopeAwareInterface; +use Symfony\Component\Messenger\Middleware\MiddlewareInterface; + +/** + * Execute the inner middleware according to an activation strategy. + * + * @author Maxime Steinhausser + */ +class ActivationMiddlewareDecorator implements MiddlewareInterface, EnvelopeAwareInterface +{ + private $inner; + private $activated; + + /** + * @param bool|callable $activated + */ + public function __construct(MiddlewareInterface $inner, $activated) + { + $this->inner = $inner; + $this->activated = $activated; + } + + /** + * @param Envelope $message + */ + public function handle($message, callable $next) + { + if (\is_callable($this->activated) ? ($this->activated)($message) : $this->activated) { + return $this->inner->handle($message->getMessageFor($this->inner), $next); + } + + return $next($message); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/Enhancers/ActivationMiddlewareDecoratorTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/Enhancers/ActivationMiddlewareDecoratorTest.php new file mode 100644 index 0000000000000..32b4a7d0f2fdf --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Middleware/Enhancers/ActivationMiddlewareDecoratorTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Middleware\Enhancers; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\EnvelopeAwareInterface; +use Symfony\Component\Messenger\Middleware\Enhancers\ActivationMiddlewareDecorator; +use Symfony\Component\Messenger\Middleware\MiddlewareInterface; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; + +/** + * @author Maxime Steinhausser + */ +class ActivationMiddlewareDecoratorTest extends TestCase +{ + public function testExecuteMiddlewareOnActivated() + { + $message = new DummyMessage('Hello'); + $envelope = Envelope::wrap($message); + + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + $next->expects($this->never())->method('__invoke'); + + $middleware = $this->createMock(MiddlewareInterface::class); + $middleware->expects($this->once())->method('handle')->with($message, $next)->willReturn('Hello from middleware'); + + $decorator = new ActivationMiddlewareDecorator($middleware, true); + + $this->assertSame('Hello from middleware', $decorator->handle($envelope, $next)); + } + + public function testExecuteMiddlewareOnActivatedWithCallable() + { + $message = new DummyMessage('Hello'); + $envelope = Envelope::wrap($message); + + $activated = $this->createPartialMock(\stdClass::class, array('__invoke')); + $activated->expects($this->once())->method('__invoke')->with($envelope)->willReturn(true); + + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + $next->expects($this->never())->method('__invoke'); + + $middleware = $this->createMock(MiddlewareInterface::class); + $middleware->expects($this->once())->method('handle')->with($message, $next)->willReturn('Hello from middleware'); + + $decorator = new ActivationMiddlewareDecorator($middleware, $activated); + + $this->assertSame('Hello from middleware', $decorator->handle($envelope, $next)); + } + + public function testExecuteEnvelopeAwareMiddlewareWithEnvelope() + { + $message = new DummyMessage('Hello'); + $envelope = Envelope::wrap($message); + + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + $next->expects($this->never())->method('__invoke'); + + $middleware = $this->createMock(array(MiddlewareInterface::class, EnvelopeAwareInterface::class)); + $middleware->expects($this->once())->method('handle')->with($envelope, $next)->willReturn('Hello from middleware'); + + $decorator = new ActivationMiddlewareDecorator($middleware, true); + + $this->assertSame('Hello from middleware', $decorator->handle($envelope, $next)); + } + + public function testExecuteMiddlewareOnDeactivated() + { + $message = new DummyMessage('Hello'); + $envelope = Envelope::wrap($message); + + $next = $this->createPartialMock(\stdClass::class, array('__invoke')); + $next->expects($this->once())->method('__invoke')->with($envelope)->willReturn('Hello from $next'); + + $middleware = $this->createMock(MiddlewareInterface::class); + $middleware->expects($this->never())->method('handle'); + + $decorator = new ActivationMiddlewareDecorator($middleware, false); + + $this->assertSame('Hello from $next', $decorator->handle($envelope, $next)); + } +} From 42186a2bac29f073e2dd70abddd565a59794f020 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Fri, 4 May 2018 18:10:27 +0200 Subject: [PATCH 018/125] [DI] Select specific key from an array resolved env var --- .../DependencyInjection/EnvVarProcessor.php | 20 ++++ .../RegisterEnvVarProcessorsPassTest.php | 1 + .../Tests/EnvVarProcessorTest.php | 106 ++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index 43022ddb7b9d3..e91df9670b5a1 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -41,6 +41,7 @@ public static function getProvidedTypes() 'float' => 'float', 'int' => 'int', 'json' => 'array', + 'key' => 'bool|int|float|string|array', 'resolve' => 'string', 'string' => 'string', ); @@ -53,6 +54,25 @@ public function getEnv($prefix, $name, \Closure $getEnv) { $i = strpos($name, ':'); + if ('key' === $prefix) { + if (false === $i) { + throw new RuntimeException(sprintf('Invalid configuration: env var "key:%s" does not contain a key specifier.', $name)); + } + + $next = substr($name, $i + 1); + $key = substr($name, 0, $i); + $array = $getEnv($next); + + if (!is_array($array)) { + throw new RuntimeException(sprintf('Resolved value of "%s" did not result in an array value.', $next)); + } + if (!array_key_exists($key, $array)) { + throw new RuntimeException(sprintf('Key "%s" not found in "%s" (resolved from "%s")', $key, json_encode($array), $next)); + } + + return $array[$key]; + } + if ('file' === $prefix) { if (!is_scalar($file = $getEnv($name))) { throw new RuntimeException(sprintf('Invalid file name: env var "%s" is non-scalar.', $name)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php index e330017bcd8e8..4681092ca7849 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterEnvVarProcessorsPassTest.php @@ -38,6 +38,7 @@ public function testSimpleProcessor() 'float' => array('float'), 'int' => array('int'), 'json' => array('array'), + 'key' => array('bool', 'int', 'float', 'string', 'array'), 'resolve' => array('string'), 'string' => array('string'), ); diff --git a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php index 79b3e47c79de9..5cd3a68b21baa 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php @@ -314,4 +314,110 @@ public function testGetEnvUnknown() return 'foo'; }); } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Invalid configuration: env var "key:foo" does not contain a key specifier. + */ + public function testGetEnvKeyInvalidKey() + { + $processor = new EnvVarProcessor(new Container()); + + $processor->getEnv('key', 'foo', function ($name) { + $this->fail('Should not get here'); + }); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Resolved value of "foo" did not result in an array value. + * @dataProvider noArrayValues + */ + public function testGetEnvKeyNoArrayResult($value) + { + $processor = new EnvVarProcessor(new Container()); + + $processor->getEnv('key', 'index:foo', function ($name) use ($value) { + $this->assertSame('foo', $name); + + return $value; + }); + } + + public function noArrayValues() + { + return array( + array(null), + array('string'), + array(1), + array(true), + ); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Key "index" not found in + * @dataProvider invalidArrayValues + */ + public function testGetEnvKeyArrayKeyNotFound($value) + { + $processor = new EnvVarProcessor(new Container()); + + $processor->getEnv('key', 'index:foo', function ($name) use ($value) { + $this->assertSame('foo', $name); + + return $value; + }); + } + + public function invalidArrayValues() + { + return array( + array(array()), + array(array('index2' => 'value')), + array(array('index', 'index2')), + ); + } + + /** + * @dataProvider arrayValues + */ + public function testGetEnvKey($value) + { + $processor = new EnvVarProcessor(new Container()); + + $this->assertSame($value['index'], $processor->getEnv('key', 'index:foo', function ($name) use ($value) { + $this->assertSame('foo', $name); + + return $value; + })); + } + + public function arrayValues() + { + return array( + array(array('index' => 'password')), + array(array('index' => 'true')), + array(array('index' => false)), + array(array('index' => '1')), + array(array('index' => 1)), + array(array('index' => '1.1')), + array(array('index' => 1.1)), + array(array('index' => array())), + array(array('index' => array('val1', 'val2'))), + ); + } + + public function testGetEnvKeyChained() + { + $processor = new EnvVarProcessor(new Container()); + + $this->assertSame('password', $processor->getEnv('key', 'index:file:foo', function ($name) { + $this->assertSame('file:foo', $name); + + return array( + 'index' => 'password', + ); + })); + } } From 57a1dd1c57170f610c8396001c24f18c03f45f81 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 21 May 2018 20:43:31 +0200 Subject: [PATCH 019/125] fix deps=low --- src/Symfony/Component/HttpKernel/composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 816af1222faf5..a66f70c80f2bf 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -28,7 +28,7 @@ "symfony/config": "~3.4|~4.0", "symfony/console": "~3.4|~4.0", "symfony/css-selector": "~3.4|~4.0", - "symfony/dependency-injection": "^4.1", + "symfony/dependency-injection": "^4.2", "symfony/dom-crawler": "~3.4|~4.0", "symfony/expression-language": "~3.4|~4.0", "symfony/finder": "~3.4|~4.0", @@ -45,7 +45,7 @@ }, "conflict": { "symfony/config": "<3.4", - "symfony/dependency-injection": "<4.1", + "symfony/dependency-injection": "<4.2", "symfony/var-dumper": "<4.1", "twig/twig": "<1.34|<2.4,>=2" }, From a71ba784782b87317694add148f8e7434767c6f7 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Thu, 17 May 2018 18:15:49 +0200 Subject: [PATCH 020/125] [Security][SecurityBundle] FirewallMap/FirewallContext deprecations --- UPGRADE-4.2.md | 8 ++++ UPGRADE-5.0.md | 4 ++ .../Security/FirewallContext.php | 1 + .../SecurityDataCollectorTest.php | 2 +- .../Debug/TraceableFirewallListenerTest.php | 2 +- .../Tests/Security/FirewallContextTest.php | 9 +++++ .../Component/Security/Http/Firewall.php | 8 +++- .../Security/Http/FirewallMapInterface.php | 5 ++- .../Security/Http/Tests/FirewallTest.php | 37 ++++++++++++++++++- 9 files changed, 70 insertions(+), 6 deletions(-) diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 23982ef159b36..2bac6f514b7ab 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -5,3 +5,11 @@ Security -------- * Using the `has_role()` function in security expressions is deprecated, use the `is_granted()` function instead. + * Not returning an array of 3 elements from `FirewallMapInterface::getListeners()` is deprecated, the 3rd element + must be an instance of `LogoutListener` or `null`. + +SecurityBundle +-------------- + + * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor is deprecated, + pass a `LogoutListener` instance instead. diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 61b7237b44923..48be0e094a65b 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -78,6 +78,8 @@ Security * The `ContextListener::setLogoutOnUserChange()` method has been removed. * The `Symfony\Component\Security\Core\User\AdvancedUserInterface` has been removed. * The `ExpressionVoter::addExpressionLanguageProvider()` method has been removed. + * The `FirewallMapInterface::getListeners()` method must return an array of 3 elements, + the 3rd one must be either a `LogoutListener` instance or `null`. SecurityBundle -------------- @@ -85,6 +87,8 @@ SecurityBundle * The `logout_on_user_change` firewall option has been removed. * The `switch_user.stateless` firewall option has been removed. * The `SecurityUserValueResolver` class has been removed. + * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor + now throws a `\TypeError`, pass a `LogoutListener` instance instead. Translation ----------- diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php index a3b7f15406919..ac0d1f2406841 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php @@ -36,6 +36,7 @@ public function __construct(iterable $listeners, ExceptionListener $exceptionLis $this->exceptionListener = $exceptionListener; if ($logoutListener instanceof FirewallConfig) { $this->config = $logoutListener; + @trigger_error(sprintf('Passing an instance of %s as 3rd argument to %s() is deprecated since Symfony 4.2. Pass a %s instance instead.', FirewallConfig::class, __METHOD__, LogoutListener::class), E_USER_DEPRECATED); } elseif (null === $logoutListener || $logoutListener instanceof LogoutListener) { $this->logoutListener = $logoutListener; $this->config = $config; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 548b1939fc251..0ceacd22468b3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -208,7 +208,7 @@ public function testGetListeners() ->expects($this->once()) ->method('getListeners') ->with($request) - ->willReturn(array(array($listener), null)); + ->willReturn(array(array($listener), null, null)); $firewall = new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()); $firewall->onKernelRequest($event); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php index 287ba531f4031..829d1f11e67d2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php @@ -51,7 +51,7 @@ public function testOnKernelRequestRecordsListeners() ->expects($this->once()) ->method('getListeners') ->with($request) - ->willReturn(array(array($listener), null)); + ->willReturn(array(array($listener), null, null)); $firewall = new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()); $firewall->onKernelRequest($event); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php index 520a129716f4f..129c72fab9107 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php @@ -40,6 +40,15 @@ public function testGetters() $this->assertEquals($config, $context->getConfig()); } + /** + * @group legacy + * @expectedDeprecation Passing an instance of Symfony\Bundle\SecurityBundle\Security\FirewallConfig as 3rd argument to Symfony\Bundle\SecurityBundle\Security\FirewallContext::__construct() is deprecated since Symfony 4.2. Pass a Symfony\Component\Security\Http\Firewall\LogoutListener instance instead. + */ + public function testFirewallConfigAs3rdConstructorArgument() + { + new FirewallContext(array(), $this->getExceptionListenerMock(), new FirewallConfig('main', 'user_checker', 'request_matcher')); + } + private function getExceptionListenerMock() { return $this diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php index 9bee596759bd8..b533f547b643b 100644 --- a/src/Symfony/Component/Security/Http/Firewall.php +++ b/src/Symfony/Component/Security/Http/Firewall.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Http\Firewall\LogoutListener; /** * Firewall uses a FirewallMap to register security listeners for the given @@ -49,9 +50,14 @@ public function onKernelRequest(GetResponseEvent $event) // register listeners for this firewall $listeners = $this->map->getListeners($event->getRequest()); + if (3 !== \count($listeners)) { + @trigger_error(sprintf('Not returning an array of 3 elements from %s::getListeners() is deprecated since Symfony 4.2, the 3rd element must be an instance of %s or null.', FirewallMapInterface::class, LogoutListener::class), E_USER_DEPRECATED); + $listeners[2] = null; + } + $authenticationListeners = $listeners[0]; $exceptionListener = $listeners[1]; - $logoutListener = isset($listeners[2]) ? $listeners[2] : null; + $logoutListener = $listeners[2]; if (null !== $exceptionListener) { $this->exceptionListeners[$event->getRequest()] = $exceptionListener; diff --git a/src/Symfony/Component/Security/Http/FirewallMapInterface.php b/src/Symfony/Component/Security/Http/FirewallMapInterface.php index ebdd498123a1c..be7a78ccdb883 100644 --- a/src/Symfony/Component/Security/Http/FirewallMapInterface.php +++ b/src/Symfony/Component/Security/Http/FirewallMapInterface.php @@ -30,7 +30,10 @@ interface FirewallMapInterface * If there is no exception listener, the second element of the outer array * must be null. * - * @return array of the format array(array(AuthenticationListener), ExceptionListener) + * If there is no logout listener, the third element of the outer array + * must be null. + * + * @return array of the format array(array(AuthenticationListener), ExceptionListener, LogoutListener) */ public function getListeners(Request $request); } diff --git a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php index bd475bb4e5b1f..acc231dc93581 100644 --- a/src/Symfony/Component/Security/Http/Tests/FirewallTest.php +++ b/src/Symfony/Component/Security/Http/Tests/FirewallTest.php @@ -12,10 +12,14 @@ namespace Symfony\Component\Security\Http\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Http\Firewall; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\FirewallMapInterface; class FirewallTest extends TestCase { @@ -37,7 +41,7 @@ public function testOnKernelRequestRegistersExceptionListener() ->expects($this->once()) ->method('getListeners') ->with($this->equalTo($request)) - ->will($this->returnValue(array(array(), $listener))) + ->will($this->returnValue(array(array(), $listener, null))) ; $event = new GetResponseEvent($this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(), $request, HttpKernelInterface::MASTER_REQUEST); @@ -66,7 +70,7 @@ public function testOnKernelRequestStopsWhenThereIsAResponse() $map ->expects($this->once()) ->method('getListeners') - ->will($this->returnValue(array(array($first, $second), null))) + ->will($this->returnValue(array(array($first, $second), null, null))) ; $event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent') @@ -107,4 +111,33 @@ public function testOnKernelRequestWithSubRequest() $this->assertFalse($event->hasResponse()); } + + /** + * @group legacy + * @expectedDeprecation Not returning an array of 3 elements from Symfony\Component\Security\Http\FirewallMapInterface::getListeners() is deprecated since Symfony 4.2, the 3rd element must be an instance of Symfony\Component\Security\Http\Firewall\LogoutListener or null. + */ + public function testMissingLogoutListener() + { + $dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock(); + + $listener = $this->getMockBuilder(ExceptionListener::class)->disableOriginalConstructor()->getMock(); + $listener + ->expects($this->once()) + ->method('register') + ->with($this->equalTo($dispatcher)) + ; + + $request = new Request(); + + $map = $this->getMockBuilder(FirewallMapInterface::class)->getMock(); + $map + ->expects($this->once()) + ->method('getListeners') + ->with($this->equalTo($request)) + ->willReturn(array(array(), $listener)) + ; + + $firewall = new Firewall($map, $dispatcher); + $firewall->onKernelRequest(new GetResponseEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST)); + } } From 860d4549c242c66348f2eddcf6d6b6186e334e11 Mon Sep 17 00:00:00 2001 From: Iltar van der Berg Date: Thu, 19 Apr 2018 12:35:58 +0200 Subject: [PATCH 021/125] No more support for custom anon/remember tokens based on FQCN --- UPGRADE-4.2.md | 13 ++ UPGRADE-5.0.md | 3 + .../Bundle/SecurityBundle/CHANGELOG.md | 9 + .../Resources/config/security.xml | 4 +- src/Symfony/Component/Security/CHANGELOG.md | 5 + .../AuthenticationTrustResolver.php | 24 ++- .../AuthenticationTrustResolverTest.php | 161 +++++++++++++++++- .../Authorization/ExpressionLanguageTest.php | 4 +- .../Voter/AuthenticatedVoterTest.php | 10 +- 9 files changed, 214 insertions(+), 19 deletions(-) diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 23982ef159b36..b5f7f022aa8f0 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -5,3 +5,16 @@ Security -------- * Using the `has_role()` function in security expressions is deprecated, use the `is_granted()` function instead. + * Passing custom class names to the + `Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver` to define + custom anonymous and remember me token classes is deprecated. To + use custom tokens, extend the existing `Symfony\Component\Security\Core\Authentication\Token\AnonymousToken` + or `Symfony\Component\Security\Core\Authentication\Token\RememberMeToken`. + +SecurityBundle +-------------- + + * Using the `security.authentication.trust_resolver.anonymous_class` and + `security.authentication.trust_resolver.rememberme_class` parameters to define + the token classes is deprecated. To use + custom tokens extend the existing AnonymousToken and RememberMeToken. diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 61b7237b44923..66ae66c172340 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -78,6 +78,7 @@ Security * The `ContextListener::setLogoutOnUserChange()` method has been removed. * The `Symfony\Component\Security\Core\User\AdvancedUserInterface` has been removed. * The `ExpressionVoter::addExpressionLanguageProvider()` method has been removed. + * The `AuthenticationTrustResolver` constructor arguments have been removed. SecurityBundle -------------- @@ -85,6 +86,8 @@ SecurityBundle * The `logout_on_user_change` firewall option has been removed. * The `switch_user.stateless` firewall option has been removed. * The `SecurityUserValueResolver` class has been removed. + * The `security.authentication.trust_resolver.anonymous_class` parameter has been removed. + * The `security.authentication.trust_resolver.rememberme_class` parameter has been removed. Translation ----------- diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 648189bb15a68..32b9f618adafd 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +4.2.0 +----- + + * Using the `security.authentication.trust_resolver.anonymous_class` and + `security.authentication.trust_resolver.rememberme_class` parameters to define + the token classes is deprecated. To use + custom tokens extend the existing `Symfony\Component\Security\Core\Authentication\Token\AnonymousToken` + or `Symfony\Component\Security\Core\Authentication\Token\RememberMeToken`. + 4.1.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 65c48e0855cb7..a9d81bc4174a9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -5,8 +5,8 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - Symfony\Component\Security\Core\Authentication\Token\AnonymousToken - Symfony\Component\Security\Core\Authentication\Token\RememberMeToken + null + null diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 87939e3a26388..9348d1f38f08e 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -6,6 +6,11 @@ CHANGELOG * added the `is_granted()` function in security expressions * deprecated the `has_role()` function in security expressions, use `is_granted()` instead +* Passing custom class names to the + `Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver` to define + custom anonymous and remember me token classes is deprecated. To + use custom tokens, extend the existing `Symfony\Component\Security\Core\Authentication\Token\AnonymousToken` + or `Symfony\Component\Security\Core\Authentication\Token\RememberMeToken`. 4.1.0 ----- diff --git a/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php b/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php index da7c34e58625b..cba6a8708243e 100644 --- a/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php +++ b/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Security\Core\Authentication; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; /** @@ -23,10 +25,18 @@ class AuthenticationTrustResolver implements AuthenticationTrustResolverInterfac private $anonymousClass; private $rememberMeClass; - public function __construct(string $anonymousClass, string $rememberMeClass) + public function __construct(?string $anonymousClass = null, ?string $rememberMeClass = null) { $this->anonymousClass = $anonymousClass; $this->rememberMeClass = $rememberMeClass; + + if (null !== $anonymousClass && !is_a($anonymousClass, AnonymousToken::class, true)) { + @trigger_error(sprintf('Configuring a custom anonymous token class is deprecated since Symfony 4.2; have the "%s" class extend the "%s" class instead, and remove the "%s" constructor argument.', $anonymousClass, AnonymousToken::class, self::class), E_USER_DEPRECATED); + } + + if (null !== $rememberMeClass && !is_a($rememberMeClass, RememberMeToken::class, true)) { + @trigger_error(sprintf('Configuring a custom remember me token class is deprecated since Symfony 4.2; have the "%s" class extend the "%s" class instead, and remove the "%s" constructor argument.', $rememberMeClass, RememberMeToken::class, self::class), E_USER_DEPRECATED); + } } /** @@ -38,7 +48,11 @@ public function isAnonymous(TokenInterface $token = null) return false; } - return $token instanceof $this->anonymousClass; + if (null !== $this->anonymousClass) { + return $token instanceof $this->anonymousClass; + } + + return $token instanceof AnonymousToken; } /** @@ -50,7 +64,11 @@ public function isRememberMe(TokenInterface $token = null) return false; } - return $token instanceof $this->rememberMeClass; + if (null !== $this->rememberMeClass) { + return $token instanceof $this->rememberMeClass; + } + + return $token instanceof RememberMeToken; } /** diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationTrustResolverTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationTrustResolverTest.php index 55ca05b43b5fb..fb5a885161bf1 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationTrustResolverTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationTrustResolverTest.php @@ -13,10 +13,82 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; class AuthenticationTrustResolverTest extends TestCase { public function testIsAnonymous() + { + $resolver = new AuthenticationTrustResolver(); + $this->assertFalse($resolver->isAnonymous(null)); + $this->assertFalse($resolver->isAnonymous($this->getToken())); + $this->assertFalse($resolver->isAnonymous($this->getRememberMeToken())); + $this->assertFalse($resolver->isAnonymous(new FakeCustomToken())); + $this->assertTrue($resolver->isAnonymous(new RealCustomAnonymousToken())); + $this->assertTrue($resolver->isAnonymous($this->getAnonymousToken())); + } + + public function testIsRememberMe() + { + $resolver = new AuthenticationTrustResolver(); + + $this->assertFalse($resolver->isRememberMe(null)); + $this->assertFalse($resolver->isRememberMe($this->getToken())); + $this->assertFalse($resolver->isRememberMe($this->getAnonymousToken())); + $this->assertFalse($resolver->isRememberMe(new FakeCustomToken())); + $this->assertTrue($resolver->isRememberMe(new RealCustomRememberMeToken())); + $this->assertTrue($resolver->isRememberMe($this->getRememberMeToken())); + } + + public function testisFullFledged() + { + $resolver = new AuthenticationTrustResolver(); + + $this->assertFalse($resolver->isFullFledged(null)); + $this->assertFalse($resolver->isFullFledged($this->getAnonymousToken())); + $this->assertFalse($resolver->isFullFledged($this->getRememberMeToken())); + $this->assertFalse($resolver->isFullFledged(new RealCustomAnonymousToken())); + $this->assertFalse($resolver->isFullFledged(new RealCustomRememberMeToken())); + $this->assertTrue($resolver->isFullFledged($this->getToken())); + $this->assertTrue($resolver->isFullFledged(new FakeCustomToken())); + } + + /** + * @group legacy + * @expectedDeprecation Configuring a custom anonymous token class is deprecated since Symfony 4.2; have the "Symfony\Component\Security\Core\Tests\Authentication\FakeCustomToken" class extend the "Symfony\Component\Security\Core\Authentication\Token\AnonymousToken" class instead, and remove the "Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver" constructor argument. + */ + public function testsAnonymousDeprecationWithCustomClasses() + { + $resolver = new AuthenticationTrustResolver(FakeCustomToken::class); + + $this->assertTrue($resolver->isAnonymous(new FakeCustomToken())); + } + + /** + * @group legacy + * @expectedDeprecation Configuring a custom remember me token class is deprecated since Symfony 4.2; have the "Symfony\Component\Security\Core\Tests\Authentication\FakeCustomToken" class extend the "Symfony\Component\Security\Core\Authentication\Token\RememberMeToken" class instead, and remove the "Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver" constructor argument. + */ + public function testIsRememberMeDeprecationWithCustomClasses() + { + $resolver = new AuthenticationTrustResolver(null, FakeCustomToken::class); + + $this->assertTrue($resolver->isRememberMe(new FakeCustomToken())); + } + + /** + * @group legacy + * @expectedDeprecation Configuring a custom remember me token class is deprecated since Symfony 4.2; have the "Symfony\Component\Security\Core\Tests\Authentication\FakeCustomToken" class extend the "Symfony\Component\Security\Core\Authentication\Token\RememberMeToken" class instead, and remove the "Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver" constructor argument. + */ + public function testIsFullFledgedDeprecationWithCustomClasses() + { + $resolver = new AuthenticationTrustResolver(FakeCustomToken::class, FakeCustomToken::class); + + $this->assertFalse($resolver->isFullFledged(new FakeCustomToken())); + } + + public function testIsAnonymousWithClassAsConstructorButStillExtending() { $resolver = $this->getResolver(); @@ -24,9 +96,10 @@ public function testIsAnonymous() $this->assertFalse($resolver->isAnonymous($this->getToken())); $this->assertFalse($resolver->isAnonymous($this->getRememberMeToken())); $this->assertTrue($resolver->isAnonymous($this->getAnonymousToken())); + $this->assertTrue($resolver->isAnonymous(new RealCustomAnonymousToken())); } - public function testIsRememberMe() + public function testIsRememberMeWithClassAsConstructorButStillExtending() { $resolver = $this->getResolver(); @@ -34,15 +107,18 @@ public function testIsRememberMe() $this->assertFalse($resolver->isRememberMe($this->getToken())); $this->assertFalse($resolver->isRememberMe($this->getAnonymousToken())); $this->assertTrue($resolver->isRememberMe($this->getRememberMeToken())); + $this->assertTrue($resolver->isRememberMe(new RealCustomRememberMeToken())); } - public function testisFullFledged() + public function testisFullFledgedWithClassAsConstructorButStillExtending() { $resolver = $this->getResolver(); $this->assertFalse($resolver->isFullFledged(null)); $this->assertFalse($resolver->isFullFledged($this->getAnonymousToken())); $this->assertFalse($resolver->isFullFledged($this->getRememberMeToken())); + $this->assertFalse($resolver->isFullFledged(new RealCustomAnonymousToken())); + $this->assertFalse($resolver->isFullFledged(new RealCustomRememberMeToken())); $this->assertTrue($resolver->isFullFledged($this->getToken())); } @@ -69,3 +145,84 @@ protected function getResolver() ); } } + +class FakeCustomToken implements TokenInterface +{ + public function serialize() + { + } + + public function unserialize($serialized) + { + } + + public function __toString() + { + } + + public function getRoles() + { + } + + public function getCredentials() + { + } + + public function getUser() + { + } + + public function setUser($user) + { + } + + public function getUsername() + { + } + + public function isAuthenticated() + { + } + + public function setAuthenticated($isAuthenticated) + { + } + + public function eraseCredentials() + { + } + + public function getAttributes() + { + } + + public function setAttributes(array $attributes) + { + } + + public function hasAttribute($name) + { + } + + public function getAttribute($name) + { + } + + public function setAttribute($name, $value) + { + } +} + +class RealCustomAnonymousToken extends AnonymousToken +{ + public function __construct() + { + } +} + +class RealCustomRememberMeToken extends RememberMeToken +{ + public function __construct() + { + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php index 6c05ecfab506a..7991715b7c18e 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/ExpressionLanguageTest.php @@ -31,10 +31,8 @@ class ExpressionLanguageTest extends TestCase */ public function testIsAuthenticated($token, $expression, $result) { - $anonymousTokenClass = 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken'; - $rememberMeTokenClass = 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\RememberMeToken'; $expressionLanguage = new ExpressionLanguage(); - $trustResolver = new AuthenticationTrustResolver($anonymousTokenClass, $rememberMeTokenClass); + $trustResolver = new AuthenticationTrustResolver(); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); $accessDecisionManager = new AccessDecisionManager(array(new RoleVoter())); diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php index 1ba7e39163ad1..0651526f494a9 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php @@ -23,7 +23,7 @@ class AuthenticatedVoterTest extends TestCase */ public function testVote($authenticated, $attributes, $expected) { - $voter = new AuthenticatedVoter($this->getResolver()); + $voter = new AuthenticatedVoter(new AuthenticationTrustResolver()); $this->assertSame($expected, $voter->vote($this->getToken($authenticated), null, $attributes)); } @@ -52,14 +52,6 @@ public function getVoteTests() ); } - protected function getResolver() - { - return new AuthenticationTrustResolver( - 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\AnonymousToken', - 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\RememberMeToken' - ); - } - protected function getToken($authenticated) { if ('fully' === $authenticated) { From c250fbdda08a7279155bb18af33129bda11d3e41 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 May 2018 21:30:19 +0200 Subject: [PATCH 022/125] [Cache] Remove TaggableCacheInterface, alias cache.app.taggable to CacheInterface --- .../Resources/config/cache.xml | 3 +- .../Cache/Adapter/TagAwareAdapter.php | 4 +-- .../Adapter/TraceableTagAwareAdapter.php | 4 +-- src/Symfony/Component/Cache/CHANGELOG.md | 2 +- .../Component/Cache/CacheInterface.php | 6 +--- .../Cache/TaggableCacheInterface.php | 35 ------------------- 6 files changed, 7 insertions(+), 47 deletions(-) delete mode 100644 src/Symfony/Component/Cache/TaggableCacheInterface.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index d0e596ab83338..cd4d51e2c3936 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -126,9 +126,8 @@ - - + diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index 257e404e1c43e..e810f5d00fb0d 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -13,17 +13,17 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\InvalidArgumentException; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; -use Symfony\Component\Cache\TaggableCacheInterface; use Symfony\Component\Cache\Traits\GetTrait; use Symfony\Component\Cache\Traits\ProxyTrait; /** * @author Nicolas Grekas */ -class TagAwareAdapter implements TagAwareAdapterInterface, TaggableCacheInterface, PruneableInterface, ResettableInterface +class TagAwareAdapter implements CacheInterface, TagAwareAdapterInterface, PruneableInterface, ResettableInterface { const TAGS_PREFIX = "\0tags\0"; diff --git a/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php index 2fda8b360240e..c597c81c386ea 100644 --- a/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Cache\Adapter; -use Symfony\Component\Cache\TaggableCacheInterface; +use Symfony\Component\Cache\CacheInterface; /** * @author Robin Chalas */ -class TraceableTagAwareAdapter extends TraceableAdapter implements TaggableCacheInterface, TagAwareAdapterInterface +class TraceableTagAwareAdapter extends TraceableAdapter implements CacheInterface, TagAwareAdapterInterface { public function __construct(TagAwareAdapterInterface $pool) { diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index d21b2cbda4df1..f6fb43ebe7874 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 4.2.0 ----- - * added `CacheInterface` and `TaggableCacheInterface` + * added `CacheInterface`, which should become the preferred way to use a cache * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool 3.4.0 diff --git a/src/Symfony/Component/Cache/CacheInterface.php b/src/Symfony/Component/Cache/CacheInterface.php index c5c877ddbd746..49194d135e9bd 100644 --- a/src/Symfony/Component/Cache/CacheInterface.php +++ b/src/Symfony/Component/Cache/CacheInterface.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Cache; -use Psr\Cache\CacheItemInterface; - /** * Gets and stores items from a cache. * @@ -22,14 +20,12 @@ * - the corresponding PSR-6 CacheItemInterface object, * allowing time-based expiration control. * - * If you need tag-based invalidation, use TaggableCacheInterface instead. - * * @author Nicolas Grekas */ interface CacheInterface { /** - * @param callable(CacheItemInterface):mixed $callback Should return the computed value for the given key/item + * @param callable(CacheItem):mixed $callback Should return the computed value for the given key/item * * @return mixed The value corresponding to the provided key */ diff --git a/src/Symfony/Component/Cache/TaggableCacheInterface.php b/src/Symfony/Component/Cache/TaggableCacheInterface.php deleted file mode 100644 index c112e72586d74..0000000000000 --- a/src/Symfony/Component/Cache/TaggableCacheInterface.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\Cache; - -/** - * Gets and stores items from a tag-aware cache. - * - * On cache misses, a callback is called that should return the missing value. - * It is given two arguments: - * - the missing cache key - * - the corresponding Symfony CacheItem object, - * allowing time-based *and* tags-based expiration control - * - * If you don't need tags-based invalidation, use CacheInterface instead. - * - * @author Nicolas Grekas - */ -interface TaggableCacheInterface extends CacheInterface -{ - /** - * @param callable(CacheItem):mixed $callback Should return the computed value for the given key/item - * - * @return mixed The value corresponding to the provided key - */ - public function get(string $key, callable $callback); -} From 896be4cc2b7235745911876bbcc74b9c577bf39b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 14 Apr 2018 23:12:15 -0500 Subject: [PATCH 023/125] [FrameworkBundle] Allow configuring taggable cache pools --- .../Bundle/FrameworkBundle/CHANGELOG.md | 5 +++++ .../Compiler/CachePoolClearerPass.php | 6 ++--- .../Compiler/CachePoolPass.php | 12 +++++----- .../DependencyInjection/Configuration.php | 1 + .../FrameworkExtension.php | 22 ++++++++++++++++++- .../Compiler/CachePoolClearerPassTest.php | 13 +++++++++-- .../Compiler/CachePoolPassTest.php | 22 +++++++++++++++++++ .../Tests/Functional/CachePoolsTest.php | 20 +++++++++++++++++ .../Functional/app/CachePools/config.yml | 12 ++++++++++ .../app/CachePools/redis_config.yml | 14 ++++++++++++ .../app/CachePools/redis_custom_config.yml | 14 ++++++++++++ 11 files changed, 130 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index b03567d68e35a..139041aba81a8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + + * Allowed configuring taggable cache pools via a new `framework.cache.pools.tags` option (bool|service-id) + 4.1.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php index 094712ded69d3..c285e935cbad8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php @@ -31,9 +31,9 @@ public function process(ContainerBuilder $container) foreach ($container->findTaggedServiceIds('cache.pool.clearer') as $id => $attr) { $clearer = $container->getDefinition($id); $pools = array(); - foreach ($clearer->getArgument(0) as $id => $ref) { - if ($container->hasDefinition($id)) { - $pools[$id] = new Reference($id); + foreach ($clearer->getArgument(0) as $name => $ref) { + if ($container->hasDefinition($ref)) { + $pools[$name] = new Reference($ref); } } $clearer->replaceArgument(0, $pools); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php index 2530d9e75e024..670c5edd36440 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php @@ -41,6 +41,7 @@ public function process(ContainerBuilder $container) $clearers = array(); $attributes = array( 'provider', + 'name', 'namespace', 'default_lifetime', 'reset', @@ -56,8 +57,9 @@ public function process(ContainerBuilder $container) $tags[0] += $t[0]; } } + $name = $tags[0]['name'] ?? $id; if (!isset($tags[0]['namespace'])) { - $tags[0]['namespace'] = $this->getNamespace($seed, $id); + $tags[0]['namespace'] = $this->getNamespace($seed, $name); } if (isset($tags[0]['clearer'])) { $clearer = $tags[0]['clearer']; @@ -67,7 +69,7 @@ public function process(ContainerBuilder $container) } else { $clearer = null; } - unset($tags[0]['clearer']); + unset($tags[0]['clearer'], $tags[0]['name']); if (isset($tags[0]['provider'])) { $tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider'])); @@ -86,14 +88,14 @@ public function process(ContainerBuilder $container) unset($tags[0][$attr]); } if (!empty($tags[0])) { - throw new InvalidArgumentException(sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "namespace", "default_lifetime" and "reset", found "%s".', $id, implode('", "', array_keys($tags[0])))); + throw new InvalidArgumentException(sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime" and "reset", found "%s".', $id, implode('", "', array_keys($tags[0])))); } if (null !== $clearer) { - $clearers[$clearer][$id] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); + $clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); } - $pools[$id] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); + $pools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); } $clearer = 'cache.global_clearer'; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 31e8e6a6ba912..e9a380dd78e63 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -870,6 +870,7 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->prototype('array') ->children() ->scalarNode('adapter')->defaultValue('cache.app')->end() + ->scalarNode('tags')->defaultNull()->end() ->booleanNode('public')->defaultFalse()->end() ->integerNode('default_lifetime')->end() ->scalarNode('provider') diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 318af6c8b9a2f..f15e9ac12c435 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -22,6 +22,7 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; @@ -1556,12 +1557,31 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con $config['pools']['cache.'.$name] = array( 'adapter' => $config[$name], 'public' => true, + 'tags' => false, ); } foreach ($config['pools'] as $name => $pool) { + if ($config['pools'][$pool['adapter']]['tags'] ?? false) { + $pool['adapter'] = '.'.$pool['adapter'].'.inner'; + } $definition = new ChildDefinition($pool['adapter']); + + if ($pool['tags']) { + if ($config['pools'][$pool['tags']]['tags'] ?? false) { + $pool['tags'] = '.'.$pool['tags'].'.inner'; + } + $container->register($name, TagAwareAdapter::class) + ->addArgument(new Reference('.'.$name.'.inner')) + ->addArgument(true !== $pool['tags'] ? new Reference($pool['tags']) : null) + ->setPublic($pool['public']) + ; + + $pool['name'] = $name; + $pool['public'] = false; + $name = '.'.$name.'.inner'; + } $definition->setPublic($pool['public']); - unset($pool['adapter'], $pool['public']); + unset($pool['adapter'], $pool['public'], $pool['tags']); $definition->addTag('cache.pool', $pool); $container->setDefinition($name, $definition); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php index 9230405d7560e..243061712a8c4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php @@ -39,6 +39,11 @@ public function testPoolRefsAreWeak() $publicPool->addTag('cache.pool', array('clearer' => 'clearer_alias')); $container->setDefinition('public.pool', $publicPool); + $publicPool = new Definition(); + $publicPool->addArgument('namespace'); + $publicPool->addTag('cache.pool', array('clearer' => 'clearer_alias', 'name' => 'pool2')); + $container->setDefinition('public.pool2', $publicPool); + $privatePool = new Definition(); $privatePool->setPublic(false); $privatePool->addArgument('namespace'); @@ -55,7 +60,11 @@ public function testPoolRefsAreWeak() $pass->process($container); } - $this->assertEquals(array(array('public.pool' => new Reference('public.pool'))), $clearer->getArguments()); - $this->assertEquals(array(array('public.pool' => new Reference('public.pool'))), $globalClearer->getArguments()); + $expected = array(array( + 'public.pool' => new Reference('public.pool'), + 'pool2' => new Reference('public.pool2'), + )); + $this->assertEquals($expected, $clearer->getArguments()); + $this->assertEquals($expected, $globalClearer->getArguments()); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php index 4619301b6e997..44443a52a50e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php @@ -93,6 +93,28 @@ public function testArgsAreReplaced() $this->assertSame(3, $cachePool->getArgument(2)); } + public function testWithNameAttribute() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('kernel.name', 'app'); + $container->setParameter('kernel.environment', 'prod'); + $container->setParameter('cache.prefix.seed', 'foo'); + $cachePool = new Definition(); + $cachePool->addTag('cache.pool', array( + 'name' => 'foobar', + 'provider' => 'foobar', + )); + $cachePool->addArgument(null); + $cachePool->addArgument(null); + $cachePool->addArgument(null); + $container->setDefinition('app.cache_pool', $cachePool); + + $this->cachePoolPass->process($container); + + $this->assertSame('9HvPgAayyh', $cachePool->getArgument(1)); + } + /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Invalid "cache.pool" tag for service "app.cache_pool": accepted attributes are diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php index 9cdb93a493f20..d152f5cd873af 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php @@ -13,6 +13,7 @@ use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\Exception\InvalidArgumentException; class CachePoolsTest extends WebTestCase @@ -94,6 +95,25 @@ private function doTestCachePools($options, $adapterClass) $item = $pool2->getItem($key); $this->assertTrue($item->isHit()); + + $prefix = "\0".TagAwareAdapter::class."\0"; + $pool4 = $container->get('cache.pool4'); + $this->assertInstanceof(TagAwareAdapter::class, $pool4); + $pool4 = (array) $pool4; + $this->assertSame($pool4[$prefix.'pool'], $pool4[$prefix.'tags'] ?? $pool4['tags']); + + $pool5 = $container->get('cache.pool5'); + $this->assertInstanceof(TagAwareAdapter::class, $pool5); + $pool5 = (array) $pool5; + $this->assertSame($pool2, $pool5[$prefix.'tags'] ?? $pool5['tags']); + + $pool6 = $container->get('cache.pool6'); + $this->assertInstanceof(TagAwareAdapter::class, $pool6); + $pool6 = (array) $pool6; + $this->assertSame($pool4[$prefix.'pool'], $pool6[$prefix.'tags'] ?? $pool6['tags']); + + $pool7 = $container->get('cache.pool7'); + $this->assertNotInstanceof(TagAwareAdapter::class, $pool7); } protected static function createKernel(array $options = array()) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml index de1e144dad062..8c7bcb4eb1fac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml @@ -12,3 +12,15 @@ framework: adapter: cache.pool3 cache.pool3: clearer: ~ + cache.pool4: + tags: true + public: true + cache.pool5: + tags: cache.pool2 + public: true + cache.pool6: + tags: cache.pool4 + public: true + cache.pool7: + adapter: cache.pool4 + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml index 3bf10f448f9c2..30c69163d4f2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml @@ -15,3 +15,17 @@ framework: cache.pool2: public: true clearer: ~ + cache.pool3: + clearer: ~ + cache.pool4: + tags: true + public: true + cache.pool5: + tags: cache.pool2 + public: true + cache.pool6: + tags: cache.pool4 + public: true + cache.pool7: + adapter: cache.pool4 + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml index d0a219753eb8e..df20c5357f7a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml @@ -26,3 +26,17 @@ framework: cache.pool2: public: true clearer: ~ + cache.pool3: + clearer: ~ + cache.pool4: + tags: true + public: true + cache.pool5: + tags: cache.pool2 + public: true + cache.pool6: + tags: cache.pool4 + public: true + cache.pool7: + adapter: cache.pool4 + public: true From 3a2eb0d9514f0b322ab31a128aa48b9f89e28d50 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 May 2018 15:22:55 +0200 Subject: [PATCH 024/125] Minor clean up in UPGRADE files --- UPGRADE-4.2.md | 6 +++--- UPGRADE-5.0.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index c714e7f9190ec..7386221a5df9d 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -5,7 +5,7 @@ Security -------- * Using the `has_role()` function in security expressions is deprecated, use the `is_granted()` function instead. - * Not returning an array of 3 elements from `FirewallMapInterface::getListeners()` is deprecated, the 3rd element + * Not returning an array of 3 elements from `FirewallMapInterface::getListeners()` is deprecated, the 3rd element must be an instance of `LogoutListener` or `null`. * Passing custom class names to the `Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver` to define @@ -16,9 +16,9 @@ Security SecurityBundle -------------- - * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor is deprecated, + * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor is deprecated, pass a `LogoutListener` instance instead. - * Using the `security.authentication.trust_resolver.anonymous_class` and + * Using the `security.authentication.trust_resolver.anonymous_class` and `security.authentication.trust_resolver.rememberme_class` parameters to define the token classes is deprecated. To use custom tokens extend the existing AnonymousToken and RememberMeToken. diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 75d8febcadebe..7c4ddf1d8eeb3 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -88,10 +88,10 @@ SecurityBundle * The `logout_on_user_change` firewall option has been removed. * The `switch_user.stateless` firewall option has been removed. * The `SecurityUserValueResolver` class has been removed. - * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor + * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor now throws a `\TypeError`, pass a `LogoutListener` instance instead. - * The `security.authentication.trust_resolver.anonymous_class` parameter has been removed. - * The `security.authentication.trust_resolver.rememberme_class` parameter has been removed. + * The `security.authentication.trust_resolver.anonymous_class` parameter has been removed. + * The `security.authentication.trust_resolver.rememberme_class` parameter has been removed. Translation ----------- From cac37caa7de85cdd4f16072e4f1f86e2c0afed7c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 30 May 2018 05:47:13 +0200 Subject: [PATCH 025/125] [WebProfilerBundle] made Twig bundle an explicit dependency --- src/Symfony/Bundle/WebProfilerBundle/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index 4e3783d695dcb..712ec9cdffda7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -19,7 +19,7 @@ "php": "^7.1.3", "symfony/http-kernel": "~4.1", "symfony/routing": "~3.4|~4.0", - "symfony/twig-bridge": "~3.4|~4.0", + "symfony/twig-bundle": "^3.4.3|^4.0.3", "symfony/var-dumper": "~3.4|~4.0", "twig/twig": "~1.34|~2.4" }, From 1c2f43f17cea2a9470a6d1d70ed96882c1d194dc Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Tue, 22 May 2018 19:48:15 +0200 Subject: [PATCH 026/125] [Messenger][Profiler] Show dispatch caller --- .../views/Collector/messenger.html.twig | 33 ++++++++++++++-- .../views/Profiler/profiler.css.twig | 21 ++++++++++ .../DataCollector/MessengerDataCollector.php | 1 + .../MessengerDataCollectorTest.php | 21 ++++++++-- .../Tests/TraceableMessageBusTest.php | 18 +++++++++ .../Messenger/TraceableMessageBus.php | 39 +++++++++++++++++++ 6 files changed, 125 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig index d8befbbf8dca6..50dfbb9d3a719 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig @@ -55,7 +55,8 @@ .message-bus .badge.status-some-errors { line-height: 16px; border-bottom: 2px solid #B0413E; } - .message-item .sf-toggle-content.sf-toggle-visible { display: table-row-group; } + .message-item tbody.sf-toggle-content.sf-toggle-visible { display: table-row-group; } + td.message-bus-dispatch-caller { background: #f1f2f3; } {% endblock %} @@ -100,12 +101,12 @@ {% macro render_bus_messages(messages, showBus = false) %} {% set discr = random() %} - {% for i, dispatchCall in messages %} + {% for dispatchCall in messages %} - + + + + {% if showBus %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index f9bc41d6a1b54..eb66f63681f8c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -913,6 +913,27 @@ table.logs .metadata { background: rgba(255, 255, 153, 0.5); } +{# Messenger panel + ========================================================================= #} + +#collector-content .message-bus .trace { + border: 1px solid #DDD; + background: #FFF; + padding: 10px; + margin: 0.5em 0; + overflow: auto; +} +#collector-content .message-bus .trace { + font-size: 12px; +} +#collector-content .message-bus .trace li { + margin-bottom: 0; + padding: 0; +} +#collector-content .message-bus .trace li.selected { + background: rgba(255, 255, 153, 0.5); +} + {# Dump panel ========================================================================= #} #collector-content .sf-dump { diff --git a/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php b/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php index ed98f4cfa43f5..d71f16d544952 100644 --- a/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php +++ b/src/Symfony/Component/Messenger/DataCollector/MessengerDataCollector.php @@ -97,6 +97,7 @@ private function collectMessage(string $busName, array $tracedMessage) 'type' => new ClassStub(\get_class($message)), 'value' => $message, ), + 'caller' => $tracedMessage['caller'], ); if (array_key_exists('result', $tracedMessage)) { diff --git a/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php b/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php index d88593e3e747d..7c4fafa34090b 100644 --- a/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php +++ b/src/Symfony/Component/Messenger/Tests/DataCollector/MessengerDataCollectorTest.php @@ -59,6 +59,7 @@ public function testHandle($returnedValue, $expected) public function getHandleTestData() { + $file = __FILE__; $messageDump = << "default" "envelopeItems" => null @@ -68,12 +69,17 @@ public function getHandleTestData() -message: "dummy message" } ] + "caller" => array:3 [ + "name" => "MessengerDataCollectorTest.php" + "file" => "$file" + "line" => %d + ] DUMP; yield 'no returned value' => array( null, << array:2 [ "type" => "NULL" @@ -86,7 +92,7 @@ public function getHandleTestData() yield 'scalar returned value' => array( 'returned value', << array:2 [ "type" => "string" @@ -99,7 +105,7 @@ public function getHandleTestData() yield 'array returned value' => array( array('returned value'), << array:2 [ "type" => "array" @@ -124,6 +130,7 @@ public function testHandleWithException() $collector->registerBus('default', $bus); try { + $line = __LINE__ + 1; $bus->dispatch($message); } catch (\Throwable $e) { // Ignore. @@ -134,8 +141,9 @@ public function testHandleWithException() $messages = $collector->getMessages(); $this->assertCount(1, $messages); + $file = __FILE__; $this->assertStringMatchesFormat(<< "default" "envelopeItems" => null "message" => array:2 [ @@ -144,6 +152,11 @@ public function testHandleWithException() -message: "dummy message" } ] + "caller" => array:3 [ + "name" => "MessengerDataCollectorTest.php" + "file" => "$file" + "line" => $line + ] "exception" => array:2 [ "type" => "RuntimeException" "value" => RuntimeException %A diff --git a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php index 8a2946ee42778..339dc9ae5264d 100644 --- a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php +++ b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php @@ -28,12 +28,18 @@ public function testItTracesResult() $bus->expects($this->once())->method('dispatch')->with($message)->willReturn($result = array('foo' => 'bar')); $traceableBus = new TraceableMessageBus($bus); + $line = __LINE__ + 1; $this->assertSame($result, $traceableBus->dispatch($message)); $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); $this->assertArraySubset(array( 'message' => $message, 'result' => $result, 'envelopeItems' => null, + 'caller' => array( + 'name' => 'TraceableMessageBusTest.php', + 'file' => __FILE__, + 'line' => $line, + ), ), $tracedMessages[0], true); } @@ -45,12 +51,18 @@ public function testItTracesResultWithEnvelope() $bus->expects($this->once())->method('dispatch')->with($envelope)->willReturn($result = array('foo' => 'bar')); $traceableBus = new TraceableMessageBus($bus); + $line = __LINE__ + 1; $this->assertSame($result, $traceableBus->dispatch($envelope)); $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); $this->assertArraySubset(array( 'message' => $message, 'result' => $result, 'envelopeItems' => array($envelopeItem), + 'caller' => array( + 'name' => 'TraceableMessageBusTest.php', + 'file' => __FILE__, + 'line' => $line, + ), ), $tracedMessages[0], true); } @@ -64,6 +76,7 @@ public function testItTracesExceptions() $traceableBus = new TraceableMessageBus($bus); try { + $line = __LINE__ + 1; $traceableBus->dispatch($message); } catch (\RuntimeException $e) { $this->assertSame($exception, $e); @@ -74,6 +87,11 @@ public function testItTracesExceptions() 'message' => $message, 'exception' => $exception, 'envelopeItems' => null, + 'caller' => array( + 'name' => 'TraceableMessageBusTest.php', + 'file' => __FILE__, + 'line' => $line, + ), ), $tracedMessages[0], true); } } diff --git a/src/Symfony/Component/Messenger/TraceableMessageBus.php b/src/Symfony/Component/Messenger/TraceableMessageBus.php index b60d220b15ce1..2204f4e41ae2b 100644 --- a/src/Symfony/Component/Messenger/TraceableMessageBus.php +++ b/src/Symfony/Component/Messenger/TraceableMessageBus.php @@ -29,6 +29,7 @@ public function __construct(MessageBusInterface $decoratedBus) */ public function dispatch($message) { + $caller = $this->getCaller(); $callTime = microtime(true); $messageToTrace = $message instanceof Envelope ? $message->getMessage() : $message; $envelopeItems = $message instanceof Envelope ? array_values($message->all()) : null; @@ -41,6 +42,7 @@ public function dispatch($message) 'message' => $messageToTrace, 'result' => $result, 'callTime' => $callTime, + 'caller' => $caller, ); return $result; @@ -50,6 +52,7 @@ public function dispatch($message) 'message' => $messageToTrace, 'exception' => $e, 'callTime' => $callTime, + 'caller' => $caller, ); throw $e; @@ -65,4 +68,40 @@ public function reset() { $this->dispatchedMessages = array(); } + + private function getCaller(): array + { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 8); + + $file = $trace[1]['file']; + $line = $trace[1]['line']; + + for ($i = 2; $i < 8; ++$i) { + if (isset($trace[$i]['class'], $trace[$i]['function']) + && 'dispatch' === $trace[$i]['function'] + && is_a($trace[$i]['class'], MessageBusInterface::class, true) + ) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + while (++$i < 8) { + if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos( + $trace[$i]['function'], + 'call_user_func' + )) { + $file = $trace[$i]['file']; + $line = $trace[$i]['line']; + + break; + } + } + break; + } + } + + $name = str_replace('\\', '/', $file); + $name = substr($name, strrpos($name, '/') + 1); + + return compact('name', 'file', 'line'); + } } From bbbcd46005557ef27b42876c6b6bddbf2d48a9ff Mon Sep 17 00:00:00 2001 From: Samuel ROZE Date: Wed, 30 May 2018 17:45:47 +0200 Subject: [PATCH 027/125] Add an alias to the property info type extractor --- .../Bundle/FrameworkBundle/Resources/config/property_info.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml index a893127276564..bcf2f33b10a3a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml @@ -13,7 +13,11 @@ + + + + From 06ea72e3b298eb8b8d63900d395c9acc1c06541d Mon Sep 17 00:00:00 2001 From: Samuel ROZE Date: Wed, 30 May 2018 17:38:36 +0200 Subject: [PATCH 028/125] [PropertyInfo] Auto-enable PropertyInfo component --- .../FrameworkBundle/DependencyInjection/Configuration.php | 3 ++- .../Tests/DependencyInjection/ConfigurationTest.php | 2 +- .../Tests/DependencyInjection/FrameworkExtensionTest.php | 6 ------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index e9a380dd78e63..59289c38b3029 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -21,6 +21,7 @@ use Symfony\Component\Form\Form; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\Store\SemaphoreStore; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Serializer\Serializer; @@ -833,7 +834,7 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode) ->children() ->arrayNode('property_info') ->info('Property info configuration') - ->canBeEnabled() + ->{!class_exists(FullStack::class) && interface_exists(PropertyInfoExtractorInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->end() ->end() ; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index edac04c4c4f7c..58f7e564c97f3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -219,7 +219,7 @@ protected static function getBundleDefaultConfig() 'throw_exception_on_invalid_index' => false, ), 'property_info' => array( - 'enabled' => false, + 'enabled' => !class_exists(FullStack::class), ), 'router' => array( 'enabled' => false, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 2ae8411c22909..85908734396ca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -1153,12 +1153,6 @@ public function testSerializerServiceIsNotRegisteredWhenDisabled() $this->assertFalse($container->hasDefinition('serializer')); } - public function testPropertyInfoDisabled() - { - $container = $this->createContainerFromFile('default_config'); - $this->assertFalse($container->has('property_info')); - } - public function testPropertyInfoEnabled() { $container = $this->createContainerFromFile('property_info'); From f8746ce8bd7d3707c291fb7bf175c5c880ce3cdb Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Wed, 9 May 2018 12:49:31 -0400 Subject: [PATCH 029/125] Add ability to deprecate options --- .../Component/OptionsResolver/CHANGELOG.md | 5 + .../OptionsResolver/OptionsResolver.php | 71 +++++++ .../Tests/OptionsResolverTest.php | 181 ++++++++++++++++++ 3 files changed, 257 insertions(+) diff --git a/src/Symfony/Component/OptionsResolver/CHANGELOG.md b/src/Symfony/Component/OptionsResolver/CHANGELOG.md index 6e9d49fb61d75..ec0084acd15b0 100644 --- a/src/Symfony/Component/OptionsResolver/CHANGELOG.md +++ b/src/Symfony/Component/OptionsResolver/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + + * added `setDeprecated` and `isDeprecated` methods + 3.4.0 ----- diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 68b4154b10da2..d42c567ab74e9 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\OptionsResolver; use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; @@ -75,6 +76,11 @@ class OptionsResolver implements Options */ private $calling = array(); + /** + * A list of deprecated options. + */ + private $deprecated = array(); + /** * Whether the instance is locked for reading. * @@ -348,6 +354,57 @@ public function getDefinedOptions() return array_keys($this->defined); } + /** + * Deprecates an option, allowed types or values. + * + * Instead of passing the message, you may also pass a closure with the + * following signature: + * + * function ($value) { + * // ... + * } + * + * The closure receives the value as argument and should return a string. + * Returns an empty string to ignore the option deprecation. + * + * The closure is invoked when {@link resolve()} is called. The parameter + * passed to the closure is the value of the option after validating it + * and before normalizing it. + * + * @param string|\Closure $deprecationMessage + */ + public function setDeprecated(string $option, $deprecationMessage = 'The option "%name%" is deprecated.'): self + { + if ($this->locked) { + throw new AccessException('Options cannot be deprecated from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf('The option "%s" does not exist, defined options are: "%s".', $option, implode('", "', array_keys($this->defined)))); + } + + if (!\is_string($deprecationMessage) && !$deprecationMessage instanceof \Closure) { + throw new InvalidArgumentException(sprintf('Invalid type for deprecation message argument, expected string or \Closure, but got "%s".', \gettype($deprecationMessage))); + } + + // ignore if empty string + if ('' === $deprecationMessage) { + return $this; + } + + $this->deprecated[$option] = $deprecationMessage; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + public function isDeprecated(string $option): bool + { + return isset($this->deprecated[$option]); + } + /** * Sets the normalizer for an option. * @@ -620,6 +677,7 @@ public function clear() $this->normalizers = array(); $this->allowedTypes = array(); $this->allowedValues = array(); + $this->deprecated = array(); return $this; } @@ -836,6 +894,19 @@ public function offsetGet($option) } } + // Check whether the option is deprecated + if (isset($this->deprecated[$option])) { + $deprecationMessage = $this->deprecated[$option]; + + if ($deprecationMessage instanceof \Closure && !\is_string($deprecationMessage = $deprecationMessage($value))) { + throw new InvalidOptionsException(sprintf('Invalid type for deprecation message, expected string but got "%s", returns an empty string to ignore.', \gettype($deprecationMessage))); + } + + if ('' !== $deprecationMessage) { + @trigger_error(strtr($deprecationMessage, array('%name%' => $option)), E_USER_DEPRECATED); + } + } + // Normalize the validated option if (isset($this->normalizers[$option])) { // If the closure is already being called, we have a cyclic diff --git a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php index 12dc77b3c6b1c..8f3ab6370dc4c 100644 --- a/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php +++ b/src/Symfony/Component/OptionsResolver/Tests/OptionsResolverTest.php @@ -450,6 +450,187 @@ public function testClearedOptionsAreNotDefined() $this->assertFalse($this->resolver->isDefined('foo')); } + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDeprecatedFromLazyOption() + { + $this->resolver + ->setDefault('bar', 'baz') + ->setDefault('foo', function (Options $options) { + $options->setDeprecated('bar'); + }) + ->resolve() + ; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetDeprecatedFailsIfUnknownOption() + { + $this->resolver->setDeprecated('foo'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid type for deprecation message argument, expected string or \Closure, but got "boolean". + */ + public function testSetDeprecatedFailsIfInvalidDeprecationMessageType() + { + $this->resolver + ->setDefined('foo') + ->setDeprecated('foo', true) + ; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidArgumentException + * @expectedExceptionMessage Invalid type for deprecation message, expected string but got "boolean", returns an empty string to ignore. + */ + public function testLazyDeprecationFailsIfInvalidDeprecationMessageType() + { + $this->resolver + ->setDefault('foo', true) + ->setDeprecated('foo', function ($value) { + return false; + }) + ; + $this->resolver->resolve(); + } + + public function testIsDeprecated() + { + $this->resolver + ->setDefined('foo') + ->setDeprecated('foo') + ; + $this->assertTrue($this->resolver->isDeprecated('foo')); + } + + public function testIsNotDeprecatedIfEmptyString() + { + $this->resolver + ->setDefined('foo') + ->setDeprecated('foo', '') + ; + $this->assertFalse($this->resolver->isDeprecated('foo')); + } + + /** + * @dataProvider provideDeprecationData + */ + public function testDeprecationMessages(\Closure $configureOptions, array $options, ?array $expectedError) + { + error_clear_last(); + set_error_handler(function () { return false; }); + $e = error_reporting(0); + + $configureOptions($this->resolver); + $this->resolver->resolve($options); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $this->assertSame($expectedError, $lastError); + } + + public function provideDeprecationData() + { + yield 'It deprecates an option with default message' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefined(array('foo', 'bar')) + ->setDeprecated('foo') + ; + }, + array('foo' => 'baz'), + array( + 'type' => E_USER_DEPRECATED, + 'message' => 'The option "foo" is deprecated.', + ), + ); + + yield 'It deprecates an option with custom message' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefined('foo') + ->setDefault('bar', function (Options $options) { + return $options['foo']; + }) + ->setDeprecated('foo', 'The option "foo" is deprecated, use "bar" option instead.') + ; + }, + array('foo' => 'baz'), + array( + 'type' => E_USER_DEPRECATED, + 'message' => 'The option "foo" is deprecated, use "bar" option instead.', + ), + ); + + yield 'It deprecates a missing option with default value' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefaults(array('foo' => null, 'bar' => null)) + ->setDeprecated('foo') + ; + }, + array('bar' => 'baz'), + array( + 'type' => E_USER_DEPRECATED, + 'message' => 'The option "foo" is deprecated.', + ), + ); + + yield 'It deprecates allowed type and value' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefault('foo', null) + ->setAllowedTypes('foo', array('null', 'string', \stdClass::class)) + ->setDeprecated('foo', function ($value) { + if ($value instanceof \stdClass) { + return sprintf('Passing an instance of "%s" to option "foo" is deprecated, pass its FQCN instead.', \stdClass::class); + } + + return ''; + }) + ; + }, + array('foo' => new \stdClass()), + array( + 'type' => E_USER_DEPRECATED, + 'message' => 'Passing an instance of "stdClass" to option "foo" is deprecated, pass its FQCN instead.', + ), + ); + + yield 'It ignores deprecation for missing option without default value' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefined(array('foo', 'bar')) + ->setDeprecated('foo') + ; + }, + array('bar' => 'baz'), + null, + ); + + yield 'It ignores deprecation if closure returns an empty string' => array( + function (OptionsResolver $resolver) { + $resolver + ->setDefault('foo', null) + ->setDeprecated('foo', function ($value) { + return ''; + }) + ; + }, + array('foo' => Bar::class), + null, + ); + } + /** * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException */ From e2f344fa322a0b3c3bb7d8cf67ab9ba7a615b750 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 1 Jun 2018 09:08:10 +0200 Subject: [PATCH 030/125] [FrameworkBundle] Deprecate auto-injection of the container in AbstractController instances --- src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Controller/ControllerResolver.php | 14 ++++++++++---- .../Tests/Controller/ControllerResolverTest.php | 8 ++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 139041aba81a8..fde20aaaba6e0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * Allowed configuring taggable cache pools via a new `framework.cache.pools.tags` option (bool|service-id) + * Deprecated auto-injection of the container in AbstractController instances, register them as service subscribers instead 4.1.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php index 27e714acc86e8..d8f2cd8e1af85 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php @@ -51,16 +51,22 @@ protected function createController($controller) */ protected function instantiateController($class) { - return $this->configureController(parent::instantiateController($class)); + return $this->configureController(parent::instantiateController($class), $class); } - private function configureController($controller) + private function configureController($controller, string $class) { if ($controller instanceof ContainerAwareInterface) { $controller->setContainer($this->container); } - if ($controller instanceof AbstractController && null !== $previousContainer = $controller->setContainer($this->container)) { - $controller->setContainer($previousContainer); + if ($controller instanceof AbstractController) { + if (null === $previousContainer = $controller->setContainer($this->container)) { + @trigger_error(sprintf('Auto-injection of the container for "%s" is deprecated since Symfony 4.2. Configure it as a service instead.', $class), E_USER_DEPRECATED); + // To be uncommented on Symfony 5: + //throw new \LogicException(sprintf('"%s" has no container set, did you forget to define it as a service subscriber?', $class)); + } else { + $controller->setContainer($previousContainer); + } } return $controller; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php index a4529a657c7f2..d5d35cf4e2855 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -92,6 +92,10 @@ class_exists(AbstractControllerTest::class); $this->assertSame($container, $controller->getContainer()); } + /** + * @group legacy + * @expectedDeprecation Auto-injection of the container for "Symfony\Bundle\FrameworkBundle\Tests\Controller\TestAbstractController" is deprecated since Symfony 4.2. Configure it as a service instead. + */ public function testAbstractControllerGetsContainerWhenNotSet() { class_exists(AbstractControllerTest::class); @@ -110,6 +114,10 @@ class_exists(AbstractControllerTest::class); $this->assertSame($container, $controller->setContainer($container)); } + /** + * @group legacy + * @expectedDeprecation Auto-injection of the container for "Symfony\Bundle\FrameworkBundle\Tests\Controller\DummyController" is deprecated since Symfony 4.2. Configure it as a service instead. + */ public function testAbstractControllerServiceWithFcqnIdGetsContainerWhenNotSet() { class_exists(AbstractControllerTest::class); From a6b6206a62f0846005111aafb2f3b843d89e465c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 3 Jun 2018 22:42:09 +0200 Subject: [PATCH 031/125] [DI] Don't generate factories for errored services --- .../DependencyInjection/Dumper/PhpDumper.php | 29 ++++++++++++++++++- .../Tests/Fixtures/php/services9_as_files.txt | 12 +------- .../Tests/Fixtures/php/services9_compiled.php | 17 ++++------- .../php/services_errored_definition.php | 17 ++++------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index ce96fdd607bef..7f591f7f49975 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -70,6 +70,7 @@ class PhpDumper extends Dumper private $inlinedRequires = array(); private $circularReferences = array(); private $singleUsePrivateIds = array(); + private $addThrow = false; /** * @var ProxyDumper @@ -125,6 +126,7 @@ public function dump(array $options = array()) 'build_time' => time(), ), $options); + $this->addThrow = false; $this->namespace = $options['namespace']; $this->asFiles = $options['as_files']; $this->hotPathTag = $options['hot_path_tag']; @@ -556,10 +558,13 @@ private function addServiceInstance(string $id, Definition $definition, string $ private function isTrivialInstance(Definition $definition): bool { + if ($definition->getErrors()) { + return true; + } if ($definition->isSynthetic() || $definition->getFile() || $definition->getMethodCalls() || $definition->getProperties() || $definition->getConfigurator()) { return false; } - if ($definition->isDeprecated() || $definition->isLazy() || $definition->getFactory() || 3 < count($definition->getArguments()) || $definition->getErrors()) { + if ($definition->isDeprecated() || $definition->isLazy() || $definition->getFactory() || 3 < count($definition->getArguments())) { return false; } @@ -1318,6 +1323,18 @@ private function exportParameters(array $parameters, string $path = '', int $ind private function endClass(): string { + if ($this->addThrow) { + return <<<'EOF' + + protected function throw($message) + { + throw new RuntimeException($message); + } +} + +EOF; + } + return <<<'EOF' } @@ -1525,6 +1542,11 @@ private function dumpValue($value, bool $interpolate = true): string list($this->definitionVariables, $this->referenceVariables, $this->variableCount) = $scope; } } elseif ($value instanceof Definition) { + if ($e = $value->getErrors()) { + $this->addThrow = true; + + return sprintf('$this->throw(%s)', $this->export(reset($e))); + } if (null !== $this->definitionVariables && $this->definitionVariables->contains($value)) { return $this->dumpValue($this->definitionVariables[$value], $interpolate); } @@ -1670,6 +1692,11 @@ private function getServiceCall(string $id, Reference $reference = null): string return $code; } } elseif ($this->isTrivialInstance($definition)) { + if ($e = $definition->getErrors()) { + $this->addThrow = true; + + return sprintf('$this->throw(%s)', $this->export(reset($e))); + } $code = substr($this->addNewInstance($definition, '', '', $id), 8, -2); if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) { $code = sprintf('$this->%s[\'%s\'] = %s', $definition->isPublic() ? 'services' : 'privates', $id, $code); 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 index 3a092f7306d70..90b90f64b3bb8 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt @@ -127,16 +127,6 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; return $this->services['deprecated_service'] = new \stdClass(); - [Container%s/getErroredDefinitionService.php] => services['runtime_error'] = new \stdClass($this->load('getErroredDefinitionService.php')); +return $this->services['runtime_error'] = new \stdClass($this->throw('Service "errored_definition" is broken.')); [Container%s/getServiceFromStaticMethodService.php] => services['runtime_error'] = new \stdClass($this->getErroredDefinitionService()); + return $this->services['runtime_error'] = new \stdClass($this->throw('Service "errored_definition" is broken.')); } /** @@ -407,16 +407,6 @@ protected function getTaggedIteratorService() }, 2)); } - /** - * Gets the private 'errored_definition' shared service. - * - * @return \stdClass - */ - protected function getErroredDefinitionService() - { - throw new RuntimeException('Service "errored_definition" is broken.'); - } - /** * Gets the private 'factory_simple' shared service. * @@ -500,4 +490,9 @@ protected function getDefaultParameters() 'foo' => 'bar', ); } + + protected function throw($message) + { + throw new RuntimeException($message); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php index 9ff22bf4d43d8..ddc5dc59da4f3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_errored_definition.php @@ -381,7 +381,7 @@ protected function getNewFactoryServiceService() */ protected function getRuntimeErrorService() { - return $this->services['runtime_error'] = new \stdClass($this->getErroredDefinitionService()); + return $this->services['runtime_error'] = new \stdClass($this->throw('Service "errored_definition" is broken.')); } /** @@ -407,16 +407,6 @@ protected function getTaggedIteratorService() }, 2)); } - /** - * Gets the private 'errored_definition' shared service. - * - * @return \stdClass - */ - protected function getErroredDefinitionService() - { - throw new RuntimeException('Service "errored_definition" is broken.'); - } - /** * Gets the private 'factory_simple' shared service. * @@ -501,4 +491,9 @@ protected function getDefaultParameters() 'foo_bar' => 'foo_bar', ); } + + protected function throw($message) + { + throw new RuntimeException($message); + } } From 238e793431e2d71ff89dff0fc41e974fbe50d630 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 27 Apr 2018 10:57:54 -0400 Subject: [PATCH 032/125] [DependencyInjection] add ServiceSubscriberTrait --- .../DependencyInjection/CHANGELOG.md | 5 ++ .../ServiceSubscriberTrait.php | 61 ++++++++++++++++++ .../RegisterServiceSubscribersPassTest.php | 30 +++++++++ .../Tests/Fixtures/TestDefinition1.php | 9 +++ .../Tests/Fixtures/TestDefinition2.php | 9 +++ .../Tests/Fixtures/TestDefinition3.php | 9 +++ .../Fixtures/TestServiceSubscriberChild.php | 28 +++++++++ .../Fixtures/TestServiceSubscriberParent.php | 16 +++++ .../Fixtures/TestServiceSubscriberTrait.php | 11 ++++ .../Tests/ServiceSubscriberTraitTest.php | 63 +++++++++++++++++++ 10 files changed, 241 insertions(+) create mode 100644 src/Symfony/Component/DependencyInjection/ServiceSubscriberTrait.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition1.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition2.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition3.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberChild.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberParent.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberTrait.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/ServiceSubscriberTraitTest.php diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index fb9d0ef90c82a..4393b8c44c40d 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + + * added `ServiceSubscriberTrait` + 4.1.0 ----- diff --git a/src/Symfony/Component/DependencyInjection/ServiceSubscriberTrait.php b/src/Symfony/Component/DependencyInjection/ServiceSubscriberTrait.php new file mode 100644 index 0000000000000..0f55c742fc9a8 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ServiceSubscriberTrait.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\DependencyInjection; + +use Psr\Container\ContainerInterface; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services from + * private method return types. Service ids are available as "ClassName::methodName". + * + * @author Kevin Bond + */ +trait ServiceSubscriberTrait +{ + /** @var ContainerInterface */ + private $container; + + public static function getSubscribedServices(): array + { + static $services; + + if (null !== $services) { + return $services; + } + + $services = \is_callable(array('parent', __FUNCTION__)) ? parent::getSubscribedServices() : array(); + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + continue; + } + + if (self::class === $method->getDeclaringClass()->name && ($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) { + $services[self::class.'::'.$method->name] = '?'.$returnType->getName(); + } + } + + return $services; + } + + /** + * @required + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + + if (\is_callable(array('parent', __FUNCTION__))) { + return parent::setContainer($container); + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index f7314948aeef6..3a0f2b93353cf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -21,7 +21,12 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition1; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition2; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition3; use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberChild; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberParent; use Symfony\Component\DependencyInjection\TypedReference; require_once __DIR__.'/../Fixtures/includes/classes.php'; @@ -136,4 +141,29 @@ public function testExtraServiceSubscriber() $container->register(TestServiceSubscriber::class, TestServiceSubscriber::class); $container->compile(); } + + public function testServiceSubscriberTrait() + { + $container = new ContainerBuilder(); + + $container->register('foo', TestServiceSubscriberChild::class) + ->addMethodCall('setContainer', array(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->getMethodCalls()[0][1][0]); + + $expected = array( + TestServiceSubscriberChild::class.'::invalidDefinition' => new ServiceClosureArgument(new TypedReference('Symfony\Component\DependencyInjection\Tests\Fixtures\InvalidDefinition', 'Symfony\Component\DependencyInjection\Tests\Fixtures\InvalidDefinition', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + TestServiceSubscriberChild::class.'::testDefinition2' => new ServiceClosureArgument(new TypedReference(TestDefinition2::class, TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + TestServiceSubscriberChild::class.'::testDefinition3' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + TestServiceSubscriberParent::class.'::testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + ); + + $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition1.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition1.php new file mode 100644 index 0000000000000..0b12c21e09125 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition1.php @@ -0,0 +1,9 @@ +container->get(__METHOD__); + } + + private function invalidDefinition(): InvalidDefinition + { + return $this->container->get(__METHOD__); + } + + private function privateFunction1(): string + { + } + + private function privateFunction2(): string + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberParent.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberParent.php new file mode 100644 index 0000000000000..d2abb7a859cdf --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberParent.php @@ -0,0 +1,16 @@ +container->get(__METHOD__); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberTrait.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberTrait.php new file mode 100644 index 0000000000000..ea98a0f2f13e1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberTrait.php @@ -0,0 +1,11 @@ +container->get(__CLASS__.'::'.__FUNCTION__); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/ServiceSubscriberTraitTest.php b/src/Symfony/Component/DependencyInjection/Tests/ServiceSubscriberTraitTest.php new file mode 100644 index 0000000000000..e230b2069ef9a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/ServiceSubscriberTraitTest.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\DependencyInjection\Tests; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; +use Symfony\Component\DependencyInjection\ServiceSubscriberTrait; + +class ServiceSubscriberTraitTest extends TestCase +{ + public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices() + { + $expected = array(TestService::class.'::aService' => '?Symfony\Component\DependencyInjection\Tests\Service2'); + + $this->assertEquals($expected, ChildTestService::getSubscribedServices()); + } + + public function testSetContainerIsCalledOnParent() + { + $container = new Container(); + + $this->assertSame($container, (new TestService())->setContainer($container)); + } +} + +class ParentTestService +{ + public function aParentService(): Service1 + { + } + + public function setContainer(ContainerInterface $container) + { + return $container; + } +} + +class TestService extends ParentTestService implements ServiceSubscriberInterface +{ + use ServiceSubscriberTrait; + + public function aService(): Service2 + { + } +} + +class ChildTestService extends TestService +{ + public function aChildService(): Service3 + { + } +} From cf375e528635dcd38b0eb68e3eb33235bc8ed969 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 1 Jun 2018 18:19:33 +0200 Subject: [PATCH 033/125] [DI] Improve performance of removing/inlining passes --- .../Compiler/CachePoolClearerPassTest.php | 7 +- .../Compiler/AbstractRecursivePass.php | 38 +++++++ .../Compiler/AnalyzeServiceReferencesPass.php | 86 ++++++---------- .../Compiler/InlineServiceDefinitionsPass.php | 98 ++++++++++++++++--- .../Compiler/PassConfig.php | 8 +- .../Compiler/RemoveUnusedDefinitionsPass.php | 87 +++++++++------- .../Compiler/RepeatablePassInterface.php | 2 + .../Compiler/RepeatedPass.php | 4 + .../DependencyInjection/Dumper/PhpDumper.php | 7 +- .../AnalyzeServiceReferencesPassTest.php | 3 +- .../InlineServiceDefinitionsPassTest.php | 16 +-- .../RemoveUnusedDefinitionsPassTest.php | 5 +- .../Fixtures/xml/services_instanceof.xml | 2 +- .../Fixtures/yaml/services_instanceof.yml | 4 +- 14 files changed, 239 insertions(+), 128 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php index 243061712a8c4..3de867203f1e1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php @@ -55,7 +55,12 @@ public function testPoolRefsAreWeak() $container->setAlias('clearer_alias', 'clearer'); $pass = new RemoveUnusedDefinitionsPass(); - $pass->setRepeatedPass(new RepeatedPass(array($pass))); + foreach ($container->getCompiler()->getPassConfig()->getRemovingPasses() as $removingPass) { + if ($removingPass instanceof RepeatedPass) { + $pass->setRepeatedPass(new RepeatedPass(array($pass))); + break; + } + } foreach (array(new CachePoolPass(), $pass, new CachePoolClearerPass()) as $pass) { $pass->process($container); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index 901dc06ffaee5..ef50f8234e628 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -15,7 +15,9 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\ExpressionLanguage; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\Expression; /** * @author Nicolas Grekas @@ -28,6 +30,9 @@ abstract class AbstractRecursivePass implements CompilerPassInterface protected $container; protected $currentId; + private $processExpressions = false; + private $expressionLanguage; + /** * {@inheritdoc} */ @@ -42,11 +47,17 @@ public function process(ContainerBuilder $container) } } + protected function enableExpressionProcessing() + { + $this->processExpressions = true; + } + /** * Processes a value found in a definition tree. * * @param mixed $value * @param bool $isRoot + * @param bool $inExpression * * @return mixed The processed value */ @@ -63,6 +74,8 @@ protected function processValue($value, $isRoot = false) } } elseif ($value instanceof ArgumentInterface) { $value->setValues($this->processValue($value->getValues())); + } elseif ($value instanceof Expression && $this->processExpressions) { + $this->getExpressionLanguage()->compile((string) $value, array('this' => 'container')); } elseif ($value instanceof Definition) { $value->setArguments($this->processValue($value->getArguments())); $value->setProperties($this->processValue($value->getProperties())); @@ -169,4 +182,29 @@ protected function getReflectionMethod(Definition $definition, $method) return $r; } + + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists(ExpressionLanguage::class)) { + throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + + $providers = $this->container->getExpressionLanguageProviders(); + $this->expressionLanguage = new ExpressionLanguage(null, $providers, function ($arg) { + if ('""' === substr_replace($arg, '', 1, -1)) { + $id = stripcslashes(substr($arg, 1, -1)); + $arg = $this->processValue(new Reference($id), false, true); + if (!$arg instanceof Reference) { + throw new RuntimeException(sprintf('"%s::processValue()" must return a Reference when processing an expression, %s returned for service("%s").', get_class($this), is_object($arg) ? get_class($arg) : gettype($arg))); + } + $arg = sprintf('"%s"', $arg); + } + + return sprintf('$this->get(%s)', $arg); + }); + } + + return $this->expressionLanguage; + } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php index a67d9b044f4f2..3740c740b4442 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php @@ -14,11 +14,8 @@ use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; -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 @@ -28,6 +25,7 @@ * retrieve the graph in other passes from the compiler. * * @author Johannes M. Schmitt + * @author Nicolas Grekas */ class AnalyzeServiceReferencesPass extends AbstractRecursivePass implements RepeatablePassInterface { @@ -35,7 +33,8 @@ class AnalyzeServiceReferencesPass extends AbstractRecursivePass implements Repe private $currentDefinition; private $onlyConstructorArguments; private $lazy; - private $expressionLanguage; + private $definitions; + private $aliases; /** * @param bool $onlyConstructorArguments Sets this Service Reference pass to ignore method calls @@ -43,6 +42,7 @@ class AnalyzeServiceReferencesPass extends AbstractRecursivePass implements Repe public function __construct(bool $onlyConstructorArguments = false) { $this->onlyConstructorArguments = $onlyConstructorArguments; + $this->enableExpressionProcessing(); } /** @@ -50,7 +50,7 @@ public function __construct(bool $onlyConstructorArguments = false) */ public function setRepeatedPass(RepeatedPass $repeatedPass) { - // no-op for BC + @trigger_error(sprintf('The "%s" method is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); } /** @@ -62,16 +62,22 @@ public function process(ContainerBuilder $container) $this->graph = $container->getCompiler()->getServiceReferenceGraph(); $this->graph->clear(); $this->lazy = false; + $this->definitions = $container->getDefinitions(); + $this->aliases = $container->getAliases(); - foreach ($container->getAliases() as $id => $alias) { + foreach ($this->aliases as $id => $alias) { $targetId = $this->getDefinitionId((string) $alias); - $this->graph->connect($id, $alias, $targetId, $this->getDefinition($targetId), null); + $this->graph->connect($id, $alias, $targetId, null !== $targetId ? $this->container->getDefinition($targetId) : null, null); } - parent::process($container); + try { + parent::process($container); + } finally { + $this->aliases = $this->definitions = array(); + } } - protected function processValue($value, $isRoot = false) + protected function processValue($value, $isRoot = false, bool $inExpression = false) { $lazy = $this->lazy; @@ -82,14 +88,9 @@ protected function processValue($value, $isRoot = false) return $value; } - if ($value instanceof Expression) { - $this->getExpressionLanguage()->compile((string) $value, array('this' => 'container')); - - return $value; - } if ($value instanceof Reference) { $targetId = $this->getDefinitionId((string) $value); - $targetDefinition = $this->getDefinition($targetId); + $targetDefinition = null !== $targetId ? $this->container->getDefinition($targetId) : null; $this->graph->connect( $this->currentId, @@ -101,6 +102,18 @@ protected function processValue($value, $isRoot = false) ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior() ); + if ($inExpression) { + $this->graph->connect( + '.internal.reference_in_expression', + null, + $targetId, + $targetDefinition, + $value, + $this->lazy || ($targetDefinition && $targetDefinition->isLazy()), + true + ); + } + return $value; } if (!$value instanceof Definition) { @@ -127,49 +140,12 @@ protected function processValue($value, $isRoot = false) return $value; } - private function getDefinition(?string $id): ?Definition - { - return null === $id ? null : $this->container->getDefinition($id); - } - private function getDefinitionId(string $id): ?string { - while ($this->container->hasAlias($id)) { - $id = (string) $this->container->getAlias($id); - } - - if (!$this->container->hasDefinition($id)) { - return null; - } - - return $id; - } - - private function getExpressionLanguage() - { - if (null === $this->expressionLanguage) { - if (!class_exists(ExpressionLanguage::class)) { - throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); - } - - $providers = $this->container->getExpressionLanguageProviders(); - $this->expressionLanguage = new ExpressionLanguage(null, $providers, function ($arg) { - if ('""' === substr_replace($arg, '', 1, -1)) { - $id = stripcslashes(substr($arg, 1, -1)); - $id = $this->getDefinitionId($id); - - $this->graph->connect( - $this->currentId, - $this->currentDefinition, - $id, - $this->getDefinition($id) - ); - } - - return sprintf('$this->get(%s)', $arg); - }); + while (isset($this->aliases[$id])) { + $id = (string) $this->aliases[$id]; } - return $this->expressionLanguage; + return isset($this->definitions[$id]) ? $id : null; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index 9aee66c8e0d5b..d255293d02cf9 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -12,6 +12,7 @@ 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\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Reference; @@ -23,14 +24,78 @@ */ class InlineServiceDefinitionsPass extends AbstractRecursivePass implements RepeatablePassInterface { + private $analyzingPass; + private $repeatedPass; private $cloningIds = array(); + private $connectedIds = array(); + private $notInlinedIds = array(); + private $inlinedIds = array(); + private $graph; + + public function __construct(AnalyzeServiceReferencesPass $analyzingPass = null) + { + $this->analyzingPass = $analyzingPass; + } /** * {@inheritdoc} */ public function setRepeatedPass(RepeatedPass $repeatedPass) { - // no-op for BC + @trigger_error(sprintf('The "%s" method is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + $this->repeatedPass = $repeatedPass; + } + + public function process(ContainerBuilder $container) + { + $this->container = $container; + if ($this->analyzingPass) { + $analyzedContainer = new ContainerBuilder(); + $analyzedContainer->setAliases($container->getAliases()); + $analyzedContainer->setDefinitions($container->getDefinitions()); + } else { + $analyzedContainer = $container; + } + try { + $this->connectedIds = $this->notInlinedIds = $container->getDefinitions(); + do { + if ($this->analyzingPass) { + $analyzedContainer->setDefinitions(array_intersect_key($analyzedContainer->getDefinitions(), $this->connectedIds)); + $this->analyzingPass->process($analyzedContainer); + } + $this->graph = $analyzedContainer->getCompiler()->getServiceReferenceGraph(); + $notInlinedIds = $this->notInlinedIds; + $this->connectedIds = $this->notInlinedIds = $this->inlinedIds = array(); + + foreach ($analyzedContainer->getDefinitions() as $id => $definition) { + if (!$this->graph->hasNode($id)) { + continue; + } + foreach ($this->graph->getNode($id)->getOutEdges() as $edge) { + if (isset($notInlinedIds[$edge->getSourceNode()->getId()])) { + $this->currentId = $id; + $this->processValue($definition, true); + break; + } + } + } + + foreach ($this->inlinedIds as $id => $isPublic) { + if (!$isPublic) { + $container->removeDefinition($id); + $analyzedContainer->removeDefinition($id); + } + } + } while ($this->inlinedIds && $this->analyzingPass); + + if ($this->inlinedIds && $this->repeatedPass) { + $this->repeatedPass->setRepeat(); + } + } finally { + $this->container = null; + $this->connectedIds = $this->notInlinedIds = $this->inlinedIds = array(); + $this->graph = null; + } } /** @@ -50,17 +115,21 @@ protected function processValue($value, $isRoot = false) $value = clone $value; } - if (!$value instanceof Reference || !$this->container->hasDefinition($id = (string) $value)) { + if (!$value instanceof Reference) { return parent::processValue($value, $isRoot); + } elseif (!$this->container->hasDefinition($id = (string) $value)) { + return $value; } $definition = $this->container->getDefinition($id); - if (!$this->isInlineableDefinition($id, $definition, $this->container->getCompiler()->getServiceReferenceGraph())) { + if (!$this->isInlineableDefinition($id, $definition)) { return $value; } $this->container->log($this, sprintf('Inlined service "%s" to "%s".', $id, $this->currentId)); + $this->inlinedIds[$id] = $definition->isPublic(); + $this->notInlinedIds[$this->currentId] = true; if ($definition->isShared()) { return $definition; @@ -86,7 +155,7 @@ protected function processValue($value, $isRoot = false) * * @return bool If the definition is inlineable */ - private function isInlineableDefinition($id, Definition $definition, ServiceReferenceGraph $graph) + private function isInlineableDefinition($id, Definition $definition) { if ($definition->getErrors() || $definition->isDeprecated() || $definition->isLazy() || $definition->isSynthetic()) { return false; @@ -100,30 +169,37 @@ private function isInlineableDefinition($id, Definition $definition, ServiceRefe return false; } - if (!$graph->hasNode($id)) { + if (!$this->graph->hasNode($id)) { return true; } if ($this->currentId == $id) { return false; } + $this->connectedIds[$id] = true; - $ids = array(); - foreach ($graph->getNode($id)->getInEdges() as $edge) { + $srcIds = array(); + $srcCount = 0; + foreach ($this->graph->getNode($id)->getInEdges() as $edge) { + $srcId = $edge->getSourceNode()->getId(); + $this->connectedIds[$srcId] = true; if ($edge->isWeak()) { return false; } - $ids[] = $edge->getSourceNode()->getId(); + $srcIds[$srcId] = true; + ++$srcCount; } - if (count(array_unique($ids)) > 1) { + if (1 !== \count($srcIds)) { + $this->notInlinedIds[$id] = true; + return false; } - if (count($ids) > 1 && is_array($factory = $definition->getFactory()) && ($factory[0] instanceof Reference || $factory[0] instanceof Definition)) { + if ($srcCount > 1 && is_array($factory = $definition->getFactory()) && ($factory[0] instanceof Reference || $factory[0] instanceof Definition)) { return false; } - return !$ids || $this->container->getDefinition($ids[0])->isShared(); + return $this->container->getDefinition($srcId)->isShared(); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 170a0edc8aabb..0d8ba9c047df8 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -81,12 +81,8 @@ public function __construct() new RemovePrivateAliasesPass(), new ReplaceAliasByActualDefinitionPass(), new RemoveAbstractDefinitionsPass(), - new RepeatedPass(array( - new AnalyzeServiceReferencesPass(), - new InlineServiceDefinitionsPass(), - new AnalyzeServiceReferencesPass(), - new RemoveUnusedDefinitionsPass(), - )), + new RemoveUnusedDefinitionsPass(), + new InlineServiceDefinitionsPass(new AnalyzeServiceReferencesPass()), new DefinitionErrorExceptionPass(), new CheckExceptionOnInvalidReferenceBehaviorPass(), new ResolveHotPathPass(), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php index ec2eed27edbc8..1bebd7ad5c1c4 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php @@ -12,22 +12,24 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; /** * Removes unused service definitions from the container. * * @author Johannes M. Schmitt + * @author Nicolas Grekas */ -class RemoveUnusedDefinitionsPass implements RepeatablePassInterface +class RemoveUnusedDefinitionsPass extends AbstractRecursivePass implements RepeatablePassInterface { - private $repeatedPass; + private $connectedIds = array(); /** * {@inheritdoc} */ public function setRepeatedPass(RepeatedPass $repeatedPass) { - $this->repeatedPass = $repeatedPass; + @trigger_error(sprintf('The "%s" method is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); } /** @@ -35,51 +37,62 @@ public function setRepeatedPass(RepeatedPass $repeatedPass) */ public function process(ContainerBuilder $container) { - $graph = $container->getCompiler()->getServiceReferenceGraph(); + try { + $this->enableExpressionProcessing(); + $this->container = $container; + $connectedIds = array(); + $aliases = $container->getAliases(); - $hasChanged = false; - foreach ($container->getDefinitions() as $id => $definition) { - if ($definition->isPublic()) { - continue; + foreach ($aliases as $id => $alias) { + if ($alias->isPublic()) { + $this->connectedIds[] = (string) $aliases[$id]; + } } - if ($graph->hasNode($id)) { - $edges = $graph->getNode($id)->getInEdges(); - $referencingAliases = array(); - $sourceIds = array(); - foreach ($edges as $edge) { - if ($edge->isWeak()) { - continue; - } - $node = $edge->getSourceNode(); - $sourceIds[] = $node->getId(); + foreach ($container->getDefinitions() as $id => $definition) { + if ($definition->isPublic()) { + $connectedIds[$id] = true; + $this->processValue($definition); + } + } - if ($node->isAlias()) { - $referencingAliases[] = $node->getValue(); + while ($this->connectedIds) { + $ids = $this->connectedIds; + $this->connectedIds = array(); + foreach ($ids as $id) { + if (!isset($connectedIds[$id]) && $container->has($id)) { + $connectedIds[$id] = true; + $this->processValue($container->getDefinition($id)); } } - $isReferenced = (count(array_unique($sourceIds)) - count($referencingAliases)) > 0; - } else { - $referencingAliases = array(); - $isReferenced = false; } - if (1 === count($referencingAliases) && false === $isReferenced) { - $container->setDefinition((string) reset($referencingAliases), $definition); - $definition->setPublic(!$definition->isPrivate()); - $definition->setPrivate(reset($referencingAliases)->isPrivate()); - $container->removeDefinition($id); - $container->log($this, sprintf('Removed service "%s"; reason: replaces alias %s.', $id, reset($referencingAliases))); - } elseif (0 === count($referencingAliases) && false === $isReferenced) { - $container->removeDefinition($id); - $container->resolveEnvPlaceholders(serialize($definition)); - $container->log($this, sprintf('Removed service "%s"; reason: unused.', $id)); - $hasChanged = true; + foreach ($container->getDefinitions() as $id => $definition) { + if (!isset($connectedIds[$id])) { + $container->removeDefinition($id); + $container->resolveEnvPlaceholders(serialize($definition)); + $container->log($this, sprintf('Removed service "%s"; reason: unused.', $id)); + } } + } finally { + $this->container = null; + $this->connectedIds = array(); } + } - if ($hasChanged) { - $this->repeatedPass->setRepeat(); + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Reference) { + return parent::processValue($value); } + + if (ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE !== $value->getInvalidBehavior()) { + $this->connectedIds[] = (string) $value; + } + + return $value; } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RepeatablePassInterface.php b/src/Symfony/Component/DependencyInjection/Compiler/RepeatablePassInterface.php index 2b88bfb917a0f..11a5b0d54ffa9 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RepeatablePassInterface.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RepeatablePassInterface.php @@ -16,6 +16,8 @@ * RepeatedPass. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 4.2. */ interface RepeatablePassInterface extends CompilerPassInterface { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RepeatedPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RepeatedPass.php index 3da1a0d5be8e3..b8add1b83d263 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RepeatedPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RepeatedPass.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Compiler; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', RepeatedPass::class), E_USER_DEPRECATED); + use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; @@ -18,6 +20,8 @@ * A pass that might be run repeatedly. * * @author Johannes M. Schmitt + * + * @deprecated since Symfony 4.2. */ class RepeatedPass implements CompilerPassInterface { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index ce96fdd607bef..ed3ea9c32ec8e 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -1809,12 +1809,15 @@ private function isHotPath(Definition $definition) private function isSingleUsePrivateNode(ServiceReferenceGraphNode $node): bool { - if ($node->getValue()->isPublic()) { + if (!$node->getValue() || $node->getValue()->isPublic()) { return false; } $ids = array(); foreach ($node->getInEdges() as $edge) { - if ($edge->isLazy() || !$edge->getSourceNode()->getValue()->isShared()) { + if (!$value = $edge->getSourceNode()->getValue()) { + continue; + } + if ($edge->isLazy() || !$value->isShared()) { return false; } $ids[$edge->getSourceNode()->getId()] = true; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php index 7a06e10d9ffad..2f0b56ae7c924 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AnalyzeServiceReferencesPassTest.php @@ -15,7 +15,6 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass; -use Symfony\Component\DependencyInjection\Compiler\RepeatedPass; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -207,7 +206,7 @@ public function testProcessDetectsFactoryReferences() protected function process(ContainerBuilder $container) { - $pass = new RepeatedPass(array(new AnalyzeServiceReferencesPass())); + $pass = new AnalyzeServiceReferencesPass(); $pass->process($container); return $container->getCompiler()->getServiceReferenceGraph(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php index 78556b2ed21cd..91e7566f1db04 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/InlineServiceDefinitionsPassTest.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\TestCase; 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; @@ -26,7 +25,7 @@ class InlineServiceDefinitionsPassTest extends TestCase public function testProcess() { $container = new ContainerBuilder(); - $container + $inlineable = $container ->register('inlinable.service') ->setPublic(false) ; @@ -40,7 +39,8 @@ public function testProcess() $arguments = $container->getDefinition('service')->getArguments(); $this->assertInstanceOf('Symfony\Component\DependencyInjection\Definition', $arguments[0]); - $this->assertSame($container->getDefinition('inlinable.service'), $arguments[0]); + $this->assertSame($inlineable, $arguments[0]); + $this->assertFalse($container->has('inlinable.service')); } public function testProcessDoesNotInlinesWhenAliasedServiceIsShared() @@ -70,7 +70,7 @@ public function testProcessDoesInlineNonSharedService() ->register('foo') ->setShared(false) ; - $container + $bar = $container ->register('bar') ->setPublic(false) ->setShared(false) @@ -88,8 +88,9 @@ public function testProcessDoesInlineNonSharedService() $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]); + $this->assertEquals($bar, $arguments[2]); + $this->assertNotSame($bar, $arguments[2]); + $this->assertFalse($container->has('bar')); } public function testProcessDoesNotInlineMixedServicesLoop() @@ -327,7 +328,6 @@ public function testProcessDoesNotSetLazyArgumentValuesAfterInlining() protected function process(ContainerBuilder $container) { - $repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new InlineServiceDefinitionsPass())); - $repeatedPass->process($container); + (new InlineServiceDefinitionsPass(new AnalyzeServiceReferencesPass()))->process($container); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php index c6f4e5e79d64d..c90a8d6728fe2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RemoveUnusedDefinitionsPassTest.php @@ -12,8 +12,6 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler; use PHPUnit\Framework\TestCase; -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; @@ -131,7 +129,6 @@ public function testProcessConsiderEnvVariablesAsUsedEvenInPrivateServices() protected function process(ContainerBuilder $container) { - $repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new RemoveUnusedDefinitionsPass())); - $repeatedPass->process($container); + (new RemoveUnusedDefinitionsPass())->process($container); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml index 839776a3fed97..8e26c56b75bdf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml @@ -6,7 +6,7 @@ - + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml index a58cc079e455f..dd93ab8de4664 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml @@ -7,5 +7,7 @@ services: - { name: foo } - { name: bar } - Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: ~ + Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: + public: true + Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: '@Symfony\Component\DependencyInjection\Tests\Fixtures\Bar' From f03b8bba9d8b268f3455b6bacc73f53186dd12bd Mon Sep 17 00:00:00 2001 From: Thomas Perez Date: Wed, 30 May 2018 12:23:13 +0200 Subject: [PATCH 034/125] CacheWarmerAggregate handle deprecations logs --- .../Resources/config/services.xml | 2 + .../CacheWarmer/CacheWarmerAggregate.php | 67 ++++++++++++++++--- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 2c0072d85d9a1..ec6128553ff73 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -32,6 +32,8 @@ + %kernel.debug% + %kernel.cache_dir%/%kernel.container_class%Deprecations.log diff --git a/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php b/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php index 8a57732bf3848..4d63804be7d67 100644 --- a/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php +++ b/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerAggregate.php @@ -21,12 +21,16 @@ class CacheWarmerAggregate implements CacheWarmerInterface { private $warmers; + private $debug; + private $deprecationLogsFilepath; private $optionalsEnabled = false; private $onlyOptionalsEnabled = false; - public function __construct(iterable $warmers = array()) + public function __construct(iterable $warmers = array(), bool $debug = false, string $deprecationLogsFilepath = null) { $this->warmers = $warmers; + $this->debug = $debug; + $this->deprecationLogsFilepath = $deprecationLogsFilepath; } public function enableOptionalWarmers() @@ -46,15 +50,62 @@ public function enableOnlyOptionalWarmers() */ public function warmUp($cacheDir) { - foreach ($this->warmers as $warmer) { - if (!$this->optionalsEnabled && $warmer->isOptional()) { - continue; - } - if ($this->onlyOptionalsEnabled && !$warmer->isOptional()) { - continue; + if ($this->debug) { + $collectedLogs = array(); + $previousHandler = defined('PHPUNIT_COMPOSER_INSTALL'); + $previousHandler = $previousHandler ?: set_error_handler(function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { + if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { + return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; + } + + if (isset($collectedLogs[$message])) { + ++$collectedLogs[$message]['count']; + + return; + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + // Clean the trace by removing first frames added by the error handler itself. + for ($i = 0; isset($backtrace[$i]); ++$i) { + if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { + $backtrace = array_slice($backtrace, 1 + $i); + break; + } + } + + $collectedLogs[$message] = array( + 'type' => $type, + 'message' => $message, + 'file' => $file, + 'line' => $line, + 'trace' => $backtrace, + 'count' => 1, + ); + }); + } + + try { + foreach ($this->warmers as $warmer) { + if (!$this->optionalsEnabled && $warmer->isOptional()) { + continue; + } + if ($this->onlyOptionalsEnabled && !$warmer->isOptional()) { + continue; + } + + $warmer->warmUp($cacheDir); } + } finally { + if ($this->debug && true !== $previousHandler) { + restore_error_handler(); - $warmer->warmUp($cacheDir); + if (file_exists($this->deprecationLogsFilepath)) { + $previousLogs = unserialize(file_get_contents($this->deprecationLogsFilepath)); + $collectedLogs = array_merge($previousLogs, $collectedLogs); + } + + file_put_contents($this->deprecationLogsFilepath, serialize(array_values($collectedLogs))); + } } } From b79f38c364f923bd8fb7cfc6fafe3ae635f0d7a1 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Thu, 7 Jun 2018 15:51:52 +0200 Subject: [PATCH 035/125] [WebServerBundle] Improve the error message when web server is already running --- .../Bundle/WebServerBundle/Command/ServerStartCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php index 664bce114b643..49e38701192e1 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php @@ -135,7 +135,7 @@ protected function execute(InputInterface $input, OutputInterface $output) 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')))); + $io->error(sprintf('The web server has already been started. It is currently listening on http://%s. Please stop the web server before you try to start it again.', $server->getAddress($input->getOption('pidfile')))); return 1; } From 13523ad9854cd733c8db55640b7059e7789e2e2b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 23 Apr 2018 18:02:04 +0200 Subject: [PATCH 036/125] [Cache] Add stampede protection via probabilistic early expiration --- UPGRADE-4.2.md | 5 ++ UPGRADE-5.0.md | 5 ++ .../Cache/Adapter/AbstractAdapter.php | 21 ++++++- .../Component/Cache/Adapter/ChainAdapter.php | 11 ++-- .../Cache/Adapter/PhpArrayAdapter.php | 6 +- .../Component/Cache/Adapter/ProxyAdapter.php | 53 ++++++++++++++---- .../Cache/Adapter/TagAwareAdapter.php | 4 +- .../Cache/Adapter/TraceableAdapter.php | 4 +- src/Symfony/Component/Cache/CHANGELOG.md | 5 +- .../Component/Cache/CacheInterface.php | 6 +- src/Symfony/Component/Cache/CacheItem.php | 41 ++++++++++++-- .../Cache/Tests/Adapter/AdapterTestCase.php | 36 ++++++++++++ .../Cache/Tests/Adapter/ArrayAdapterTest.php | 1 + .../Cache/Tests/Adapter/ChainAdapterTest.php | 6 +- .../Adapter/NamespacedProxyAdapterTest.php | 7 ++- .../Tests/Adapter/PhpArrayAdapterTest.php | 7 ++- .../Cache/Tests/Adapter/ProxyAdapterTest.php | 7 ++- .../Tests/Adapter/TagAwareAdapterTest.php | 15 +++++ .../Component/Cache/Tests/CacheItemTest.php | 2 +- .../Component/Cache/Traits/GetTrait.php | 55 +++++++++++++++++-- 20 files changed, 254 insertions(+), 43 deletions(-) diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 7386221a5df9d..743543a1c871a 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -1,6 +1,11 @@ UPGRADE FROM 4.1 to 4.2 ======================= +Cache +----- + + * Deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead. + Security -------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 7c4ddf1d8eeb3..4d8447368e9cd 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -1,6 +1,11 @@ UPGRADE FROM 4.x to 5.0 ======================= +Cache +----- + + * Removed `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead. + Config ------ diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 1e246b8790cb4..c6caee6ced4e3 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -46,9 +46,18 @@ protected function __construct(string $namespace = '', int $defaultLifetime = 0) function ($key, $value, $isHit) use ($defaultLifetime) { $item = new CacheItem(); $item->key = $key; - $item->value = $value; + $item->value = $v = $value; $item->isHit = $isHit; $item->defaultLifetime = $defaultLifetime; + // Detect wrapped values that encode for their expiry and creation duration + // For compactness, these values are packed in the key of an array using + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = \key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + $item->value = $v[$k]; + $v = \unpack('Ve/Nc', \substr($k, 1, -1)); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } return $item; }, @@ -64,12 +73,18 @@ function ($deferred, $namespace, &$expiredIds) use ($getId) { foreach ($deferred as $key => $item) { if (null === $item->expiry) { - $byLifetime[0 < $item->defaultLifetime ? $item->defaultLifetime : 0][$getId($key)] = $item->value; + $ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0; } elseif ($item->expiry > $now) { - $byLifetime[$item->expiry - $now][$getId($key)] = $item->value; + $ttl = $item->expiry - $now; } else { $expiredIds[] = $getId($key); + continue; + } + if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) { + unset($metadata[CacheItem::METADATA_TAGS]); } + // For compactness, expiry and creation duration are packed in the key of a array, using magic numbers as separators + $byLifetime[$ttl][$getId($key)] = $metadata ? array("\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item->value) : $item->value; } return $byLifetime; diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php index ea0af87d9f238..57b6cafd0970c 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -64,8 +64,10 @@ function ($sourceItem, $item) use ($defaultLifetime) { $item->value = $sourceItem->value; $item->expiry = $sourceItem->expiry; $item->isHit = $sourceItem->isHit; + $item->metadata = $sourceItem->metadata; $sourceItem->isTaggable = false; + unset($sourceItem->metadata[CacheItem::METADATA_TAGS]); if (0 < $sourceItem->defaultLifetime && $sourceItem->defaultLifetime < $defaultLifetime) { $defaultLifetime = $sourceItem->defaultLifetime; @@ -84,19 +86,20 @@ function ($sourceItem, $item) use ($defaultLifetime) { /** * {@inheritdoc} */ - public function get(string $key, callable $callback) + public function get(string $key, callable $callback, float $beta = null) { $lastItem = null; $i = 0; - $wrap = function (CacheItem $item = null) use ($key, $callback, &$wrap, &$i, &$lastItem) { + $wrap = function (CacheItem $item = null) use ($key, $callback, $beta, &$wrap, &$i, &$lastItem) { $adapter = $this->adapters[$i]; if (isset($this->adapters[++$i])) { $callback = $wrap; + $beta = INF === $beta ? INF : 0; } if ($adapter instanceof CacheInterface) { - $value = $adapter->get($key, $callback); + $value = $adapter->get($key, $callback, $beta); } else { - $value = $this->doGet($adapter, $key, $callback); + $value = $this->doGet($adapter, $key, $callback, $beta ?? 1.0); } if (null !== $item) { ($this->syncItem)($lastItem = $lastItem ?? $item, $item); diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index bcd322fede1ba..daba071bb7eb6 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -83,17 +83,17 @@ public static function create($file, CacheItemPoolInterface $fallbackPool) /** * {@inheritdoc} */ - public function get(string $key, callable $callback) + public function get(string $key, callable $callback, float $beta = null) { if (null === $this->values) { $this->initialize(); } if (null === $value = $this->values[$key] ?? null) { if ($this->pool instanceof CacheInterface) { - return $this->pool->get($key, $callback); + return $this->pool->get($key, $callback, $beta); } - return $this->doGet($this->pool, $key, $callback); + return $this->doGet($this->pool, $key, $callback, $beta ?? 1.0); } if ('N;' === $value) { return null; diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index b9981f5e64c0c..796dd6c6063fc 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -31,6 +31,7 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa private $namespace; private $namespaceLen; private $createCacheItem; + private $setInnerItem; private $poolHash; public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0) @@ -43,11 +44,22 @@ public function __construct(CacheItemPoolInterface $pool, string $namespace = '' function ($key, $innerItem) use ($defaultLifetime, $poolHash) { $item = new CacheItem(); $item->key = $key; - $item->value = $innerItem->get(); + $item->value = $v = $innerItem->get(); $item->isHit = $innerItem->isHit(); $item->defaultLifetime = $defaultLifetime; $item->innerItem = $innerItem; $item->poolHash = $poolHash; + // Detect wrapped values that encode for their expiry and creation duration + // For compactness, these values are packed in the key of an array using + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = \key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + $item->value = $v[$k]; + $v = \unpack('Ve/Nc', \substr($k, 1, -1)); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } elseif ($innerItem instanceof CacheItem) { + $item->metadata = $innerItem->metadata; + } $innerItem->set(null); return $item; @@ -55,20 +67,43 @@ function ($key, $innerItem) use ($defaultLifetime, $poolHash) { null, CacheItem::class ); + $this->setInnerItem = \Closure::bind( + /** + * @param array $item A CacheItem cast to (array); accessing protected properties requires adding the \0*\0" PHP prefix + */ + function (CacheItemInterface $innerItem, array $item) { + // Tags are stored separately, no need to account for them when considering this item's newly set metadata + if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) { + unset($metadata[CacheItem::METADATA_TAGS]); + } + if ($metadata) { + // For compactness, expiry and creation duration are packed in the key of a array, using magic numbers as separators + $item["\0*\0value"] = array("\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item["\0*\0value"]); + } + $innerItem->set($item["\0*\0value"]); + $innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U', $item["\0*\0expiry"]) : null); + }, + null, + CacheItem::class + ); } /** * {@inheritdoc} */ - public function get(string $key, callable $callback) + public function get(string $key, callable $callback, float $beta = null) { if (!$this->pool instanceof CacheInterface) { - return $this->doGet($this->pool, $key, $callback); + return $this->doGet($this, $key, $callback, $beta ?? 1.0); } return $this->pool->get($this->getId($key), function ($innerItem) use ($key, $callback) { - return $callback(($this->createCacheItem)($key, $innerItem)); - }); + $item = ($this->createCacheItem)($key, $innerItem); + $item->set($value = $callback($item)); + ($this->setInnerItem)($innerItem, (array) $item); + + return $value; + }, $beta); } /** @@ -164,13 +199,11 @@ private function doSave(CacheItemInterface $item, $method) return false; } $item = (array) $item; - $expiry = $item["\0*\0expiry"]; - if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) { - $expiry = time() + $item["\0*\0defaultLifetime"]; + if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) { + $item["\0*\0expiry"] = 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); + ($this->setInnerItem)($innerItem, $item); return $this->pool->$method($innerItem); } diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index e810f5d00fb0d..f49e97119ad53 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -67,7 +67,7 @@ function (CacheItem $item, $key, array &$itemTags) { } if (isset($itemTags[$key])) { foreach ($itemTags[$key] as $tag => $version) { - $item->prevTags[$tag] = $tag; + $item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag; } unset($itemTags[$key]); } else { @@ -84,7 +84,7 @@ function (CacheItem $item, $key, array &$itemTags) { function ($deferred) { $tagsByKey = array(); foreach ($deferred as $key => $item) { - $tagsByKey[$key] = $item->tags; + $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? array(); } return $tagsByKey; diff --git a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php index a0df682d92b69..76db2f66d8386 100644 --- a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php @@ -37,7 +37,7 @@ public function __construct(AdapterInterface $pool) /** * {@inheritdoc} */ - public function get(string $key, callable $callback) + public function get(string $key, callable $callback, float $beta = null) { if (!$this->pool instanceof CacheInterface) { throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_class($this->pool), CacheInterface::class)); @@ -52,7 +52,7 @@ public function get(string $key, callable $callback) $event = $this->start(__FUNCTION__); try { - $value = $this->pool->get($key, $callback); + $value = $this->pool->get($key, $callback, $beta); $event->result[$key] = \is_object($value) ? \get_class($value) : gettype($value); } finally { $event->end = microtime(true); diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index f6fb43ebe7874..b0f7793a25386 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -4,8 +4,9 @@ CHANGELOG 4.2.0 ----- - * added `CacheInterface`, which should become the preferred way to use a cache + * added `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool + * deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead 3.4.0 ----- @@ -19,7 +20,7 @@ CHANGELOG 3.3.0 ----- - * [EXPERIMENTAL] added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any + * 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) diff --git a/src/Symfony/Component/Cache/CacheInterface.php b/src/Symfony/Component/Cache/CacheInterface.php index 49194d135e9bd..7a149d71a9268 100644 --- a/src/Symfony/Component/Cache/CacheInterface.php +++ b/src/Symfony/Component/Cache/CacheInterface.php @@ -26,8 +26,12 @@ interface CacheInterface { /** * @param callable(CacheItem):mixed $callback Should return the computed value for the given key/item + * @param float|null $beta A float that controls the likeliness of triggering early expiration. + * 0 disables it, INF forces immediate expiration. + * The default (or providing null) is implementation dependent but should + * typically be 1.0, which should provide optimal stampede protection. * * @return mixed The value corresponding to the provided key */ - public function get(string $key, callable $callback); + public function get(string $key, callable $callback, float $beta = null); } diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index 82ad9df68262c..91fdc53164040 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -21,13 +21,30 @@ */ final class CacheItem implements CacheItemInterface { + /** + * References the Unix timestamp stating when the item will expire. + */ + const METADATA_EXPIRY = 'expiry'; + + /** + * References the time the item took to be created, in milliseconds. + */ + const METADATA_CTIME = 'ctime'; + + /** + * References the list of tags that were assigned to the item, as string[]. + */ + const METADATA_TAGS = 'tags'; + + private const METADATA_EXPIRY_OFFSET = 1527506807; + protected $key; protected $value; protected $isHit = false; protected $expiry; protected $defaultLifetime; - protected $tags = array(); - protected $prevTags = array(); + protected $metadata = array(); + protected $newMetadata = array(); protected $innerItem; protected $poolHash; protected $isTaggable = false; @@ -121,7 +138,7 @@ public function tag($tags) 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])) { + if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) { continue; } if ('' === $tag) { @@ -130,7 +147,7 @@ public function tag($tags) if (false !== strpbrk($tag, '{}()/\@:')) { throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters {}()/\@:', $tag)); } - $this->tags[$tag] = $tag; + $this->newMetadata[self::METADATA_TAGS][$tag] = $tag; } return $this; @@ -140,10 +157,24 @@ public function tag($tags) * Returns the list of tags bound to the value coming from the pool storage if any. * * @return array + * + * @deprecated since Symfony 4.2, use the "getMetadata()" method instead. */ public function getPreviousTags() { - return $this->prevTags; + @trigger_error(sprintf('The "%s" method is deprecated since Symfony 4.2, use the "getMetadata()" method instead.', __METHOD__), E_USER_DEPRECATED); + + return $this->metadata[self::METADATA_TAGS] ?? array(); + } + + /** + * Returns a list of metadata info that were saved alongside with the cached value. + * + * See public CacheItem::METADATA_* consts for keys potentially found in the returned array. + */ + public function getMetadata(): array + { + return $this->metadata; } /** diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index 3c96b731cc565..385c70720901a 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -45,6 +45,42 @@ public function testGet() $item = $cache->getItem('foo'); $this->assertSame($value, $item->get()); + + $isHit = true; + $this->assertSame($value, $cache->get('foo', function (CacheItem $item) use (&$isHit) { $isHit = false; }, 0)); + $this->assertTrue($isHit); + + $this->assertNull($cache->get('foo', function (CacheItem $item) use (&$isHit, $value) { + $isHit = false; + $this->assertTrue($item->isHit()); + $this->assertSame($value, $item->get()); + }, INF)); + $this->assertFalse($isHit); + } + + public function testGetMetadata() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = $this->createCachePool(0, __FUNCTION__); + + $cache->deleteItem('foo'); + $cache->get('foo', function ($item) { + $item->expiresAfter(10); + sleep(1); + + return 'bar'; + }); + + $item = $cache->getItem('foo'); + + $expected = array( + CacheItem::METADATA_EXPIRY => 9 + time(), + CacheItem::METADATA_CTIME => 1000, + ); + $this->assertSame($expected, $item->getMetadata()); } public function testDefaultLifeTime() diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php index 725d79015082e..9503501899b33 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php @@ -19,6 +19,7 @@ class ArrayAdapterTest extends AdapterTestCase { protected $skippedTests = array( + 'testGetMetadata' => 'ArrayAdapter does not keep metadata.', 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.', 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.', ); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php index 293a90cc86783..0c9969e9275c8 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php @@ -24,8 +24,12 @@ */ class ChainAdapterTest extends AdapterTestCase { - public function createCachePool($defaultLifetime = 0) + public function createCachePool($defaultLifetime = 0, $testMethod = null) { + if ('testGetMetadata' === $testMethod) { + return new ChainAdapter(array(new FilesystemAdapter('', $defaultLifetime)), $defaultLifetime); + } + return new ChainAdapter(array(new ArrayAdapter($defaultLifetime), new ExternalAdapter(), new FilesystemAdapter('', $defaultLifetime)), $defaultLifetime); } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/NamespacedProxyAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/NamespacedProxyAdapterTest.php index c2714033385f4..f1ffcbb823fb7 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/NamespacedProxyAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/NamespacedProxyAdapterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter; /** @@ -19,8 +20,12 @@ */ class NamespacedProxyAdapterTest extends ProxyAdapterTest { - public function createCachePool($defaultLifetime = 0) + public function createCachePool($defaultLifetime = 0, $testMethod = null) { + if ('testGetMetadata' === $testMethod) { + return new ProxyAdapter(new FilesystemAdapter(), 'foo', $defaultLifetime); + } + return new ProxyAdapter(new ArrayAdapter($defaultLifetime), 'foo', $defaultLifetime); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php index 8630b52cf30c9..19c1285af40d5 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; @@ -68,8 +69,12 @@ protected function tearDown() } } - public function createCachePool() + public function createCachePool($defaultLifetime = 0, $testMethod = null) { + if ('testGetMetadata' === $testMethod) { + return new PhpArrayAdapter(self::$file, new FilesystemAdapter()); + } + return new PhpArrayAdapterWrapper(self::$file, new NullAdapter()); } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php index ff4b9d34bcbaf..fbbdac22a8874 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ProxyAdapterTest.php @@ -13,6 +13,7 @@ use Psr\Cache\CacheItemInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\CacheItem; @@ -27,8 +28,12 @@ class ProxyAdapterTest extends AdapterTestCase 'testPrune' => 'ProxyAdapter just proxies', ); - public function createCachePool($defaultLifetime = 0) + public function createCachePool($defaultLifetime = 0, $testMethod = null) { + if ('testGetMetadata' === $testMethod) { + return new ProxyAdapter(new FilesystemAdapter(), '', $defaultLifetime); + } + return new ProxyAdapter(new ArrayAdapter(), '', $defaultLifetime); } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php index 7074299e7ac34..ad37fbef7d25d 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; +use Symfony\Component\Cache\CacheItem; /** * @group time-sensitive @@ -138,6 +139,9 @@ public function testTagItemExpiry() $this->assertFalse($pool->getItem('foo')->isHit()); } + /** + * @group legacy + */ public function testGetPreviousTags() { $pool = $this->createCachePool(); @@ -149,6 +153,17 @@ public function testGetPreviousTags() $this->assertSame(array('foo' => 'foo'), $i->getPreviousTags()); } + public function testGetMetadata() + { + $pool = $this->createCachePool(); + + $i = $pool->getItem('k'); + $pool->save($i->tag('foo')); + + $i = $pool->getItem('k'); + $this->assertSame(array('foo' => 'foo'), $i->getMetadata()[CacheItem::METADATA_TAGS]); + } + public function testPrune() { $cache = new TagAwareAdapter($this->getPruneableMock()); diff --git a/src/Symfony/Component/Cache/Tests/CacheItemTest.php b/src/Symfony/Component/Cache/Tests/CacheItemTest.php index 3a0ea098ad7c1..2572651290cfc 100644 --- a/src/Symfony/Component/Cache/Tests/CacheItemTest.php +++ b/src/Symfony/Component/Cache/Tests/CacheItemTest.php @@ -63,7 +63,7 @@ public function testTag() $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->assertSame(array('foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'), $item->newMetadata[CacheItem::METADATA_TAGS]); }, $this, CacheItem::class)); } diff --git a/src/Symfony/Component/Cache/Traits/GetTrait.php b/src/Symfony/Component/Cache/Traits/GetTrait.php index d2a5f92da2e53..c2aef90c389dc 100644 --- a/src/Symfony/Component/Cache/Traits/GetTrait.php +++ b/src/Symfony/Component/Cache/Traits/GetTrait.php @@ -11,9 +11,15 @@ namespace Symfony\Component\Cache\Traits; +use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; /** + * An implementation for CacheInterface that provides stampede protection via probabilistic early expiration. + * + * @see https://en.wikipedia.org/wiki/Cache_stampede + * * @author Nicolas Grekas * * @internal @@ -23,21 +29,58 @@ trait GetTrait /** * {@inheritdoc} */ - public function get(string $key, callable $callback) + public function get(string $key, callable $callback, float $beta = null) { - return $this->doGet($this, $key, $callback); + return $this->doGet($this, $key, $callback, $beta ?? 1.0); } - private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback) + private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, float $beta) { + $t = 0; $item = $pool->getItem($key); + $recompute = !$item->isHit() || INF === $beta; + + if ($item instanceof CacheItem && 0 < $beta) { + if ($recompute) { + $t = microtime(true); + } else { + $metadata = $item->getMetadata(); + $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? false; + $ctime = $metadata[CacheItem::METADATA_CTIME] ?? false; + + if ($ctime && $expiry) { + $t = microtime(true); + $recompute = $expiry <= $t - $ctime / 1000 * $beta * log(random_int(1, PHP_INT_MAX) / PHP_INT_MAX); + } + } + if ($recompute) { + // force applying defaultLifetime to expiry + $item->expiresAt(null); + } + } - if ($item->isHit()) { + if (!$recompute) { return $item->get(); } - $pool->save($item->set($value = $callback($item))); + static $save = null; + + if (null === $save) { + $save = \Closure::bind( + function (CacheItemPoolInterface $pool, CacheItemInterface $item, $value, float $startTime) { + if ($item instanceof CacheItem && $startTime && $item->expiry > $endTime = microtime(true)) { + $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry; + $item->newMetadata[CacheItem::METADATA_CTIME] = 1000 * (int) ($endTime - $startTime); + } + $pool->save($item->set($value)); + + return $value; + }, + null, + CacheItem::class + ); + } - return $value; + return $save($pool, $item, $callback($item), $t); } } From 51381e530a948af3162b4a29fede0843795c35c7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 7 Jun 2018 22:31:06 +0200 Subject: [PATCH 037/125] [Cache] Unconditionally use PhpFilesAdapter for system pools --- .../FrameworkExtension.php | 1 - .../Resources/config/cache.xml | 8 ++--- .../Cache/Adapter/AbstractAdapter.php | 4 +++ .../Cache/Adapter/PhpArrayAdapter.php | 1 - .../Cache/Adapter/PhpFilesAdapter.php | 5 +-- src/Symfony/Component/Cache/CHANGELOG.md | 1 + .../Component/Cache/Simple/PhpArrayCache.php | 1 - .../Component/Cache/Simple/PhpFilesCache.php | 5 +-- .../Tests/Adapter/MaxIdLengthAdapterTest.php | 2 +- .../Tests/Adapter/PhpFilesAdapterTest.php | 4 --- .../Cache/Tests/Simple/PhpFilesCacheTest.php | 4 --- .../Component/Cache/Traits/AbstractTrait.php | 14 ++++++-- .../Cache/Traits/FilesystemCommonTrait.php | 12 +++++-- .../Component/Cache/Traits/PhpArrayTrait.php | 13 +------ .../Component/Cache/Traits/PhpFilesTrait.php | 34 ++++++++++--------- 15 files changed, 51 insertions(+), 58 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f15e9ac12c435..b5f4b4d9c366c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1538,7 +1538,6 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con { $version = new Parameter('container.build_id'); $container->getDefinition('cache.adapter.apcu')->replaceArgument(2, $version); - $container->getDefinition('cache.adapter.system')->replaceArgument(2, $version); $container->getDefinition('cache.adapter.filesystem')->replaceArgument(2, $config['directory']); if (isset($config['prefix_seed'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml index cd4d51e2c3936..4040709c788f8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml @@ -35,15 +35,15 @@ - - + 0 - %kernel.cache_dir%/pools - + + + diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index c6caee6ced4e3..6b9f7ed50ddf7 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -102,9 +102,13 @@ function ($deferred, $namespace, &$expiredIds) use ($getId) { * @param LoggerInterface|null $logger * * @return AdapterInterface + * + * @deprecated since Symfony 4.2 */ public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null) { + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2.', __CLASS__), E_USER_DEPRECATED); + if (null === self::$apcuSupported) { self::$apcuSupported = ApcuAdapter::isSupported(); } diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index 0cc791cd5bb88..f72fb8a6f8be0 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -43,7 +43,6 @@ public function __construct(string $file, AdapterInterface $fallbackPool) { $this->file = $file; $this->pool = $fallbackPool; - $this->zendDetectUnicode = ini_get('zend.detect_unicode'); $this->createCacheItem = \Closure::bind( function ($key, $value, $isHit) { $item = new CacheItem(); diff --git a/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php index 41879df266571..7c1850662d86d 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php @@ -24,14 +24,11 @@ class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface */ public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null) { - if (!static::isSupported()) { - throw new CacheException('OPcache is not enabled'); - } + self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); parent::__construct('', $defaultLifetime); $this->init($namespace, $directory); $e = new \Exception(); $this->includeHandler = function () use ($e) { throw $e; }; - $this->zendDetectUnicode = ini_get('zend.detect_unicode'); } } diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index b0f7793a25386..9026c1c95adf3 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * added `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool * deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead + * deprecated the `AbstractAdapter::createSystemCache()` method 3.4.0 ----- diff --git a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php index 64dc776f74a31..5d401be767d78 100644 --- a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php +++ b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php @@ -36,7 +36,6 @@ public function __construct(string $file, CacheInterface $fallbackPool) { $this->file = $file; $this->pool = $fallbackPool; - $this->zendDetectUnicode = ini_get('zend.detect_unicode'); } /** diff --git a/src/Symfony/Component/Cache/Simple/PhpFilesCache.php b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php index 77239c32eda69..3347038bb33aa 100644 --- a/src/Symfony/Component/Cache/Simple/PhpFilesCache.php +++ b/src/Symfony/Component/Cache/Simple/PhpFilesCache.php @@ -24,14 +24,11 @@ class PhpFilesCache extends AbstractCache implements PruneableInterface */ public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null) { - if (!static::isSupported()) { - throw new CacheException('OPcache is not enabled'); - } + self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); parent::__construct('', $defaultLifetime); $this->init($namespace, $directory); $e = new \Exception(); $this->includeHandler = function () use ($e) { throw $e; }; - $this->zendDetectUnicode = ini_get('zend.detect_unicode'); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php index cf2384c5f37e6..660f5c2b2c1be 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php @@ -26,7 +26,7 @@ public function testLongKey() $cache->expects($this->exactly(2)) ->method('doHave') ->withConsecutive( - array($this->equalTo('----------:0GTYWa9n4ed8vqNlOT2iEr:')), + array($this->equalTo('----------:nWfzGiCgLczv3SSUzXL3kg:')), array($this->equalTo('----------:---------------------------------------')) ); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php index 8e93c937f6a65..9fecd9724b029 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php @@ -25,10 +25,6 @@ class PhpFilesAdapterTest extends AdapterTestCase public function createCachePool() { - if (!PhpFilesAdapter::isSupported()) { - $this->markTestSkipped('OPcache extension is not enabled.'); - } - return new PhpFilesAdapter('sf-cache'); } diff --git a/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php index 7a402682ae247..38e5ee90b1221 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/PhpFilesCacheTest.php @@ -25,10 +25,6 @@ class PhpFilesCacheTest extends CacheTestCase public function createSimpleCache() { - if (!PhpFilesCache::isSupported()) { - $this->markTestSkipped('OPcache extension is not enabled.'); - } - return new PhpFilesCache('sf-cache'); } diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php index 92999a2f3c34d..60a9e77abac48 100644 --- a/src/Symfony/Component/Cache/Traits/AbstractTrait.php +++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php @@ -27,6 +27,7 @@ trait AbstractTrait private $namespaceVersion = ''; private $versioningIsEnabled = false; private $deferred = array(); + private $ids = array(); /** * @var int|null The maximum length to enforce for identifiers or null when no limit applies @@ -198,6 +199,7 @@ public function reset() $this->commit(); } $this->namespaceVersion = ''; + $this->ids = array(); } /** @@ -229,8 +231,6 @@ protected static function unserialize($value) private function getId($key) { - CacheItem::validateKey($key); - if ($this->versioningIsEnabled && '' === $this->namespaceVersion) { $this->namespaceVersion = '1:'; foreach ($this->doFetch(array('@'.$this->namespace)) as $v) { @@ -238,11 +238,19 @@ private function getId($key) } } + if (\is_string($key) && isset($this->ids[$key])) { + return $this->namespace.$this->namespaceVersion.$this->ids[$key]; + } + CacheItem::validateKey($key); + $this->ids[$key] = $key; + if (null === $this->maxIdLength) { return $this->namespace.$this->namespaceVersion.$key; } if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) { - $id = $this->namespace.$this->namespaceVersion.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -22); + // Use MD5 to favor speed over security, which is not an issue here + $this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), ':', -2); + $id = $this->namespace.$this->namespaceVersion.$id; } return $id; diff --git a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php index b0f495e4d4c51..2f1764c425df2 100644 --- a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php +++ b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php @@ -56,7 +56,7 @@ 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; + $ok = ($file->isDir() || $this->doUnlink($file) || !file_exists($file)) && $ok; } return $ok; @@ -71,12 +71,17 @@ protected function doDelete(array $ids) foreach ($ids as $id) { $file = $this->getFile($id); - $ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok; + $ok = (!file_exists($file) || $this->doUnlink($file) || !file_exists($file)) && $ok; } return $ok; } + protected function doUnlink($file) + { + return @unlink($file); + } + private function write($file, $data, $expiresAt = null) { set_error_handler(__CLASS__.'::throwError'); @@ -98,7 +103,8 @@ private function write($file, $data, $expiresAt = null) private function getFile($id, $mkdir = false) { - $hash = str_replace('/', '-', base64_encode(hash('sha256', static::class.$id, true))); + // Use MD5 to favor speed over security, which is not an issue here + $hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true))); $dir = $this->directory.strtoupper($hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR); if ($mkdir && !file_exists($dir)) { diff --git a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php index e90492b3a14d3..837d429854fc5 100644 --- a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php @@ -26,7 +26,6 @@ trait PhpArrayTrait private $file; private $values; - private $zendDetectUnicode; /** * Store an array of cached values. @@ -98,7 +97,6 @@ public function warmUp(array $values) } $dump .= "\n);\n"; - $dump = str_replace("' . \"\\0\" . '", "\0", $dump); $tmpFile = uniqid($this->file, true); @@ -128,15 +126,6 @@ public function clear() */ private function initialize() { - if ($this->zendDetectUnicode) { - $zmb = ini_set('zend.detect_unicode', 0); - } - try { - $this->values = file_exists($this->file) ? (include $this->file ?: array()) : array(); - } finally { - if ($this->zendDetectUnicode) { - ini_set('zend.detect_unicode', $zmb); - } - } + $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 index 32bbeb71237e2..2c0ff3aef1577 100644 --- a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php @@ -26,11 +26,14 @@ trait PhpFilesTrait use FilesystemCommonTrait; private $includeHandler; - private $zendDetectUnicode; + + private static $startTime; public static function isSupported() { - return function_exists('opcache_invalidate') && ini_get('opcache.enable'); + self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); + + return \function_exists('opcache_invalidate') && ini_get('opcache.enable') && ('cli' !== \PHP_SAPI || ini_get('opcache.enable_cli')); } /** @@ -40,7 +43,6 @@ public function prune() { $time = time(); $pruned = true; - $allowCompile = 'cli' !== PHP_SAPI || ini_get('opcache.enable_cli'); set_error_handler($this->includeHandler); try { @@ -48,11 +50,7 @@ public function prune() list($expiresAt) = include $file; if ($time >= $expiresAt) { - $pruned = @unlink($file) && !file_exists($file) && $pruned; - - if ($allowCompile) { - @opcache_invalidate($file, true); - } + $pruned = $this->doUnlink($file) && !file_exists($file) && $pruned; } } } finally { @@ -70,9 +68,6 @@ protected function doFetch(array $ids) $values = array(); $now = time(); - if ($this->zendDetectUnicode) { - $zmb = ini_set('zend.detect_unicode', 0); - } set_error_handler($this->includeHandler); try { foreach ($ids as $id) { @@ -88,9 +83,6 @@ protected function doFetch(array $ids) } } finally { restore_error_handler(); - if ($this->zendDetectUnicode) { - ini_set('zend.detect_unicode', $zmb); - } } foreach ($values as $id => $value) { @@ -119,7 +111,7 @@ 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'); + $allowCompile = self::isSupported(); foreach ($values as $key => $value) { if (null === $value || \is_object($value)) { @@ -142,7 +134,8 @@ protected function doSave(array $values, $lifetime) $data[1] = $value; $file = $this->getFile($key, true); - $ok = $this->write($file, 'write($file, ' Date: Tue, 24 Apr 2018 14:56:46 +0200 Subject: [PATCH 038/125] [Cache] Use sub-second accuracy for internal expiry calculations --- .../Component/Cache/Adapter/AbstractAdapter.php | 8 +++----- src/Symfony/Component/Cache/Adapter/ArrayAdapter.php | 6 +++--- src/Symfony/Component/Cache/Adapter/ProxyAdapter.php | 4 ++-- src/Symfony/Component/Cache/CHANGELOG.md | 1 + src/Symfony/Component/Cache/CacheItem.php | 10 +++++----- src/Symfony/Component/Cache/Simple/ArrayCache.php | 4 ++-- .../Component/Cache/Tests/Adapter/AdapterTestCase.php | 4 ++-- .../Component/Cache/Tests/Fixtures/ArrayCache.php | 4 ++-- src/Symfony/Component/Cache/Traits/ArrayTrait.php | 4 ++-- 9 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 6b9f7ed50ddf7..b6e493916b860 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -68,15 +68,13 @@ function ($key, $value, $isHit) use ($defaultLifetime) { $this->mergeByLifetime = \Closure::bind( function ($deferred, $namespace, &$expiredIds) use ($getId) { $byLifetime = array(); - $now = time(); + $now = microtime(true); $expiredIds = array(); foreach ($deferred as $key => $item) { if (null === $item->expiry) { $ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0; - } elseif ($item->expiry > $now) { - $ttl = $item->expiry - $now; - } else { + } elseif (0 >= $ttl = (int) ($item->expiry - $now)) { $expiredIds[] = $getId($key); continue; } @@ -107,7 +105,7 @@ function ($deferred, $namespace, &$expiredIds) use ($getId) { */ public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); if (null === self::$apcuSupported) { self::$apcuSupported = ApcuAdapter::isSupported(); diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index 17f2beaf0fe75..07fc198115a50 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -87,7 +87,7 @@ public function getItems(array $keys = array()) CacheItem::validateKey($key); } - return $this->generateItems($keys, time(), $this->createCacheItem); + return $this->generateItems($keys, microtime(true), $this->createCacheItem); } /** @@ -115,7 +115,7 @@ public function save(CacheItemInterface $item) $value = $item["\0*\0value"]; $expiry = $item["\0*\0expiry"]; - if (null !== $expiry && $expiry <= time()) { + if (null !== $expiry && $expiry <= microtime(true)) { $this->deleteItem($key); return true; @@ -131,7 +131,7 @@ public function save(CacheItemInterface $item) } } if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) { - $expiry = time() + $item["\0*\0defaultLifetime"]; + $expiry = microtime(true) + $item["\0*\0defaultLifetime"]; } $this->values[$key] = $value; diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index 796dd6c6063fc..ddb533e0a95d7 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -81,7 +81,7 @@ function (CacheItemInterface $innerItem, array $item) { $item["\0*\0value"] = array("\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item["\0*\0value"]); } $innerItem->set($item["\0*\0value"]); - $innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U', $item["\0*\0expiry"]) : null); + $innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6f', $item["\0*\0expiry"])) : null); }, null, CacheItem::class @@ -200,7 +200,7 @@ private function doSave(CacheItemInterface $item, $method) } $item = (array) $item; if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) { - $item["\0*\0expiry"] = time() + $item["\0*\0defaultLifetime"]; + $item["\0*\0expiry"] = microtime(true) + $item["\0*\0defaultLifetime"]; } $innerItem = $item["\0*\0poolHash"] === $this->poolHash ? $item["\0*\0innerItem"] : $this->pool->getItem($this->namespace.$item["\0*\0key"]); ($this->setInnerItem)($innerItem, $item); diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 9026c1c95adf3..2288db8cfebdb 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * added `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache + * added sub-second expiry accuracy for backends that support it * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool * deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead * deprecated the `AbstractAdapter::createSystemCache()` method diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index 91fdc53164040..d762539203df6 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -89,9 +89,9 @@ public function set($value) public function expiresAt($expiration) { if (null === $expiration) { - $this->expiry = $this->defaultLifetime > 0 ? time() + $this->defaultLifetime : null; + $this->expiry = $this->defaultLifetime > 0 ? microtime(true) + $this->defaultLifetime : null; } elseif ($expiration instanceof \DateTimeInterface) { - $this->expiry = (int) $expiration->format('U'); + $this->expiry = (float) $expiration->format('U.u'); } else { throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given', is_object($expiration) ? get_class($expiration) : gettype($expiration))); } @@ -105,11 +105,11 @@ public function expiresAt($expiration) public function expiresAfter($time) { if (null === $time) { - $this->expiry = $this->defaultLifetime > 0 ? time() + $this->defaultLifetime : null; + $this->expiry = $this->defaultLifetime > 0 ? microtime(true) + $this->defaultLifetime : null; } elseif ($time instanceof \DateInterval) { - $this->expiry = (int) \DateTime::createFromFormat('U', time())->add($time)->format('U'); + $this->expiry = microtime(true) + \DateTime::createFromFormat('U', 0)->add($time)->format('U.u'); } elseif (\is_int($time)) { - $this->expiry = $time + time(); + $this->expiry = $time + microtime(true); } 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))); } diff --git a/src/Symfony/Component/Cache/Simple/ArrayCache.php b/src/Symfony/Component/Cache/Simple/ArrayCache.php index d1ef583125b8b..6cef8b6e745c4 100644 --- a/src/Symfony/Component/Cache/Simple/ArrayCache.php +++ b/src/Symfony/Component/Cache/Simple/ArrayCache.php @@ -64,7 +64,7 @@ public function getMultiple($keys, $default = null) CacheItem::validateKey($key); } - return $this->generateItems($keys, time(), function ($k, $v, $hit) use ($default) { return $hit ? $v : $default; }); + return $this->generateItems($keys, microtime(true), function ($k, $v, $hit) use ($default) { return $hit ? $v : $default; }); } /** @@ -121,7 +121,7 @@ public function setMultiple($values, $ttl = null) } } } - $expiry = 0 < $ttl ? time() + $ttl : PHP_INT_MAX; + $expiry = 0 < $ttl ? microtime(true) + $ttl : PHP_INT_MAX; foreach ($valuesArray as $key => $value) { $this->values[$key] = $value; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index 385c70720901a..72d143e3c039c 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -77,10 +77,10 @@ public function testGetMetadata() $item = $cache->getItem('foo'); $expected = array( - CacheItem::METADATA_EXPIRY => 9 + time(), + CacheItem::METADATA_EXPIRY => 9.5 + time(), CacheItem::METADATA_CTIME => 1000, ); - $this->assertSame($expected, $item->getMetadata()); + $this->assertEquals($expected, $item->getMetadata(), 'Item metadata should embed expiry and ctime.', .6); } public function testDefaultLifeTime() diff --git a/src/Symfony/Component/Cache/Tests/Fixtures/ArrayCache.php b/src/Symfony/Component/Cache/Tests/Fixtures/ArrayCache.php index 1a6157e822117..27fb82de0186e 100644 --- a/src/Symfony/Component/Cache/Tests/Fixtures/ArrayCache.php +++ b/src/Symfony/Component/Cache/Tests/Fixtures/ArrayCache.php @@ -21,12 +21,12 @@ protected function doContains($id) $expiry = $this->data[$id][1]; - return !$expiry || time() <= $expiry || !$this->doDelete($id); + return !$expiry || microtime(true) < $expiry || !$this->doDelete($id); } protected function doSave($id, $data, $lifeTime = 0) { - $this->data[$id] = array($data, $lifeTime ? time() + $lifeTime : false); + $this->data[$id] = array($data, $lifeTime ? microtime(true) + $lifeTime : false); return true; } diff --git a/src/Symfony/Component/Cache/Traits/ArrayTrait.php b/src/Symfony/Component/Cache/Traits/ArrayTrait.php index b7d2ad6d6299c..86ad17e9c52e6 100644 --- a/src/Symfony/Component/Cache/Traits/ArrayTrait.php +++ b/src/Symfony/Component/Cache/Traits/ArrayTrait.php @@ -44,7 +44,7 @@ public function hasItem($key) { CacheItem::validateKey($key); - return isset($this->expiries[$key]) && ($this->expiries[$key] >= time() || !$this->deleteItem($key)); + return isset($this->expiries[$key]) && ($this->expiries[$key] > microtime(true) || !$this->deleteItem($key)); } /** @@ -81,7 +81,7 @@ 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))) { + 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]; From 473a025643cc24b6ea9e74bb1cf25201a2fdb21e Mon Sep 17 00:00:00 2001 From: Rhodri Pugh Date: Tue, 12 Jun 2018 16:19:40 +0100 Subject: [PATCH 039/125] add property path to exception message when error writing property --- src/Symfony/Component/PropertyAccess/PropertyAccessor.php | 6 +++--- .../Component/PropertyAccess/Tests/PropertyAccessorTest.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 37637e67833a4..bb959223c4f73 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -151,14 +151,14 @@ public function setValue(&$objectOrArray, $propertyPath, $value) $value = $zval[self::VALUE]; } } catch (\TypeError $e) { - self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0); + self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $propertyPath); // It wasn't thrown in this class so rethrow it throw $e; } } - private static function throwInvalidArgumentException($message, $trace, $i) + private static function throwInvalidArgumentException($message, $trace, $i, $propertyPath) { if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && isset($trace[$i]['args'][0])) { $pos = strpos($message, $delim = 'must be of the type ') ?: (strpos($message, $delim = 'must be an instance of ') ?: strpos($message, $delim = 'must implement interface ')); @@ -166,7 +166,7 @@ private static function throwInvalidArgumentException($message, $trace, $i) $type = $trace[$i]['args'][0]; $type = is_object($type) ? get_class($type) : gettype($type); - throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given.', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type)); + throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type, $propertyPath)); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index f160be61f325f..edbf9840f62e0 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -529,7 +529,7 @@ public function testIsWritableForReferenceChainIssue($object, $path, $value) /** * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException - * @expectedExceptionMessage Expected argument of type "DateTime", "string" given + * @expectedExceptionMessage Expected argument of type "DateTime", "string" given at property path "date" */ public function testThrowTypeError() { From 32988b42941b657dd48d633bd57e5305d363b081 Mon Sep 17 00:00:00 2001 From: benoushnorouzi Date: Thu, 7 Jun 2018 11:24:03 +0200 Subject: [PATCH 040/125] Enhance the twig not found exception Enhance the twig not found exception --- .../DependencyInjection/TwigExtension.php | 5 ++ .../Loader/NativeFilesystemLoader.php | 50 +++++++++++++++++++ .../Loader/NativeFilesystemLoaderTest.php | 41 +++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php create mode 100644 src/Symfony/Bundle/TwigBundle/Tests/Loader/NativeFilesystemLoaderTest.php diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index b44dd2481c631..f117dd47bc4fe 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection; use Symfony\Bridge\Twig\Extension\WebLinkExtension; +use Symfony\Bundle\TwigBundle\Loader\NativeFilesystemLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Console\Application; @@ -92,6 +93,10 @@ public function load(array $configs, ContainerBuilder $container) $twigFilesystemLoaderDefinition = $container->getDefinition('twig.loader.native_filesystem'); + if ($container->getParameter('kernel.debug')) { + $twigFilesystemLoaderDefinition->setClass(NativeFilesystemLoader::class); + } + // register user-configured paths foreach ($config['paths'] as $path => $namespace) { if (!$namespace) { diff --git a/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php new file mode 100644 index 0000000000000..9ef58d7bdbbe6 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.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\Bundle\TwigBundle\Loader; + +use Twig\Error\LoaderError; +use Twig\Loader\FilesystemLoader; + +/** + * @author Behnoush Norouzali + * + * @internal + */ +class NativeFilesystemLoader extends FilesystemLoader +{ + /** + * {@inheritdoc} + */ + protected function findTemplate($template, $throw = true) + { + try { + return parent::findTemplate($template, $throw); + } catch (LoaderError $e) { + if ('' === $template || '@' === $template[0] || !preg_match('/^(?P[^:]*?)(?:Bundle)?:(?P[^:]*+):(?P
{{ profiler_dump(dispatchCall.message.type) }} @@ -122,7 +123,31 @@
+ + + +
Bus