diff --git a/Drivers/AbstractDriver.php b/Drivers/AbstractDriver.php index 17c5d0f..8a71ced 100644 --- a/Drivers/AbstractDriver.php +++ b/Drivers/AbstractDriver.php @@ -2,6 +2,11 @@ namespace Lexik\Bundle\MaintenanceBundle\Drivers; +use Lexik\Bundle\MaintenanceBundle\Event\PostLockEvent; +use Lexik\Bundle\MaintenanceBundle\Event\PostUnlockEvent; +use Lexik\Bundle\MaintenanceBundle\Event\PreLockEvent; +use Lexik\Bundle\MaintenanceBundle\Event\PreUnlockEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Translation\TranslatorInterface; /** @@ -22,6 +27,11 @@ abstract class AbstractDriver */ protected $translator; + /** + * @var EventDispatcherInterface + */ + private $eventDispatcher; + /** * Constructor * @@ -76,10 +86,14 @@ abstract public function getMessageUnlock($resultTest); * * @return boolean */ - public function lock() + final public function lock() { if (!$this->isExists()) { - return $this->createLock(); + $this->eventDispatcher->dispatch(PreLockEvent::NAME, new PreLockEvent()); + $success = $this->createLock(); + $this->eventDispatcher->dispatch(PostLockEvent::NAME, new PostLockEvent($success)); + + return $success; } else { return false; } @@ -90,10 +104,14 @@ public function lock() * * @return boolean */ - public function unlock() + final public function unlock() { if ($this->isExists()) { - return $this->createUnlock(); + $this->eventDispatcher->dispatch(PreUnlockEvent::NAME, new PreUnlockEvent()); + $success = $this->createUnlock(); + $this->eventDispatcher->dispatch(PostUnlockEvent::NAME, new PostUnlockEvent($success)); + + return $success; } else { return false; } @@ -128,4 +146,15 @@ public function setTranslator(TranslatorInterface $translator) { $this->translator = $translator; } + + /** + * @param EventDispatcherInterface $eventDispatcher + * @return AbstractDriver + */ + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) + { + $this->eventDispatcher = $eventDispatcher; + + return $this; + } } diff --git a/Drivers/DriverFactory.php b/Drivers/DriverFactory.php index 3a71523..195f9c2 100644 --- a/Drivers/DriverFactory.php +++ b/Drivers/DriverFactory.php @@ -4,6 +4,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Translation\Translator; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Translation\TranslatorInterface; /** @@ -29,17 +30,23 @@ class DriverFactory */ protected $translator; + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + const DATABASE_DRIVER = 'Lexik\Bundle\MaintenanceBundle\Drivers\DatabaseDriver'; /** * Constructor driver factory * - * @param DatabaseDriver $dbDriver The databaseDriver Service - * @param TranslatorInterface $translator The translator service - * @param array $driverOptions Options driver + * @param DatabaseDriver $dbDriver The databaseDriver Service + * @param TranslatorInterface $translator The translator service + * @param EventDispatcherInterface $eventDispatcher Event Dispatcher + * @param array $driverOptions Options driver * @throws \ErrorException */ - public function __construct(DatabaseDriver $dbDriver, TranslatorInterface $translator, array $driverOptions) + public function __construct(DatabaseDriver $dbDriver, TranslatorInterface $translator, EventDispatcherInterface $eventDispatcher, array $driverOptions) { $this->driverOptions = $driverOptions; @@ -49,6 +56,7 @@ public function __construct(DatabaseDriver $dbDriver, TranslatorInterface $trans $this->dbDriver = $dbDriver; $this->translator = $translator; + $this->eventDispatcher = $eventDispatcher; } /** @@ -73,6 +81,7 @@ public function getDriver() } $driver->setTranslator($this->translator); + $driver->setEventDispatcher($this->eventDispatcher); return $driver; } diff --git a/Drivers/MemcachedDriver.php b/Drivers/MemcachedDriver.php new file mode 100644 index 0000000..d091d15 --- /dev/null +++ b/Drivers/MemcachedDriver.php @@ -0,0 +1,156 @@ +key = $options['key']; + // TODO: A configured Memcached instance should be injected into the constructor for easier testing. + $this->setMemcached(new \Memcached()); + foreach ($options['servers'] as $server) { + if (isset($server['host']) === false) { + throw new \InvalidArgumentException('Option "host" must be defined for each server if driver Memcached configuration is used'); + } + + if (isset($server['port']) === false || is_int($server['port']) === false) { + throw new \InvalidArgumentException('Option "port" must be defined as an integer for each server if driver Memcached configuration is used'); + } + + $this->getMemcached()->addServer($server['host'], $server['port']); + } + } + + /** + * {@inheritdoc} + */ + public function isExists() + { + if (false !== $this->getMemcached()->get($this->key)) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getMessageLock($resultTest) + { + $key = $resultTest ? 'lexik_maintenance.success_lock_memc' : 'lexik_maintenance.not_success_lock'; + + return $this->translator->trans($key, array(), 'maintenance'); + } + + /** + * {@inheritdoc} + */ + public function getMessageUnlock($resultTest) + { + $key = $resultTest ? 'lexik_maintenance.success_unlock' : 'lexik_maintenance.not_success_unlock'; + + return $this->translator->trans($key, array(), 'maintenance'); + } + + /** + * {@inheritdoc} + */ + public function setTtl($value) + { + $this->options['ttl'] = $value; + } + + /** + * {@inheritdoc} + */ + public function getTtl() + { + return $this->options['ttl']; + } + + /** + * {@inheritdoc} + */ + public function hasTtl() + { + return isset($this->options['ttl']); + } + + /** + * {@inheritdoc} + */ + protected function createLock() + { + return $this->getMemcached()->set($this->key, self::VALUE_TO_STORE, (isset($this->options['ttl']) ? $this->options['ttl'] : 0)); + } + + /** + * {@inheritdoc} + */ + protected function createUnlock() + { + return $this->getMemcached()->delete($this->key); + } + + /** + * @return \Memcached + */ + private function getMemcached() + { + return $this->memcached; + } + + /** + * @param \Memcached $memcached + * @return MemcachedDriver + */ + private function setMemcached($memcached) + { + $this->memcached = $memcached; + + return $this; + } +} diff --git a/Event/AbstractEvent.php b/Event/AbstractEvent.php new file mode 100644 index 0000000..882378e --- /dev/null +++ b/Event/AbstractEvent.php @@ -0,0 +1,15 @@ +setSuccess($success); + } + + /** + * @return bool + */ + public function isSuccess() + { + return $this->success; + } + + /** + * @param bool $success + * @return AbstractPostEvent + */ + private function setSuccess($success) + { + $this->success = $success; + + return $this; + } +} diff --git a/Event/PostLockEvent.php b/Event/PostLockEvent.php new file mode 100644 index 0000000..526a9a7 --- /dev/null +++ b/Event/PostLockEvent.php @@ -0,0 +1,13 @@ + + %lexik_maintenance.driver% diff --git a/Resources/doc/index.md b/Resources/doc/index.md index dcf556e..2d4c262 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -99,6 +99,19 @@ Or (with the optional ttl overwriting) --------------------- +### Events + +Events are thrown before and after locking and unlocking of the site, so that you can easily add other actions to the +act of making the site available or not. For example, you might want to disable external site monitoring for the duration +of your maintenance period. + +The events are: + +* `Lexik\Bundle\MaintenanceBundle\Event\PreLockEvent::NAME` - Throw just before the lock is engaged +* `Lexik\Bundle\MaintenanceBundle\Event\PostLockEvent::NAME` - Throw just after the lock is engaged +* `Lexik\Bundle\MaintenanceBundle\Event\PreUnlockEvent::NAME` - Throw just before the lock is removed +* `Lexik\Bundle\MaintenanceBundle\Event\PostUnlockEvent::NAME` - Throw just after the lock is removed + Custom error page 503 --------------------- diff --git a/Tests/Event/EventDispatcherTest.php b/Tests/Event/EventDispatcherTest.php new file mode 100644 index 0000000..6aa5b13 --- /dev/null +++ b/Tests/Event/EventDispatcherTest.php @@ -0,0 +1,131 @@ +getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); + if ($expectedEvents) { + $eventDispatcher->expects(self::at(0)) + ->method('dispatch') + ->with(PreLockEvent::NAME, new PreLockEvent()); + + $eventDispatcher->expects(self::at(1)) + ->method('dispatch') + ->with(PostLockEvent::NAME, new PostLockEvent($createLock)); + } + + $driver = $this->getMockBuilder('Lexik\Bundle\MaintenanceBundle\Drivers\FileDriver')->disableOriginalConstructor()->setMethods(array('isExists', 'createLock'))->getMock(); + $driver->expects(self::any()) + ->method('isExists') + ->willReturn($exists); + $driver->expects(self::any()) + ->method('createLock') + ->willReturn($createLock); + + $driver->setEventDispatcher($eventDispatcher); + $driver->lock(); + } + + public function createLockTestData() + { + return array( + 'Lock exists' => array( + 'exists' => true, + 'createLock' => null, + 'expectedEvents' => false, + ), + 'Lock does not exists / Created successfully' => array( + 'exists' => false, + 'createLock' => true, + 'expectedEvents' => true, + ), + 'Lock does not exists / Not created successfully' => array( + 'exists' => false, + 'createLock' => true, + 'expectedEvents' => true, + ), + ); + } + + /** + * @dataProvider createUnlockTestData + * @param $exists + * @param $createLock + * @param $expectedEvents + */ + public function testUnlock($exists, $createUnlock, $expectedEvents) + { + $eventDispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); + if ($expectedEvents) { + $eventDispatcher->expects(self::at(0)) + ->method('dispatch') + ->with(PreUnlockEvent::NAME, new PreUnlockEvent()); + + $eventDispatcher->expects(self::at(1)) + ->method('dispatch') + ->with(PostUnlockEvent::NAME, new PostUnlockEvent($createUnlock)); + } + + $driver = $this->getMockBuilder('Lexik\Bundle\MaintenanceBundle\Drivers\FileDriver')->disableOriginalConstructor()->setMethods(array('isExists', 'createUnlock'))->getMock(); + $driver->expects(self::any()) + ->method('isExists') + ->willReturn($exists); + $driver->expects(self::any()) + ->method('createUnlock') + ->willReturn($createUnlock); + + $driver->setEventDispatcher($eventDispatcher); + $driver->unlock(); + } + + public function createUnlockTestData() + { + return array( + 'Lock does not exists' => array( + 'exists' => false, + 'createUnlock' => null, + 'expectedEvents' => false, + ), + 'Lock exists / Created successfully' => array( + 'exists' => true, + 'createUnlock' => true, + 'expectedEvents' => true, + ), + 'Lock exists / Not created successfully' => array( + 'exists' => true, + 'createUnlock' => true, + 'expectedEvents' => true, + ), + ); + } + + /** + * @param array $mocks + * @param array $driverOptions + * @return DriverFactory + * @throws \ErrorException + */ + public function createDriverFactory(array $mocks = array(), array $driverOptions = array()) + { + $dbDriver = array_key_exists('dbDriver', $mocks) ? $mocks['dbDriver'] : $this->getMockBuilder('Lexik\Bundle\MaintenanceBundle\Drivers\DatabaseDriver')->disableOriginalConstructor()->getMock(); + $translator = array_key_exists('translator', $mocks) ? $mocks['translator'] : $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $eventDispatcher = array_key_exists('eventDispatcher', $mocks) ? $mocks['eventDispatcher'] : $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); + + return new DriverFactory($dbDriver, $translator, $eventDispatcher, $driverOptions); + } +} diff --git a/Tests/EventListener/MaintenanceListenerTest.php b/Tests/EventListener/MaintenanceListenerTest.php index e4af51a..bb4c660 100644 --- a/Tests/EventListener/MaintenanceListenerTest.php +++ b/Tests/EventListener/MaintenanceListenerTest.php @@ -38,7 +38,7 @@ public function testBaseRequest() $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(false),$this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(false), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory); @@ -47,7 +47,7 @@ public function testBaseRequest() $listener = new MaintenanceListenerTestWrapper($this->factory, 'path', 'host', array('ip'), array('query'), array('cookie'), 'route'); $this->assertTrue($listener->onKernelRequest($event), 'Permissive factory should approve with args'); - $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory); @@ -72,7 +72,7 @@ public function testPathFilter() $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory, null); @@ -103,7 +103,7 @@ public function testHostFilter() $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory, null, null); @@ -137,7 +137,7 @@ public function testIPFilter() $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory, null, null, null); @@ -171,7 +171,7 @@ public function testRouteFilter($debug, $route, $expected) $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory, null, null, null, array(), array(), $debug); @@ -219,7 +219,7 @@ public function testQueryFilter() $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory, null, null, null, null); @@ -259,7 +259,7 @@ public function testCookieFilter() $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(true), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); $listener = new MaintenanceListenerTestWrapper($this->factory, null, null, null, null, null); @@ -344,4 +344,12 @@ public function getTranslator() return TestHelper::getTranslator($this->container, $messageSelector); } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|\Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + public function getEventDispatcher() + { + return $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); + } } diff --git a/Tests/Maintenance/DriverFactoryTest.php b/Tests/Maintenance/DriverFactoryTest.php index 2139aa1..6c7f254 100644 --- a/Tests/Maintenance/DriverFactoryTest.php +++ b/Tests/Maintenance/DriverFactoryTest.php @@ -27,7 +27,7 @@ public function setUp() $this->container = $this->initContainer(); - $this->factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), $driverOptions); + $this->factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $this->factory); } @@ -47,14 +47,14 @@ public function testDriver() */ public function testExceptionConstructor() { - $factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), array()); + $factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), $this->getEventDispatcher(), array()); } public function testWithDatabaseChoice() { $driverOptions = array('class' => DriverFactory::DATABASE_DRIVER, 'options' => null); - $factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), $driverOptions); + $factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $factory); @@ -65,7 +65,7 @@ public function testExceptionGetDriver() { $driverOptions = array('class' => '\Unknown', 'options' => null); - $factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), $driverOptions); + $factory = new DriverFactory($this->getDatabaseDriver(), $this->getTranslator(), $this->getEventDispatcher(), $driverOptions); $this->container->set('lexik_maintenance.driver.factory', $factory); try { @@ -109,4 +109,12 @@ public function getTranslator() return TestHelper::getTranslator($this->container, $messageSelector); } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|\Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + public function getEventDispatcher() + { + return $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); + } } diff --git a/Tests/Maintenance/FileMaintenanceTest.php b/Tests/Maintenance/FileMaintenanceTest.php index de674f2..d505366 100644 --- a/Tests/Maintenance/FileMaintenanceTest.php +++ b/Tests/Maintenance/FileMaintenanceTest.php @@ -78,6 +78,7 @@ public function testUnlock() $fileM = new FileDriver($options); $fileM->setTranslator($this->getTranslator()); + $fileM->setEventDispatcher($this->getEventDispatcher()); $fileM->lock(); $fileM->unlock(); @@ -91,6 +92,7 @@ public function testIsExists() $fileM = new FileDriver($options); $fileM->setTranslator($this->getTranslator()); + $fileM->setEventDispatcher($this->getEventDispatcher()); $fileM->lock(); $this->assertTrue($fileM->isEndTime(3600)); @@ -102,6 +104,7 @@ public function testMessages() $fileM = new FileDriver($options); $fileM->setTranslator($this->getTranslator()); + $fileM->setEventDispatcher($this->getEventDispatcher()); $fileM->lock(); // lock @@ -141,4 +144,12 @@ public function getTranslator() return TestHelper::getTranslator($this->container, $messageSelector); } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|\Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + public function getEventDispatcher() + { + return $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); + } } diff --git a/Tests/Maintenance/MemcachedTest.php b/Tests/Maintenance/MemcachedTest.php new file mode 100644 index 0000000..6937049 --- /dev/null +++ b/Tests/Maintenance/MemcachedTest.php @@ -0,0 +1,66 @@ + 'mnt')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testConstructWithNotPort() + { + $memC = new MemcachedDriver(array('key_name' => 'mnt', 'host' => '127.0.0.1')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testConstructWithNotPortNumber() + { + $memC = new MemcachedDriver(array('key_name' => 'mnt', 'host' => '127.0.0.1', 'port' => 'roti')); + } + + protected function initContainer() + { + $container = new ContainerBuilder(new ParameterBag(array( + 'kernel.debug' => false, + 'kernel.bundles' => array('MaintenanceBundle' => 'Lexik\Bundle\MaintenanceBundle'), + 'kernel.cache_dir' => sys_get_temp_dir(), + 'kernel.environment' => 'dev', + 'kernel.root_dir' => __DIR__.'/../../../../', // src dir + 'kernel.default_locale' => 'fr', + ))); + + return $container; + } +} diff --git a/composer.json b/composer.json index da26c7b..ca9171e 100644 --- a/composer.json +++ b/composer.json @@ -22,11 +22,19 @@ "require": { "php": ">=5.3.9", "symfony/framework-bundle": "~2.7|~3.0|^4.0", - "symfony/translation": "~2.7|~3.0|^4.0" + "symfony/translation": "~2.7|~3.0|^4.0", + "symfony/event-dispatcher": "~2.7|~3.0|^4.0", + "symfony/console": "~2.7|~3.0|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "~2.7|~3.0|^4.0", - "phpunit/phpunit": "~4.8|~5.7.11" + "phpunit/phpunit": "~4.8|~5.7.11", + "doctrine/doctrine-bundle": "^1.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "For database site locking", + "ext-memcache": "For memcache site locking - Probably don't use this as the ext-memcache has been superseded by ext-memcached", + "ext-memcached": "For memcached site locking" }, "autoload": { "psr-4": { "Lexik\\Bundle\\MaintenanceBundle\\": "" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 52090e9..d3e2c15 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,6 +11,11 @@ syntaxCheck="false" bootstrap="vendor/autoload.php" > + + + + + ./Tests/