diff --git a/app/code/Magento/CacheInvalidate/Model/Observer.php b/app/code/Magento/CacheInvalidate/Model/Observer.php
new file mode 100644
index 0000000000000..6deb4f50aac51
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/Model/Observer.php
@@ -0,0 +1,99 @@
+_config = $config;
+ $this->_helper = $helper;
+ $this->_curlAdapter = $curlAdapter;
+ }
+
+ /**
+ * If Varnish caching is enabled it collects array of tags
+ * of incoming object and asks to clean cache.
+ *
+ * @param \Magento\Framework\Event\Observer $observer
+ * @return void
+ */
+ public function invalidateVarnish(\Magento\Framework\Event\Observer $observer)
+ {
+ if ($this->_config->getType() == \Magento\PageCache\Model\Config::VARNISH && $this->_config->isEnabled()) {
+ $object = $observer->getEvent()->getObject();
+ if ($object instanceof \Magento\Framework\Object\IdentityInterface) {
+ $tags = [];
+ $pattern = "((^|,)%s(,|$))";
+ foreach ($object->getIdentities() as $tag) {
+ $tags[] = sprintf($pattern, preg_replace("~_\\d+$~", '', $tag));
+ $tags[] = sprintf($pattern, $tag);
+ }
+ $this->sendPurgeRequest(implode('|', array_unique($tags)));
+ }
+ }
+ }
+
+ /**
+ * Flash Varnish cache
+ *
+ * @param \Magento\Framework\Event\Observer $observer
+ * @return void
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function flushAllCache(\Magento\Framework\Event\Observer $observer)
+ {
+ if ($this->_config->getType() == \Magento\PageCache\Model\Config::VARNISH && $this->_config->isEnabled()) {
+ $this->sendPurgeRequest('.*');
+ }
+ }
+
+ /**
+ * Send curl purge request
+ * to invalidate cache by tags pattern
+ *
+ * @param string $tagsPattern
+ * @return void
+ */
+ protected function sendPurgeRequest($tagsPattern)
+ {
+ $headers = ["X-Magento-Tags-Pattern: {$tagsPattern}"];
+ $this->_curlAdapter->setOptions([CURLOPT_CUSTOMREQUEST => 'PURGE']);
+ $this->_curlAdapter->write('', $this->_helper->getUrl('*'), '1.1', $headers);
+ $this->_curlAdapter->read();
+ $this->_curlAdapter->close();
+ }
+}
diff --git a/app/code/Magento/CacheInvalidate/README.md b/app/code/Magento/CacheInvalidate/README.md
new file mode 100644
index 0000000000000..6cca6ffec03e4
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/README.md
@@ -0,0 +1,2 @@
+The CacheInvalidate module is used to invalidate the Varnish cache if it is configured.
+It listens for events that request the cache to be flushed or cause the cache to be invalid, then sends Varnish a purge request using cURL.
\ No newline at end of file
diff --git a/app/code/Magento/CacheInvalidate/Test/Unit/Model/ObserverTest.php b/app/code/Magento/CacheInvalidate/Test/Unit/Model/ObserverTest.php
new file mode 100644
index 0000000000000..1abf5d45605ac
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/Test/Unit/Model/ObserverTest.php
@@ -0,0 +1,138 @@
+_configMock = $this->getMock(
+ 'Magento\PageCache\Model\Config',
+ ['getType', 'isEnabled'],
+ [],
+ '',
+ false
+ );
+ $this->_helperMock = $this->getMock('Magento\PageCache\Helper\Data', ['getUrl'], [], '', false);
+ $this->_curlMock = $this->getMock(
+ '\Magento\Framework\HTTP\Adapter\Curl',
+ ['setOptions', 'write', 'read', 'close'],
+ [],
+ '',
+ false
+ );
+ $this->_model = new \Magento\CacheInvalidate\Model\Observer(
+ $this->_configMock,
+ $this->_helperMock,
+ $this->_curlMock
+ );
+ $this->_observerMock = $this->getMock(
+ 'Magento\Framework\Event\Observer',
+ ['getEvent'],
+ [],
+ '',
+ false
+ );
+ $this->_observerObject = $this->getMock('\Magento\Store\Model\Store', [], [], '', false);
+ }
+
+ /**
+ * Test case for cache invalidation
+ */
+ public function testInvalidateVarnish()
+ {
+ $tags = ['cache_1', 'cache_group'];
+ $pattern = '((^|,)cache(,|$))|((^|,)cache_1(,|$))|((^|,)cache_group(,|$))';
+
+ $this->_configMock->expects($this->once())->method('isEnabled')->will($this->returnValue(true));
+ $this->_configMock->expects(
+ $this->once()
+ )->method(
+ 'getType'
+ )->will(
+ $this->returnValue(\Magento\PageCache\Model\Config::VARNISH)
+ );
+ $eventMock = $this->getMock('Magento\Framework\Event', ['getObject'], [], '', false);
+ $eventMock->expects($this->once())->method('getObject')->will($this->returnValue($this->_observerObject));
+ $this->_observerMock->expects($this->once())->method('getEvent')->will($this->returnValue($eventMock));
+ $this->_observerObject->expects($this->once())->method('getIdentities')->will($this->returnValue($tags));
+ $this->sendPurgeRequest($pattern);
+
+ $this->_model->invalidateVarnish($this->_observerMock);
+ }
+
+ /**
+ * Test case for flushing all the cache
+ */
+ public function testFlushAllCache()
+ {
+ $this->_configMock->expects($this->once())->method('isEnabled')->will($this->returnValue(true));
+ $this->_configMock->expects(
+ $this->once()
+ )->method(
+ 'getType'
+ )->will(
+ $this->returnValue(\Magento\PageCache\Model\Config::VARNISH)
+ );
+
+ $this->sendPurgeRequest('.*');
+ $this->_model->flushAllCache($this->_observerMock);
+ }
+
+ /**
+ * @param string $tags
+ */
+ protected function sendPurgeRequest($tags)
+ {
+ $url = 'http://mangento.index.php';
+ $httpVersion = '1.1';
+ $headers = ["X-Magento-Tags-Pattern: {$tags}"];
+ $this->_helperMock->expects(
+ $this->any()
+ )->method(
+ 'getUrl'
+ )->with(
+ $this->equalTo('*'),
+ []
+ )->will(
+ $this->returnValue($url)
+ );
+ $this->_curlMock->expects($this->once())->method('setOptions')->with([CURLOPT_CUSTOMREQUEST => 'PURGE']);
+ $this->_curlMock->expects(
+ $this->once()
+ )->method(
+ 'write'
+ )->with(
+ $this->equalTo(''),
+ $this->equalTo($url),
+ $httpVersion,
+ $this->equalTo($headers)
+ );
+ $this->_curlMock->expects($this->once())->method('read');
+ $this->_curlMock->expects($this->once())->method('close');
+ }
+}
diff --git a/app/code/Magento/CacheInvalidate/composer.json b/app/code/Magento/CacheInvalidate/composer.json
new file mode 100644
index 0000000000000..3fe142a52e825
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "magento/module-cache-invalidate",
+ "description": "N/A",
+ "require": {
+ "php": "~5.5.0|~5.6.0",
+ "magento/module-page-cache": "0.74.0-beta4",
+ "magento/framework": "0.74.0-beta4",
+ "magento/magento-composer-installer": "*"
+ },
+ "type": "magento2-module",
+ "version": "0.74.0-beta4",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "extra": {
+ "map": [
+ [
+ "*",
+ "Magento/CacheInvalidate"
+ ]
+ ]
+ }
+}
diff --git a/app/code/Magento/CacheInvalidate/etc/events.xml b/app/code/Magento/CacheInvalidate/etc/events.xml
new file mode 100644
index 0000000000000..d4ba3665ee66f
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/etc/events.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CacheInvalidate/etc/module.xml b/app/code/Magento/CacheInvalidate/etc/module.xml
new file mode 100644
index 0000000000000..ca87df877a43a
--- /dev/null
+++ b/app/code/Magento/CacheInvalidate/etc/module.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/composer.json b/composer.json
index c8721c87609c6..907d23ff71152 100644
--- a/composer.json
+++ b/composer.json
@@ -63,6 +63,7 @@
"magento/module-backend": "self.version",
"magento/module-backup": "self.version",
"magento/module-bundle": "self.version",
+ "magento/module-cache-invalidate": "self.version",
"magento/module-captcha": "self.version",
"magento/module-catalog": "self.version",
"magento/module-catalog-import-export": "self.version",
diff --git a/composer.lock b/composer.lock
index 02e531e791e51..1850375542413 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "hash": "974fa7d59a25f8a31c70ea53068944cd",
+ "hash": "35d05640e3dc260b7a5b09310611194a",
"packages": [
{
"name": "composer/composer",
@@ -2007,16 +2007,16 @@
},
{
"name": "fabpot/php-cs-fixer",
- "version": "v1.6.1",
+ "version": "v1.6.2",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
- "reference": "14f802b2c00b672676d55612e3c0d03465b69bf6"
+ "reference": "a574ba148953fea1f78428d4b7c6843e759711f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/14f802b2c00b672676d55612e3c0d03465b69bf6",
- "reference": "14f802b2c00b672676d55612e3c0d03465b69bf6",
+ "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/a574ba148953fea1f78428d4b7c6843e759711f3",
+ "reference": "a574ba148953fea1f78428d4b7c6843e759711f3",
"shasum": ""
},
"require": {
@@ -2056,7 +2056,7 @@
}
],
"description": "A script to automatically fix Symfony Coding Standard",
- "time": "2015-04-09 19:10:26"
+ "time": "2015-04-13 21:33:33"
},
{
"name": "league/climate",
@@ -2272,16 +2272,16 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "2.0.15",
+ "version": "2.0.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "34cc484af1ca149188d0d9e91412191e398e0b67"
+ "reference": "934fd03eb6840508231a7f73eb8940cf32c3b66c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/34cc484af1ca149188d0d9e91412191e398e0b67",
- "reference": "34cc484af1ca149188d0d9e91412191e398e0b67",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/934fd03eb6840508231a7f73eb8940cf32c3b66c",
+ "reference": "934fd03eb6840508231a7f73eb8940cf32c3b66c",
"shasum": ""
},
"require": {
@@ -2330,7 +2330,7 @@
"testing",
"xunit"
],
- "time": "2015-01-24 10:06:35"
+ "time": "2015-04-11 04:35:00"
},
{
"name": "phpunit/php-file-iterator",