From b579d5b9c26169695890ba5a47c520561c7f7468 Mon Sep 17 00:00:00 2001 From: Marco Stoll Date: Fri, 21 Jun 2019 08:46:03 +0200 Subject: [PATCH 1/5] [CHANGE] move factories to sub namespace --- src/Services/{ => Factories}/SF.php | 2 +- src/Services/{ => Factories}/ServicesFactory.php | 5 +++-- src/Services/ServiceLocatorTrait.php | 2 ++ tests/Services/{ => Factories}/SFTest.php | 4 ++-- tests/Services/{ => Factories}/ServicesFactoryTest.php | 4 ++-- 5 files changed, 10 insertions(+), 7 deletions(-) rename src/Services/{ => Factories}/SF.php (93%) rename src/Services/{ => Factories}/ServicesFactory.php (97%) rename tests/Services/{ => Factories}/SFTest.php (90%) rename tests/Services/{ => Factories}/ServicesFactoryTest.php (97%) diff --git a/src/Services/SF.php b/src/Services/Factories/SF.php similarity index 93% rename from src/Services/SF.php rename to src/Services/Factories/SF.php index 2fa7c42..51762dc 100644 --- a/src/Services/SF.php +++ b/src/Services/Factories/SF.php @@ -8,7 +8,7 @@ */ declare(strict_types=1); -namespace FF\Services; +namespace FF\Services\Factories; /** * Class SF diff --git a/src/Services/ServicesFactory.php b/src/Services/Factories/ServicesFactory.php similarity index 97% rename from src/Services/ServicesFactory.php rename to src/Services/Factories/ServicesFactory.php index b4e1fae..9c0b150 100644 --- a/src/Services/ServicesFactory.php +++ b/src/Services/Factories/ServicesFactory.php @@ -8,18 +8,19 @@ */ declare(strict_types=1); -namespace FF\Services; +namespace FF\Services\Factories; use FF\Factories\AbstractSingletonFactory; use FF\Factories\ClassLocators\ClassLocatorInterface; use FF\Factories\ClassLocators\NamespaceClassLocator; use FF\Factories\Exceptions\ClassNotFoundException; +use FF\Services\AbstractService; use FF\Services\Exceptions\ConfigurationException; /** * Class ServicesFactory * - * @package FF\Services + * @package FF\Services\Factories */ class ServicesFactory extends AbstractSingletonFactory { diff --git a/src/Services/ServiceLocatorTrait.php b/src/Services/ServiceLocatorTrait.php index 8373ceb..b32fc8a 100644 --- a/src/Services/ServiceLocatorTrait.php +++ b/src/Services/ServiceLocatorTrait.php @@ -10,6 +10,8 @@ declare(strict_types=1); namespace FF\Services; +use FF\Services\Factories\SF; + /** * Trait ServiceLocatorTrait * diff --git a/tests/Services/SFTest.php b/tests/Services/Factories/SFTest.php similarity index 90% rename from tests/Services/SFTest.php rename to tests/Services/Factories/SFTest.php index a4cc8dc..399d424 100644 --- a/tests/Services/SFTest.php +++ b/tests/Services/Factories/SFTest.php @@ -10,8 +10,8 @@ declare(strict_types=1); namespace FF\Tests\Services; -use FF\Services\ServicesFactory; -use FF\Services\SF; +use FF\Services\Factories\ServicesFactory; +use FF\Services\Factories\SF; use PHPUnit\Framework\TestCase; /** diff --git a/tests/Services/ServicesFactoryTest.php b/tests/Services/Factories/ServicesFactoryTest.php similarity index 97% rename from tests/Services/ServicesFactoryTest.php rename to tests/Services/Factories/ServicesFactoryTest.php index 6e49311..4f7407b 100644 --- a/tests/Services/ServicesFactoryTest.php +++ b/tests/Services/Factories/ServicesFactoryTest.php @@ -8,12 +8,12 @@ */ declare(strict_types=1); -namespace FF\Tests\Services; +namespace FF\Tests\Services\Factories; use FF\Factories\Exceptions\ClassNotFoundException; use FF\Services\AbstractService; use FF\Services\Exceptions\ConfigurationException; -use FF\Services\ServicesFactory; +use FF\Services\Factories\ServicesFactory; use PHPUnit\Framework\TestCase; /** From 3b0ad843ff3c02a92e51668d234d2354ca36fc9a Mon Sep 17 00:00:00 2001 From: Marco Stoll Date: Fri, 21 Jun 2019 10:22:37 +0200 Subject: [PATCH 2/5] [CHANGE] use new FF-Factories features --- composer.json | 3 +- src/Services/AbstractService.php | 25 +- src/Services/Factories/ServicesFactory.php | 11 +- .../{ => Traits}/ServiceLocatorTrait.php | 5 +- tests/Services/AbstractServiceTest.php | 8 + .../Factories/ServicesFactoryTest.php | 224 ++++++++++-------- 6 files changed, 164 insertions(+), 112 deletions(-) rename src/Services/{ => Traits}/ServiceLocatorTrait.php (85%) diff --git a/composer.json b/composer.json index c444b5a..97a09de 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ ], "require": { "php": ">=7.2", - "fastforward/factories": "^1.0.0" + "fastforward/data-structures": "^1.0.0", + "fastforward/factories": "^1.2.0" }, "require-dev": { "phpunit/phpunit": "^8" diff --git a/src/Services/AbstractService.php b/src/Services/AbstractService.php index 29f1495..8c5ab33 100644 --- a/src/Services/AbstractService.php +++ b/src/Services/AbstractService.php @@ -10,16 +10,24 @@ declare(strict_types=1); namespace FF\Services; +use FF\Factories\ClassLocators\ClassIdentifierAwareInterface; use FF\Services\Exceptions\ConfigurationException; +use FF\Services\Traits\EventEmitterTrait; +use FF\Services\Traits\ServiceLocatorTrait; /** * Class AbstractService * * @package FF\Services */ -abstract class AbstractService +abstract class AbstractService implements ClassIdentifierAwareInterface { - use ServiceLocatorTrait; + use EventEmitterTrait, ServiceLocatorTrait; + + /** + * For use with the BaseNamespaceClassLocator of the ServicesFactory + */ + const COMMON_NS_SUFFIX = 'Services'; /** * @var array @@ -57,6 +65,19 @@ abstract class AbstractService return $this->options[$key] ?? $default; } + /** + * {@inheritDoc} + */ + public static function getClassIdentifier(): string + { + $className = get_called_class(); + $needle = '\\' . self::COMMON_NS_SUFFIX . '\\'; + $pos = strpos($className, $needle); + if ($pos === false) return $className; + + return substr($className, $pos + strlen($needle)); + } + /** * Initializes the service * diff --git a/src/Services/Factories/ServicesFactory.php b/src/Services/Factories/ServicesFactory.php index 9c0b150..b528cc0 100644 --- a/src/Services/Factories/ServicesFactory.php +++ b/src/Services/Factories/ServicesFactory.php @@ -11,8 +11,8 @@ declare(strict_types=1); namespace FF\Services\Factories; use FF\Factories\AbstractSingletonFactory; +use FF\Factories\ClassLocators\BaseNamespaceClassLocator; use FF\Factories\ClassLocators\ClassLocatorInterface; -use FF\Factories\ClassLocators\NamespaceClassLocator; use FF\Factories\Exceptions\ClassNotFoundException; use FF\Services\AbstractService; use FF\Services\Exceptions\ConfigurationException; @@ -35,13 +35,14 @@ class ServicesFactory extends AbstractSingletonFactory protected $servicesOptions; /** - * Uses a NamespaceClassLocator pre-configured with the FF\Services namespace. + * Uses a BaseNamespaceClassLocator pre-configured with the 'Services' as common suffix and the FF namespace. + * * @param array $servicesOptions - * @see \FF\Factories\ClassLocators\NamespaceClassLocator + * @see \FF\Factories\ClassLocators\BaseNamespaceClassLocator */ public function __construct(array $servicesOptions = []) { - parent::__construct(new NamespaceClassLocator(__NAMESPACE__)); + parent::__construct(new BaseNamespaceClassLocator(AbstractService::COMMON_NS_SUFFIX, 'FF')); $this->servicesOptions = $servicesOptions; } @@ -117,7 +118,7 @@ class ServicesFactory extends AbstractSingletonFactory /** * {@inheritDoc} - * @return NamespaceClassLocator + * @return BaseNamespaceClassLocator */ public function getClassLocator(): ClassLocatorInterface { diff --git a/src/Services/ServiceLocatorTrait.php b/src/Services/Traits/ServiceLocatorTrait.php similarity index 85% rename from src/Services/ServiceLocatorTrait.php rename to src/Services/Traits/ServiceLocatorTrait.php index b32fc8a..4dde715 100644 --- a/src/Services/ServiceLocatorTrait.php +++ b/src/Services/Traits/ServiceLocatorTrait.php @@ -8,14 +8,15 @@ */ declare(strict_types=1); -namespace FF\Services; +namespace FF\Services\Traits; +use FF\Services\AbstractService; use FF\Services\Factories\SF; /** * Trait ServiceLocatorTrait * - * @package FF\Services + * @package FF\Services\Traits */ trait ServiceLocatorTrait { diff --git a/tests/Services/AbstractServiceTest.php b/tests/Services/AbstractServiceTest.php index 37090d7..067f7d9 100644 --- a/tests/Services/AbstractServiceTest.php +++ b/tests/Services/AbstractServiceTest.php @@ -71,6 +71,14 @@ class AbstractServiceTest extends TestCase new MyService(['foo' => 'baz']); } + + /** + * Tests the namesake method/feature + */ + public function testGetClassIdentifier() + { + $this->assertEquals('MyService', MyService::getClassIdentifier()); + } } class MyService extends AbstractService diff --git a/tests/Services/Factories/ServicesFactoryTest.php b/tests/Services/Factories/ServicesFactoryTest.php index 4f7407b..3dce62b 100644 --- a/tests/Services/Factories/ServicesFactoryTest.php +++ b/tests/Services/Factories/ServicesFactoryTest.php @@ -8,119 +8,139 @@ */ declare(strict_types=1); -namespace FF\Tests\Services\Factories; +namespace FF\Tests\Services\Factories { -use FF\Factories\Exceptions\ClassNotFoundException; -use FF\Services\AbstractService; -use FF\Services\Exceptions\ConfigurationException; -use FF\Services\Factories\ServicesFactory; -use PHPUnit\Framework\TestCase; - -/** - * Test ServicesFactoryTest - * - * @package FF\Tests - */ -class ServicesFactoryTest extends TestCase -{ - const TEST_OPTIONS = ['ServiceOne' => ['foo' => 'bar']]; + use FF\Factories\ClassLocators\BaseNamespaceClassLocator; + use FF\Factories\Exceptions\ClassNotFoundException; + use FF\Services\Exceptions\ConfigurationException; + use FF\Services\Factories\ServicesFactory; + use FF\Tests\Services\ServiceOne; + use FF\Tests\Services\ServiceTwo; + use PHPUnit\Framework\TestCase; /** - * {@inheritdoc} - */ - public static function setUpBeforeClass(): void - { - ServicesFactory::clearInstance(); - } - - /** - * Tests the namesake method/feature - */ - public function testGetInstanceConfigException() - { - $this->expectException(ConfigurationException::class); - - ServicesFactory::getInstance(); - } - - /** - * Tests the namesake method/feature - */ - public function testSetGetInstance() - { - $instance = new ServicesFactory(self::TEST_OPTIONS); - $instance->getClassLocator()->prependNamespaces(__NAMESPACE__); - ServicesFactory::setInstance($instance); - - $this->assertSame($instance, ServicesFactory::getInstance()); - } - - /** - * Tests the namesake method/feature + * Test ServicesFactoryTest * - * @depends testSetGetInstance + * @package FF\Tests */ - public function testSetServiceOptions() + class ServicesFactoryTest extends TestCase { - $this->assertEquals( - self::TEST_OPTIONS['ServiceOne'], - ServicesFactory::getInstance()->getServiceOptions('ServiceOne') - ); - $this->assertEquals([], ServicesFactory::getInstance()->getServiceOptions('unknown')); - } + const TEST_OPTIONS = ['ServiceOne' => ['foo' => 'bar']]; - /** - * Tests the namesake method/feature - * - * @depends testSetGetInstance - */ - public function testGetSingle() - { - $service = ServicesFactory::getInstance()->get('ServiceOne'); - $this->assertInstanceOf(ServiceOne::class, $service); - $this->assertEquals(self::TEST_OPTIONS['ServiceOne'], $service->getOptions()); - } - - /** - * Tests the namesake method/feature - * - * @depends testSetGetInstance - */ - public function testGetMultiples() - { - $services = ServicesFactory::getInstance()->get('ServiceOne', 'ServiceOne'); - $this->assertEquals(2, count($services)); - $this->assertInstanceOf(ServiceOne::class, $services[0]); - $this->assertInstanceOf(ServiceOne::class, $services[1]); - } - - /** - * Tests the namesake method/feature - * - * @depends testSetGetInstance - */ - public function testGetClassNotFound() - { - $this->expectException(ClassNotFoundException::class); - - ServicesFactory::getInstance()->get('ServiceUnknown'); - } -} - -class ServiceOne extends AbstractService -{ - protected function validateOptions(array $options, array &$errors): bool - { - if (isset($options['foo']) && $options['foo'] != 'bar') { - $errors[] = 'foo is not bar'; - return false; + /** + * {@inheritdoc} + */ + public static function setUpBeforeClass(): void + { + ServicesFactory::clearInstance(); } - return parent::validateOptions($options, $errors); + /** + * Tests the namesake method/feature + */ + public function testGetInstanceConfigException() + { + $this->expectException(ConfigurationException::class); + + ServicesFactory::getInstance(); + } + + /** + * Tests the namesake method/feature + */ + public function testSetGetInstance() + { + $instance = new ServicesFactory(self::TEST_OPTIONS); + $instance->getClassLocator()->prependNamespaces('FF\Tests'); + ServicesFactory::setInstance($instance); + + $this->assertSame($instance, ServicesFactory::getInstance()); + } + + /** + * Tests the namesake method/feature + */ + public function testGetClassLocator() + { + $this->assertInstanceOf( + BaseNamespaceClassLocator::class, + ServicesFactory::getInstance()->getClassLocator() + ); + } + + /** + * Tests the namesake method/feature + * + * @depends testSetGetInstance + */ + public function testSetServiceOptions() + { + $this->assertEquals( + self::TEST_OPTIONS['ServiceOne'], + ServicesFactory::getInstance()->getServiceOptions('ServiceOne') + ); + $this->assertEquals([], ServicesFactory::getInstance()->getServiceOptions('ServiceTwo')); + $this->assertEquals([], ServicesFactory::getInstance()->getServiceOptions('unknown')); + } + + /** + * Tests the namesake method/feature + * + * @depends testSetGetInstance + */ + public function testGetSingle() + { + $service = ServicesFactory::getInstance()->get('ServiceOne'); + $this->assertInstanceOf(ServiceOne::class, $service); + $this->assertEquals(self::TEST_OPTIONS['ServiceOne'], $service->getOptions()); + } + + /** + * Tests the namesake method/feature + * + * @depends testSetGetInstance + */ + public function testGetMultiples() + { + $services = ServicesFactory::getInstance()->get('ServiceOne', 'ServiceTwo'); + $this->assertEquals(2, count($services)); + $this->assertInstanceOf(ServiceOne::class, $services[0]); + $this->assertInstanceOf(ServiceTwo::class, $services[1]); + } + + /** + * Tests the namesake method/feature + * + * @depends testSetGetInstance + */ + public function testGetClassNotFound() + { + $this->expectException(ClassNotFoundException::class); + + ServicesFactory::getInstance()->get('ServiceUnknown'); + } } } -class ServiceTwo extends AbstractService -{ +namespace FF\Tests\Services { + use FF\Services\AbstractService; + + class ServiceOne extends AbstractService + { + protected function validateOptions(array $options, array &$errors): bool + { + if (isset($options['foo']) && $options['foo'] != 'bar') { + $errors[] = 'foo is not bar'; + return false; + } + + return parent::validateOptions($options, $errors); + } + } + + class ServiceTwo extends AbstractService + { + + } } \ No newline at end of file From 588a41abfe12272fe3e7dd4b6e4c1aef9f2138a3 Mon Sep 17 00:00:00 2001 From: Marco Stoll Date: Fri, 21 Jun 2019 10:22:57 +0200 Subject: [PATCH 3/5] [FEATURE] add events and the event broker --- src/Events/AbstractEvent.php | 70 +++++ src/Services/EventBroker.php | 244 +++++++++++++++ src/Services/Factories/EventsFactory.php | 82 +++++ src/Services/Traits/EventEmitterTrait.php | 45 +++ tests/Events/AbstractEventTest.php | 68 +++++ tests/Services/EventBrokerTest.php | 287 ++++++++++++++++++ .../Services/Factories/EventsFactoryTest.php | 41 +++ 7 files changed, 837 insertions(+) create mode 100644 src/Events/AbstractEvent.php create mode 100644 src/Services/EventBroker.php create mode 100644 src/Services/Factories/EventsFactory.php create mode 100644 src/Services/Traits/EventEmitterTrait.php create mode 100644 tests/Events/AbstractEventTest.php create mode 100644 tests/Services/EventBrokerTest.php create mode 100644 tests/Services/Factories/EventsFactoryTest.php diff --git a/src/Events/AbstractEvent.php b/src/Events/AbstractEvent.php new file mode 100644 index 0000000..016558f --- /dev/null +++ b/src/Events/AbstractEvent.php @@ -0,0 +1,70 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Events; + +use FF\Factories\ClassLocators\ClassIdentifierAwareInterface; + +/** + * Class AbstractEvent + * + * @package FF\Events + */ +abstract class AbstractEvent implements ClassIdentifierAwareInterface +{ + /** + * For use with the BaseNamespaceClassLocator of the EventsFactory + */ + const COMMON_NS_SUFFIX = 'Events'; + + /** + * @var bool + */ + protected $isCanceled = false; + + /** + * @return bool + */ + public function isCanceled(): bool + { + return $this->isCanceled; + } + + /** + * @param bool $isCanceled + * @return $this + */ + public function setIsCanceled(bool $isCanceled) + { + $this->isCanceled = $isCanceled; + return $this; + } + + /** + * @return $this + */ + public function cancel() + { + return $this->setIsCanceled(true); + } + + /** + * {@inheritDoc} + */ + public static function getClassIdentifier(): string + { + $className = get_called_class(); + $needle = '\\' . self::COMMON_NS_SUFFIX . '\\'; + $pos = strpos($className, $needle); + if ($pos === false) return $className; + + return substr($className, $pos + strlen($needle)); + } +} \ No newline at end of file diff --git a/src/Services/EventBroker.php b/src/Services/EventBroker.php new file mode 100644 index 0000000..aa9232a --- /dev/null +++ b/src/Services/EventBroker.php @@ -0,0 +1,244 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services; + +use FF\DataStructures\IndexedCollection; +use FF\DataStructures\OrderedCollection; +use FF\Events\AbstractEvent; +use FF\Factories\Exceptions\ClassNotFoundException; +use FF\Services\Factories\EventsFactory; + +/** + * Class EventBroker + * + * @package FF\Services + */ +class EventBroker extends AbstractService +{ + /** + * @var IndexedCollection + */ + protected $subscriptions; + + /** + * @var EventsFactory + */ + protected $eventsFactory; + + /** + * {@inheritDoc} + */ + protected function initialize(array $options) + { + parent::initialize($options); + + $this->subscriptions = new IndexedCollection(); + $this->eventsFactory = EventsFactory::getInstance(); + } + + /** + * @return EventsFactory + */ + public function getEventsFactory(): EventsFactory + { + return $this->eventsFactory; + } + + /** + * @return IndexedCollection + */ + public function getSubscriptions(): IndexedCollection + { + return $this->subscriptions; + } + + /** + * @param string $classIdentifier + * @return OrderedCollection + */ + public function getSubscribers(string $classIdentifier): OrderedCollection + { + $this->initializeSubscribersCollection($classIdentifier); + return $this->subscriptions->get($classIdentifier); + } + + /** + * Appends a listener to the subscribers list of an event + * + * Removes any previous subscriptions of the listener first to the named event. + * + * @param callable $listener + * @param string $classIdentifier + * @return $this + */ + public function subscribe(callable $listener, string $classIdentifier) + { + $this->unsubscribe($listener, $classIdentifier); + $this->initializeSubscribersCollection($classIdentifier); + + $this->subscriptions->get($classIdentifier)->push($listener); + return $this; + } + + /** + * Prepends a listener to the subscriber list of an event + * + * Removes any previous subscriptions of the listener first to the named event. + * + * @param callable $listener + * @param string $classIdentifier + * @return $this + */ + public function subscribeFirst(callable $listener, string $classIdentifier) + { + $this->unsubscribe($listener, $classIdentifier); + $this->initializeSubscribersCollection($classIdentifier); + + $this->subscriptions->get($classIdentifier)->unshift($listener); + return $this; + } + + /** + * Unsubscribes a listener + * + * If $name is omitted, the listener will be unsubscribed from each event it was subscribed to. + * + * @param callable $listener + * @param string $classIdentifier + * @return $this + */ + public function unsubscribe(callable $listener, string $classIdentifier = null) + { + /** @var OrderedCollection $listenerCollection */ + foreach ($this->subscriptions as $name => $listenerCollection) { + if (!is_null($classIdentifier) && $classIdentifier != $name) continue; + + $index = $listenerCollection->search($listener, true); + if (is_null($index)) continue; + + // remove listener from event + unset($listenerCollection[$index]); + } + + return $this; + } + + /** + * Removes all subscriptions for this event + * + * @param string $classIdentifier + * @return $this + */ + public function unsubscribeAll(string $classIdentifier) + { + unset($this->subscriptions[$classIdentifier]); + return $this; + } + + /** + * Checks whether any listeners where subscribed to the named event + * + * @param string $classIdentifier + * @return bool + */ + public function hasSubscribers(string $classIdentifier): bool + { + return $this->subscriptions->has($classIdentifier) && !$this->subscriptions->get($classIdentifier)->isEmpty(); + } + + /** + * Checks of the listener has been subscribed to the given event + * + * @param callable $listener + * @param string $classIdentifier + * @return bool + */ + public function isSubscribed(callable $listener, string $classIdentifier): bool + { + if (!$this->hasSubscribers($classIdentifier)) return false; + + return !is_null($this->subscriptions->get($classIdentifier)->search($listener)); + } + + /**Notifies all listeners of events of the given type + * + * Listeners will be notified in the order of their subscriptions. + * Does nothing if no listeners subscribed to the type of the event. + * + * Creates an event instance and fires it. + * Does nothing if no suitable event model could be created. + * + * Any given $args will be passed to the constructor of the suitable event + * model class in the given order. + * + * @param string $classIdentifier + * @param mixed ...$args + * @return $this + */ + public function fire(string $classIdentifier, ...$args) + { + $event = $this->createEvent($classIdentifier, ...$args); + if (is_null($event)) return $this; + + foreach ($this->getSubscribers($classIdentifier) as $listener) { + $this->notify($listener, $event); + + if ($event->isCanceled()) { + // stop notifying further listeners if event has been canceled + break; + } + } + + return $this; + } + + /** + * Initialize listener collection if necessary + * + * @param string $classIdentifier + */ + protected function initializeSubscribersCollection(string $classIdentifier) + { + if ($this->subscriptions->has($classIdentifier)) return; + + $this->subscriptions->set($classIdentifier, new OrderedCollection()); + } + + /** + * Create a fresh event instance + * + * @param string $classIdentifier + * @param mixed ...$args + * @return AbstractEvent|null + */ + protected function createEvent(string $classIdentifier, ...$args): ?AbstractEvent + { + try { + return $this->eventsFactory->create($classIdentifier, ...$args); + } catch (ClassNotFoundException $e) { + return null; + } + } + + /** + * Passes the event to the listener + * + * The listener will be invoked with the event as the first and only argument. + * Any return values of the listener will be discarded. + * + * @param callable $listener + * @param AbstractEvent $event + */ + protected function notify(callable $listener, AbstractEvent $event) + { + call_user_func($listener, $event); + } +} \ No newline at end of file diff --git a/src/Services/Factories/EventsFactory.php b/src/Services/Factories/EventsFactory.php new file mode 100644 index 0000000..6f76a06 --- /dev/null +++ b/src/Services/Factories/EventsFactory.php @@ -0,0 +1,82 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Factories; + +use FF\Events\AbstractEvent; +use FF\Factories\AbstractFactory; +use FF\Factories\ClassLocators\BaseNamespaceClassLocator; +use FF\Factories\ClassLocators\ClassLocatorInterface; + +/** + * Class EventsFactory + * + * @package FF\Services\Factories + */ +class EventsFactory extends AbstractFactory +{ + /** + * @var EventsFactory + */ + protected static $instance; + + /** + * Declared protected to prevent external usage. + * Uses a BaseNamespaceClassLocator pre-configured with the 'Events' as common suffix and the FF namespace. + * + * @see \FF\Factories\ClassLocators\BaseNamespaceClassLocator + */ + protected function __construct() + { + parent::__construct(new BaseNamespaceClassLocator(AbstractEvent::COMMON_NS_SUFFIX, 'FF')); + } + + /** + * Declared protected to prevent external usage + */ + protected function __clone() + { + + } + + /** + * {@inheritDoc} + * @return BaseNamespaceClassLocator + */ + public function getClassLocator(): ClassLocatorInterface + { + return parent::getClassLocator(); + } + + /** + * Retrieves the singleton instance of this class + * + * @return EventsFactory + */ + public static function getInstance(): EventsFactory + { + if (is_null(self::$instance)) { + self::$instance = new EventsFactory(); + } + + return self::$instance; + } + + /** + * {@inheritdoc} + * @return AbstractEvent + */ + public function create(string $classIdentifier, ...$args) + { + /** @var AbstractEvent $event */ + $event = parent::create($classIdentifier, ...$args); + return $event; + } +} \ No newline at end of file diff --git a/src/Services/Traits/EventEmitterTrait.php b/src/Services/Traits/EventEmitterTrait.php new file mode 100644 index 0000000..d004f2b --- /dev/null +++ b/src/Services/Traits/EventEmitterTrait.php @@ -0,0 +1,45 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Traits; + +use FF\Services\EventBroker; +use FF\Services\Factories\SF; + +/** + * Trait EventEmitterTrait + * + * @package FF\Services\Traits + */ +trait EventEmitterTrait +{ + /** + * Creates an event instance and fires it + * + * Delegates the execution to the EventBroker provided by the ServiceFactory. + * + * @param string $classIdentifier + * @param mixed ...$args + * @return $this + */ + protected function fire(string $classIdentifier, ...$args) + { + /** @var EventBroker $eventBroker */ + static $eventBroker = null; + + if (is_null($eventBroker)) { + $eventBroker = SF::i()->get('EventBroker'); + } + + $eventBroker->fire($classIdentifier, ...$args); + + return $this; + } +} \ No newline at end of file diff --git a/tests/Events/AbstractEventTest.php b/tests/Events/AbstractEventTest.php new file mode 100644 index 0000000..075ff80 --- /dev/null +++ b/tests/Events/AbstractEventTest.php @@ -0,0 +1,68 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Events; + +use FF\Events\AbstractEvent; +use PHPUnit\Framework\TestCase; + +/** + * Test AbstractEventTest + * + * @package FF\Tests + */ +class AbstractEventTest extends TestCase +{ + /** + * @var MyEvent + */ + protected $uut; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->uut = new MyEvent(); + } + + /** + * Tests the namesake method/feature + */ + public function testSetGetIsCanceled() + { + $same = $this->uut->setIsCanceled(true); + $this->assertSame($this->uut, $same); + $this->assertTrue($this->uut->isCanceled()); + } + + /** + * Tests the namesake method/feature + */ + public function testCancel() + { + $same = $this->uut->cancel(); + $this->assertSame($this->uut, $same); + $this->assertTrue($this->uut->isCanceled()); + } + + /** + * Tests the namesake method/feature + */ + public function testGetClassIdentifier() + { + $this->assertEquals('MyEvent', MyEvent::getClassIdentifier()); + } +} + +class MyEvent extends AbstractEvent +{ + +} \ No newline at end of file diff --git a/tests/Services/EventBrokerTest.php b/tests/Services/EventBrokerTest.php new file mode 100644 index 0000000..f06f527 --- /dev/null +++ b/tests/Services/EventBrokerTest.php @@ -0,0 +1,287 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Services { + + use FF\DataStructures\IndexedCollection; + use FF\DataStructures\OrderedCollection; + use FF\Services\EventBroker; + use FF\Services\Factories\EventsFactory; + use FF\Tests\Services\Events\EventA; + use FF\Tests\Services\Events\EventB; + use PHPUnit\Framework\TestCase; + + /** + * Test EventBrokerTest + * + * @package FF\Tests + */ + class EventBrokerTest extends TestCase + { + /** + * @var EventBroker + */ + protected $uut; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->uut = new EventBroker(); + $this->uut->getEventsFactory() + ->getClassLocator() + ->prependNamespaces(__NAMESPACE__); + } + + /** + * {@inheritdoc} + */ + protected function tearDown(): void + { + $this->uut->unsubscribeAll('EventA'); + } + + /** + * Tests the namesake method/feature + */ + public function testGetEventsFactory() + { + $this->assertInstanceOf(EventsFactory::class, $this->uut->getEventsFactory()); + } + + /** + * Tests the namesake method/feature + */ + public function testGetSubscriptions() + { + $this->assertInstanceOf(IndexedCollection::class, $this->uut->getSubscriptions()); + } + + /** + * Tests the namesake method/feature + */ + public function testGetSubscribers() + { + $this->assertInstanceOf(OrderedCollection::class, $this->uut->getSubscribers('EventA')); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribe() + { + $callback = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback, 'EventA'); + $this->assertSame($this->uut, $same); + $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); + $this->assertSame($callback, $this->uut->getSubscribers('EventA')->getFirst()); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribeRepeated() + { + $callback = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback, 'EventA')->subscribe($callback, 'EventA'); + $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribeAppend() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback2, 'EventA'); + $this->assertSame($callback1, $this->uut->getSubscribers('EventA')->get(0)); + $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(1)); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribeFirst() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback1, 'EventA') + ->subscribeFirst($callback2, 'EventA'); + $this->assertSame($this->uut, $same); + $this->assertSame($callback1, $this->uut->getSubscribers('EventA')->get(1)); + $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(0)); + } + + /** + * Tests the namesake method/feature + */ + public function testUnsubscribe() + { + $callback1 = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback1, 'EventB') + ->unsubscribe($callback1); + $this->assertTrue($this->uut->getSubscribers('EventA')->isEmpty()); + $this->assertTrue($this->uut->getSubscribers('EventB')->isEmpty()); + } + + /** + * Tests the namesake method/feature + */ + public function testUnsubscribeNamed() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback2, 'EventA') + ->unsubscribe($callback1, 'EventA'); + $this->assertSame($this->uut, $same); + $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); + $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(0)); + } + + /** + * Tests the namesake method/feature + */ + public function testUnsubscribeAll() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback2, 'EventA') + ->unsubscribeAll('EventA'); + $this->assertSame($this->uut, $same); + $this->assertTrue($this->uut->getSubscribers('EventA')->isEmpty()); + } + + /** + * Tests the namesake method/feature + */ + public function testHasSubscribers() + { + $callback1 = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback1, 'EventA'); + + $this->assertTrue($this->uut->hasSubscribers('EventA')); + $this->assertFalse($this->uut->hasSubscribers('EventB')); + } + + /** + * Tests the namesake method/feature + */ + public function testIsSubscribed() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutB']; + + $this->uut->subscribe($callback1, 'EventA'); + + $this->assertTrue($this->uut->isSubscribed($callback1, 'EventA')); + $this->assertFalse($this->uut->isSubscribed($callback1, 'EventB')); + $this->assertFalse($this->uut->isSubscribed($callback2, 'EventA')); + $this->assertFalse($this->uut->isSubscribed($callback2, 'EventB')); + } + + /** + * Tests the namesake method/feature + */ + public function testFire() + { + $same = $this->uut->fire('EventA', 'foo'); + $this->assertSame($this->uut, $same); + } + + /** + * Tests the namesake method/feature + */ + public function testFireWithListener() + { + $this->expectOutputString('foo'); + + $this->uut->subscribe([new ListenerA(), 'shoutA'], 'EventA') + ->fire('EventA', 'foo'); + } + + /** + * Tests the namesake method/feature + */ + public function testFireCancel() + { + $this->expectOutputString('foo'); // output would be 'foofoo' if event canceling does not work + + $this->uut->subscribe([new ListenerA(), 'cancelA'], 'EventA') + ->subscribe([new ListenerB(), 'neverToReach'], 'EventA') + ->fire('EventA', 'foo'); + } + } + + class ListenerA + { + public function shoutA(EventA $event) + { + print $event->content; + } + + public function shoutB(EventB $event) + { + print $event->content; + } + + public function cancelA(EventA $event) + { + print $event->content; + $event->cancel(); + } + } + + class ListenerB extends ListenerA + { + public function shoutB(EventB $event) + { + print $event->content; + } + + public function neverToReach(EventA $event) + { + print $event->content; + } + } +} + +namespace FF\Tests\Services\Events { + + use FF\Events\AbstractEvent; + + class EventA extends AbstractEvent + { + public $content; + + public function __construct(string $content) + { + $this->content = $content; + } + } + + class EventB extends EventA + { + + } +} \ No newline at end of file diff --git a/tests/Services/Factories/EventsFactoryTest.php b/tests/Services/Factories/EventsFactoryTest.php new file mode 100644 index 0000000..2b8aad7 --- /dev/null +++ b/tests/Services/Factories/EventsFactoryTest.php @@ -0,0 +1,41 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Services\Factories; + +use FF\Factories\ClassLocators\BaseNamespaceClassLocator; +use FF\Services\Factories\EventsFactory; +use PHPUnit\Framework\TestCase; + +/** + * Test EventsFactoryTest + * + * @package FF\Tests + */ +class EventsFactoryTest extends TestCase +{ + /** + * Tests the namesake method/feature + */ + public function testGetInstance() + { + $instance = EventsFactory::getInstance(); + $this->assertInstanceOf(EventsFactory::class, $instance); + $this->assertSame($instance, EventsFactory::getInstance()); + } + + /** + * Tests the namesake method/feature + */ + public function testGetClassLocator() + { + $this->assertInstanceOf(BaseNamespaceClassLocator::class, EventsFactory::getInstance()->getClassLocator()); + } +} \ No newline at end of file From fc1d028b6a625a4797739139e2fbd70bc19828c5 Mon Sep 17 00:00:00 2001 From: Marco Stoll Date: Fri, 21 Jun 2019 10:36:57 +0200 Subject: [PATCH 4/5] [REFACTOR] reorganize namespaces for factories --- .../Factories/EventsFactory.php | 5 +- src/{Services => }/Factories/SF.php | 2 +- .../Factories/ServicesFactory.php | 5 +- src/Services/{ => Events}/EventBroker.php | 5 +- src/Services/Traits/EventEmitterTrait.php | 6 +- src/Services/Traits/ServiceLocatorTrait.php | 2 +- .../Factories/EventsFactoryTest.php | 4 +- tests/{Services => }/Factories/SFTest.php | 6 +- .../Factories/ServicesFactoryTest.php | 4 +- tests/Services/EventBrokerTest.php | 287 ----------------- tests/Services/Events/EventBrokerTest.php | 288 ++++++++++++++++++ 11 files changed, 307 insertions(+), 307 deletions(-) rename src/{Services => }/Factories/EventsFactory.php (94%) rename src/{Services => }/Factories/SF.php (93%) rename src/{Services => }/Factories/ServicesFactory.php (97%) rename src/Services/{ => Events}/EventBroker.php (98%) rename tests/{Services => }/Factories/EventsFactoryTest.php (91%) rename tests/{Services => }/Factories/SFTest.php (86%) rename tests/{Services => }/Factories/ServicesFactoryTest.php (97%) delete mode 100644 tests/Services/EventBrokerTest.php create mode 100644 tests/Services/Events/EventBrokerTest.php diff --git a/src/Services/Factories/EventsFactory.php b/src/Factories/EventsFactory.php similarity index 94% rename from src/Services/Factories/EventsFactory.php rename to src/Factories/EventsFactory.php index 6f76a06..f398f8a 100644 --- a/src/Services/Factories/EventsFactory.php +++ b/src/Factories/EventsFactory.php @@ -8,17 +8,16 @@ */ declare(strict_types=1); -namespace FF\Services\Factories; +namespace FF\Factories; use FF\Events\AbstractEvent; -use FF\Factories\AbstractFactory; use FF\Factories\ClassLocators\BaseNamespaceClassLocator; use FF\Factories\ClassLocators\ClassLocatorInterface; /** * Class EventsFactory * - * @package FF\Services\Factories + * @package FF\Factories */ class EventsFactory extends AbstractFactory { diff --git a/src/Services/Factories/SF.php b/src/Factories/SF.php similarity index 93% rename from src/Services/Factories/SF.php rename to src/Factories/SF.php index 51762dc..122d9e1 100644 --- a/src/Services/Factories/SF.php +++ b/src/Factories/SF.php @@ -8,7 +8,7 @@ */ declare(strict_types=1); -namespace FF\Services\Factories; +namespace FF\Factories; /** * Class SF diff --git a/src/Services/Factories/ServicesFactory.php b/src/Factories/ServicesFactory.php similarity index 97% rename from src/Services/Factories/ServicesFactory.php rename to src/Factories/ServicesFactory.php index b528cc0..6f9dab0 100644 --- a/src/Services/Factories/ServicesFactory.php +++ b/src/Factories/ServicesFactory.php @@ -8,9 +8,8 @@ */ declare(strict_types=1); -namespace FF\Services\Factories; +namespace FF\Factories; -use FF\Factories\AbstractSingletonFactory; use FF\Factories\ClassLocators\BaseNamespaceClassLocator; use FF\Factories\ClassLocators\ClassLocatorInterface; use FF\Factories\Exceptions\ClassNotFoundException; @@ -20,7 +19,7 @@ use FF\Services\Exceptions\ConfigurationException; /** * Class ServicesFactory * - * @package FF\Services\Factories + * @package FF\Factories */ class ServicesFactory extends AbstractSingletonFactory { diff --git a/src/Services/EventBroker.php b/src/Services/Events/EventBroker.php similarity index 98% rename from src/Services/EventBroker.php rename to src/Services/Events/EventBroker.php index aa9232a..26d99ac 100644 --- a/src/Services/EventBroker.php +++ b/src/Services/Events/EventBroker.php @@ -8,13 +8,14 @@ */ declare(strict_types=1); -namespace FF\Services; +namespace FF\Services\Events; use FF\DataStructures\IndexedCollection; use FF\DataStructures\OrderedCollection; use FF\Events\AbstractEvent; +use FF\Factories\EventsFactory; use FF\Factories\Exceptions\ClassNotFoundException; -use FF\Services\Factories\EventsFactory; +use FF\Services\AbstractService; /** * Class EventBroker diff --git a/src/Services/Traits/EventEmitterTrait.php b/src/Services/Traits/EventEmitterTrait.php index d004f2b..2b8da54 100644 --- a/src/Services/Traits/EventEmitterTrait.php +++ b/src/Services/Traits/EventEmitterTrait.php @@ -10,8 +10,8 @@ declare(strict_types=1); namespace FF\Services\Traits; -use FF\Services\EventBroker; -use FF\Services\Factories\SF; +use FF\Factories\SF; +use FF\Services\Events\EventBroker; /** * Trait EventEmitterTrait @@ -35,7 +35,7 @@ trait EventEmitterTrait static $eventBroker = null; if (is_null($eventBroker)) { - $eventBroker = SF::i()->get('EventBroker'); + $eventBroker = SF::i()->get('Events\EventBroker'); } $eventBroker->fire($classIdentifier, ...$args); diff --git a/src/Services/Traits/ServiceLocatorTrait.php b/src/Services/Traits/ServiceLocatorTrait.php index 4dde715..f29e33d 100644 --- a/src/Services/Traits/ServiceLocatorTrait.php +++ b/src/Services/Traits/ServiceLocatorTrait.php @@ -10,8 +10,8 @@ declare(strict_types=1); namespace FF\Services\Traits; +use FF\Factories\SF; use FF\Services\AbstractService; -use FF\Services\Factories\SF; /** * Trait ServiceLocatorTrait diff --git a/tests/Services/Factories/EventsFactoryTest.php b/tests/Factories/EventsFactoryTest.php similarity index 91% rename from tests/Services/Factories/EventsFactoryTest.php rename to tests/Factories/EventsFactoryTest.php index 2b8aad7..930b1f2 100644 --- a/tests/Services/Factories/EventsFactoryTest.php +++ b/tests/Factories/EventsFactoryTest.php @@ -8,10 +8,10 @@ */ declare(strict_types=1); -namespace FF\Tests\Services\Factories; +namespace FF\Tests\Factories; use FF\Factories\ClassLocators\BaseNamespaceClassLocator; -use FF\Services\Factories\EventsFactory; +use FF\Factories\EventsFactory; use PHPUnit\Framework\TestCase; /** diff --git a/tests/Services/Factories/SFTest.php b/tests/Factories/SFTest.php similarity index 86% rename from tests/Services/Factories/SFTest.php rename to tests/Factories/SFTest.php index 399d424..685049a 100644 --- a/tests/Services/Factories/SFTest.php +++ b/tests/Factories/SFTest.php @@ -8,10 +8,10 @@ */ declare(strict_types=1); -namespace FF\Tests\Services; +namespace FF\Tests\Factories; -use FF\Services\Factories\ServicesFactory; -use FF\Services\Factories\SF; +use FF\Factories\ServicesFactory; +use FF\Factories\SF; use PHPUnit\Framework\TestCase; /** diff --git a/tests/Services/Factories/ServicesFactoryTest.php b/tests/Factories/ServicesFactoryTest.php similarity index 97% rename from tests/Services/Factories/ServicesFactoryTest.php rename to tests/Factories/ServicesFactoryTest.php index 3dce62b..12e3cde 100644 --- a/tests/Services/Factories/ServicesFactoryTest.php +++ b/tests/Factories/ServicesFactoryTest.php @@ -8,12 +8,12 @@ */ declare(strict_types=1); -namespace FF\Tests\Services\Factories { +namespace FF\Tests\Factories { use FF\Factories\ClassLocators\BaseNamespaceClassLocator; use FF\Factories\Exceptions\ClassNotFoundException; + use FF\Factories\ServicesFactory; use FF\Services\Exceptions\ConfigurationException; - use FF\Services\Factories\ServicesFactory; use FF\Tests\Services\ServiceOne; use FF\Tests\Services\ServiceTwo; use PHPUnit\Framework\TestCase; diff --git a/tests/Services/EventBrokerTest.php b/tests/Services/EventBrokerTest.php deleted file mode 100644 index f06f527..0000000 --- a/tests/Services/EventBrokerTest.php +++ /dev/null @@ -1,287 +0,0 @@ - - * @copyright 2019-forever Marco Stoll - * @filesource - */ -declare(strict_types=1); - -namespace FF\Tests\Services { - - use FF\DataStructures\IndexedCollection; - use FF\DataStructures\OrderedCollection; - use FF\Services\EventBroker; - use FF\Services\Factories\EventsFactory; - use FF\Tests\Services\Events\EventA; - use FF\Tests\Services\Events\EventB; - use PHPUnit\Framework\TestCase; - - /** - * Test EventBrokerTest - * - * @package FF\Tests - */ - class EventBrokerTest extends TestCase - { - /** - * @var EventBroker - */ - protected $uut; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->uut = new EventBroker(); - $this->uut->getEventsFactory() - ->getClassLocator() - ->prependNamespaces(__NAMESPACE__); - } - - /** - * {@inheritdoc} - */ - protected function tearDown(): void - { - $this->uut->unsubscribeAll('EventA'); - } - - /** - * Tests the namesake method/feature - */ - public function testGetEventsFactory() - { - $this->assertInstanceOf(EventsFactory::class, $this->uut->getEventsFactory()); - } - - /** - * Tests the namesake method/feature - */ - public function testGetSubscriptions() - { - $this->assertInstanceOf(IndexedCollection::class, $this->uut->getSubscriptions()); - } - - /** - * Tests the namesake method/feature - */ - public function testGetSubscribers() - { - $this->assertInstanceOf(OrderedCollection::class, $this->uut->getSubscribers('EventA')); - } - - /** - * Tests the namesake method/feature - */ - public function testSubscribe() - { - $callback = [new ListenerA(), 'shoutA']; - - $same = $this->uut->subscribe($callback, 'EventA'); - $this->assertSame($this->uut, $same); - $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); - $this->assertSame($callback, $this->uut->getSubscribers('EventA')->getFirst()); - } - - /** - * Tests the namesake method/feature - */ - public function testSubscribeRepeated() - { - $callback = [new ListenerA(), 'shoutA']; - - $this->uut->subscribe($callback, 'EventA')->subscribe($callback, 'EventA'); - $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); - } - - /** - * Tests the namesake method/feature - */ - public function testSubscribeAppend() - { - $callback1 = [new ListenerA(), 'shoutA']; - $callback2 = [new ListenerA(), 'shoutA']; - - $this->uut->subscribe($callback1, 'EventA') - ->subscribe($callback2, 'EventA'); - $this->assertSame($callback1, $this->uut->getSubscribers('EventA')->get(0)); - $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(1)); - } - - /** - * Tests the namesake method/feature - */ - public function testSubscribeFirst() - { - $callback1 = [new ListenerA(), 'shoutA']; - $callback2 = [new ListenerA(), 'shoutA']; - - $same = $this->uut->subscribe($callback1, 'EventA') - ->subscribeFirst($callback2, 'EventA'); - $this->assertSame($this->uut, $same); - $this->assertSame($callback1, $this->uut->getSubscribers('EventA')->get(1)); - $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(0)); - } - - /** - * Tests the namesake method/feature - */ - public function testUnsubscribe() - { - $callback1 = [new ListenerA(), 'shoutA']; - - $this->uut->subscribe($callback1, 'EventA') - ->subscribe($callback1, 'EventB') - ->unsubscribe($callback1); - $this->assertTrue($this->uut->getSubscribers('EventA')->isEmpty()); - $this->assertTrue($this->uut->getSubscribers('EventB')->isEmpty()); - } - - /** - * Tests the namesake method/feature - */ - public function testUnsubscribeNamed() - { - $callback1 = [new ListenerA(), 'shoutA']; - $callback2 = [new ListenerA(), 'shoutA']; - - $same = $this->uut->subscribe($callback1, 'EventA') - ->subscribe($callback2, 'EventA') - ->unsubscribe($callback1, 'EventA'); - $this->assertSame($this->uut, $same); - $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); - $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(0)); - } - - /** - * Tests the namesake method/feature - */ - public function testUnsubscribeAll() - { - $callback1 = [new ListenerA(), 'shoutA']; - $callback2 = [new ListenerA(), 'shoutA']; - - $same = $this->uut->subscribe($callback1, 'EventA') - ->subscribe($callback2, 'EventA') - ->unsubscribeAll('EventA'); - $this->assertSame($this->uut, $same); - $this->assertTrue($this->uut->getSubscribers('EventA')->isEmpty()); - } - - /** - * Tests the namesake method/feature - */ - public function testHasSubscribers() - { - $callback1 = [new ListenerA(), 'shoutA']; - - $this->uut->subscribe($callback1, 'EventA'); - - $this->assertTrue($this->uut->hasSubscribers('EventA')); - $this->assertFalse($this->uut->hasSubscribers('EventB')); - } - - /** - * Tests the namesake method/feature - */ - public function testIsSubscribed() - { - $callback1 = [new ListenerA(), 'shoutA']; - $callback2 = [new ListenerA(), 'shoutB']; - - $this->uut->subscribe($callback1, 'EventA'); - - $this->assertTrue($this->uut->isSubscribed($callback1, 'EventA')); - $this->assertFalse($this->uut->isSubscribed($callback1, 'EventB')); - $this->assertFalse($this->uut->isSubscribed($callback2, 'EventA')); - $this->assertFalse($this->uut->isSubscribed($callback2, 'EventB')); - } - - /** - * Tests the namesake method/feature - */ - public function testFire() - { - $same = $this->uut->fire('EventA', 'foo'); - $this->assertSame($this->uut, $same); - } - - /** - * Tests the namesake method/feature - */ - public function testFireWithListener() - { - $this->expectOutputString('foo'); - - $this->uut->subscribe([new ListenerA(), 'shoutA'], 'EventA') - ->fire('EventA', 'foo'); - } - - /** - * Tests the namesake method/feature - */ - public function testFireCancel() - { - $this->expectOutputString('foo'); // output would be 'foofoo' if event canceling does not work - - $this->uut->subscribe([new ListenerA(), 'cancelA'], 'EventA') - ->subscribe([new ListenerB(), 'neverToReach'], 'EventA') - ->fire('EventA', 'foo'); - } - } - - class ListenerA - { - public function shoutA(EventA $event) - { - print $event->content; - } - - public function shoutB(EventB $event) - { - print $event->content; - } - - public function cancelA(EventA $event) - { - print $event->content; - $event->cancel(); - } - } - - class ListenerB extends ListenerA - { - public function shoutB(EventB $event) - { - print $event->content; - } - - public function neverToReach(EventA $event) - { - print $event->content; - } - } -} - -namespace FF\Tests\Services\Events { - - use FF\Events\AbstractEvent; - - class EventA extends AbstractEvent - { - public $content; - - public function __construct(string $content) - { - $this->content = $content; - } - } - - class EventB extends EventA - { - - } -} \ No newline at end of file diff --git a/tests/Services/Events/EventBrokerTest.php b/tests/Services/Events/EventBrokerTest.php new file mode 100644 index 0000000..9ab656e --- /dev/null +++ b/tests/Services/Events/EventBrokerTest.php @@ -0,0 +1,288 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Services\Events; + +use FF\DataStructures\IndexedCollection; +use FF\DataStructures\OrderedCollection; +use FF\Events\AbstractEvent; +use FF\Factories\EventsFactory; +use FF\Services\Events\EventBroker; +use PHPUnit\Framework\TestCase; + +/** + * Test EventBrokerTest + * + * @package FF\Tests + */ +class EventBrokerTest extends TestCase +{ + /** + * @var EventBroker + */ + protected $uut; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->uut = new EventBroker(); + $this->uut->getEventsFactory() + ->getClassLocator() + ->prependNamespaces('FF\Tests\Services'); + } + + /** + * {@inheritdoc} + */ + protected function tearDown(): void + { + $this->uut->unsubscribeAll('EventA'); + } + + /** + * Tests the namesake method/feature + */ + public function testGetEventsFactory() + { + $this->assertInstanceOf(EventsFactory::class, $this->uut->getEventsFactory()); + } + + /** + * Tests the namesake method/feature + */ + public function testGetSubscriptions() + { + $this->assertInstanceOf(IndexedCollection::class, $this->uut->getSubscriptions()); + } + + /** + * Tests the namesake method/feature + */ + public function testGetSubscribers() + { + $this->assertInstanceOf(OrderedCollection::class, $this->uut->getSubscribers('EventA')); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribe() + { + $callback = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback, 'EventA'); + $this->assertSame($this->uut, $same); + $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); + $this->assertSame($callback, $this->uut->getSubscribers('EventA')->getFirst()); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribeRepeated() + { + $callback = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback, 'EventA')->subscribe($callback, 'EventA'); + $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribeAppend() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback2, 'EventA'); + $this->assertSame($callback1, $this->uut->getSubscribers('EventA')->get(0)); + $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(1)); + } + + /** + * Tests the namesake method/feature + */ + public function testSubscribeFirst() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback1, 'EventA') + ->subscribeFirst($callback2, 'EventA'); + $this->assertSame($this->uut, $same); + $this->assertSame($callback1, $this->uut->getSubscribers('EventA')->get(1)); + $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(0)); + } + + /** + * Tests the namesake method/feature + */ + public function testUnsubscribe() + { + $callback1 = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback1, 'EventB') + ->unsubscribe($callback1); + $this->assertTrue($this->uut->getSubscribers('EventA')->isEmpty()); + $this->assertTrue($this->uut->getSubscribers('EventB')->isEmpty()); + } + + /** + * Tests the namesake method/feature + */ + public function testUnsubscribeNamed() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback2, 'EventA') + ->unsubscribe($callback1, 'EventA'); + $this->assertSame($this->uut, $same); + $this->assertEquals(1, count($this->uut->getSubscribers('EventA'))); + $this->assertSame($callback2, $this->uut->getSubscribers('EventA')->get(0)); + } + + /** + * Tests the namesake method/feature + */ + public function testUnsubscribeAll() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutA']; + + $same = $this->uut->subscribe($callback1, 'EventA') + ->subscribe($callback2, 'EventA') + ->unsubscribeAll('EventA'); + $this->assertSame($this->uut, $same); + $this->assertTrue($this->uut->getSubscribers('EventA')->isEmpty()); + } + + /** + * Tests the namesake method/feature + */ + public function testHasSubscribers() + { + $callback1 = [new ListenerA(), 'shoutA']; + + $this->uut->subscribe($callback1, 'EventA'); + + $this->assertTrue($this->uut->hasSubscribers('EventA')); + $this->assertFalse($this->uut->hasSubscribers('EventB')); + } + + /** + * Tests the namesake method/feature + */ + public function testIsSubscribed() + { + $callback1 = [new ListenerA(), 'shoutA']; + $callback2 = [new ListenerA(), 'shoutB']; + + $this->uut->subscribe($callback1, 'EventA'); + + $this->assertTrue($this->uut->isSubscribed($callback1, 'EventA')); + $this->assertFalse($this->uut->isSubscribed($callback1, 'EventB')); + $this->assertFalse($this->uut->isSubscribed($callback2, 'EventA')); + $this->assertFalse($this->uut->isSubscribed($callback2, 'EventB')); + } + + /** + * Tests the namesake method/feature + */ + public function testFire() + { + $same = $this->uut->fire('EventA', 'foo'); + $this->assertSame($this->uut, $same); + } + + /** + * Tests the namesake method/feature + */ + public function testFireWithListener() + { + $this->expectOutputString('foo'); + + $this->uut->subscribe([new ListenerA(), 'shoutA'], 'EventA') + ->fire('EventA', 'foo'); + } + + /** + * Tests the namesake method/feature + */ + public function testFireCancel() + { + $this->expectOutputString('foo'); // output would be 'foofoo' if event canceling does not work + + $this->uut->subscribe([new ListenerA(), 'cancelA'], 'EventA') + ->subscribe([new ListenerB(), 'neverToReach'], 'EventA') + ->fire('EventA', 'foo'); + } + + /** + * Tests the namesake method/feature + */ + public function testGetClassIdentifier() + { + $this->assertEquals('Events\EventBroker', EventBroker::getClassIdentifier()); + } +} + +class ListenerA +{ + public function shoutA(EventA $event) + { + print $event->content; + } + + public function shoutB(EventB $event) + { + print $event->content; + } + + public function cancelA(EventA $event) + { + print $event->content; + $event->cancel(); + } +} + +class ListenerB extends ListenerA +{ + public function shoutB(EventB $event) + { + print $event->content; + } + + public function neverToReach(EventA $event) + { + print $event->content; + } +} + +class EventA extends AbstractEvent +{ + public $content; + + public function __construct(string $content) + { + $this->content = $content; + } +} + +class EventB extends EventA +{ + +} \ No newline at end of file From a2c2a3dc72b8224f569f5a3120b776bf50bbd9a9 Mon Sep 17 00:00:00 2001 From: Marco Stoll Date: Fri, 21 Jun 2019 10:51:21 +0200 Subject: [PATCH 5/5] [DOCS] update readme --- readme.md | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index 4a03b4f..2632a14 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,8 @@ fast and easy. It addresses the common tasks and provides configurable and/or ex to use. Currently **FF** is composed of the following features: -1. Services and a Service Factory +1. Services and the Service Factory +2. Events and the Event Broker More features will follow (see the Road Map section below). @@ -59,7 +60,7 @@ of certain domain. Common attributes of services should be ## Writing services [**CONVENTION**] Service classes extend `FF\Services\AbstractService`. -[**CONVENTION**] Services should be located in your project's `MyProject\Services` namespace (or any sub namespace +[**CONVENTION**] Services must be located in your project's `MyProject\Services` namespace (or any sub namespace thereof) to be found by the `ServicesFactory`. ***Example: Basic service implementation*** @@ -115,7 +116,7 @@ For convenience reasons there is a shorthand class `SF` that lets you retrieve o ***Example: Getting a single service*** - use FF\Services\SF; + use FF\Factories\SF; use MyProject\Services\MyService; /** @var MyService $myService */ @@ -123,8 +124,8 @@ For convenience reasons there is a shorthand class `SF` that lets you retrieve o ***Example: Getting multiple services at once*** - use FF\Services\Events\EventBroker; - use FF\Services\SF; + use FF\Factories\SF; + use FF\Services\Events\EventBroker; use MyProject\Services\MyService; /** @var EventBroker $eventBroker */ @@ -160,10 +161,115 @@ naming conventions and registering the `ServiceFactory` as shown in the **Bootst /** @var MyProject\Services\Runtime\ExceptionHandler $exceptionHandler */ $exceptionHandler = SF::get('Runtime\ExceptionHandler'); +# Events and the Event Broker + +This feature provides a basic observer/observable pattern implementation. The key class is the `EventBroker`. Any +other class may act as an observable by firing events using the `EventBroker`'s api. Other classes my act as observers +by subscribing to specific types of events and being notified by the `EventBroker` in each time this type of event was +fired. + +## Firing Events + +If an observable wants to notify potential observers of notable changes it simply fires a suitable event using the +`EventBroker`'s api. + +***Example: Fire an event*** + + use FF\Factories\SF; + use FF\Services\Events\EventBroker; + + class MyExceptionHandler + { + /** + * Generic exception handler callback + * + * @param \Throwable $e + * @see http://php.net/set_exception_handler + */ + public function onException(\Throwable $e) + { + try { + /** @var EventBroker $eventBroker */ + $eventBroker = SF::i()->get('Events\EventBroker'); + $eventBroker->fire('Runtime\Exception', $e); + } catch (\Exception $e) { + // do not handle exceptions thrown while + // processing the on-exception event + } + } + } + +The `fire` method of the `EventBroker` uses the `EventsFactory` to instantiate the actual event object passing any +additional argument provided to the event class's constructor. + +## Subscribing to Events + +A valid event handling method must meet the following requirements: + +- must be public +- must not be static or abstract +- accept exactly one argument: the event classes instance + +***Example: Subscribe to an event*** + + use FF\Events; + use FF\Factories\SF; + use FF\Services\Events\EventBroker; + + class MyErrorObserver + { + /** + * Event handling callback + * + * @param Runtime\Error $event + public function onRuntimeError(Runtime\Error $event) + { + // handle the Error event + var_dump( + $event->getErrNo(), + $event->getErrMsg(), + $event->getErrFile(), + $event->getErrLine() + ); + } + } + + // subscribe to the Runtime\Error event + /** @var EventBroker $eventBroker */ + $eventBroker = SF::i()->get('Events\EventBroker'); + $eventBroker->subscribe([new MyErrorObserver, 'onRuntimeError'], 'Runtime\Error'); + +The subscription is bases on the class identifier of the event class. This is exactly the same string to use by the +observable when firing the event. + +## Defining Custom Events + +[**CONVENTION**] Event classes extend `FF\Events\AbstractEvent`. +[**CONVENTION**] Events must be located in your project's `MyProject\Events` namespace (or any sub namespace +thereof) to be found by the `EventsFactory`. + +***Example: A custom event*** + + namespace MyProject\Events; + + use FF\Events\AbstractEvent; + + /** + * This event's class identifier would just be 'Logoff' + */ + class Logoff extends AbstractEvent + { + /** + * Define constructor arguments (the event data) to meet your needs. + */ + public function __construct($eventData) + { + var_dump($eventData); + } + } # Road map -- Events - Runtime - Controllers - Sessions