Merge branch 'feature/Events' into develop

This commit is contained in:
Marco Stoll 2019-06-21 10:52:29 +02:00
commit f6d2eee488
17 changed files with 1147 additions and 149 deletions

View File

@ -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"

118
readme.md
View File

@ -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

View File

@ -0,0 +1,70 @@
<?php
/**
* Definition of AbstractEvent
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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));
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Definition of EventsFactory
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Factories;
use FF\Events\AbstractEvent;
use FF\Factories\ClassLocators\BaseNamespaceClassLocator;
use FF\Factories\ClassLocators\ClassLocatorInterface;
/**
* Class EventsFactory
*
* @package FF\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;
}
}

View File

@ -8,7 +8,7 @@
*/
declare(strict_types=1);
namespace FF\Services;
namespace FF\Factories;
/**
* Class SF

View File

@ -8,18 +8,18 @@
*/
declare(strict_types=1);
namespace FF\Services;
namespace FF\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;
/**
* Class ServicesFactory
*
* @package FF\Services
* @package FF\Factories
*/
class ServicesFactory extends AbstractSingletonFactory
{
@ -34,13 +34,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;
}
@ -116,7 +117,7 @@ class ServicesFactory extends AbstractSingletonFactory
/**
* {@inheritDoc}
* @return NamespaceClassLocator
* @return BaseNamespaceClassLocator
*/
public function getClassLocator(): ClassLocatorInterface
{

View File

@ -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
*

View File

@ -0,0 +1,245 @@
<?php
/**
* Definition of EventBroker
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
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\AbstractService;
/**
* 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);
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* Definition of EventEmitterTrait
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Services\Traits;
use FF\Factories\SF;
use FF\Services\Events\EventBroker;
/**
* 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('Events\EventBroker');
}
$eventBroker->fire($classIdentifier, ...$args);
return $this;
}
}

View File

@ -8,12 +8,15 @@
*/
declare(strict_types=1);
namespace FF\Services;
namespace FF\Services\Traits;
use FF\Factories\SF;
use FF\Services\AbstractService;
/**
* Trait ServiceLocatorTrait
*
* @package FF\Services
* @package FF\Services\Traits
*/
trait ServiceLocatorTrait
{

View File

@ -0,0 +1,68 @@
<?php
/**
* Definition of AbstractEventTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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
{
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Definition of EventsFactoryTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Tests\Factories;
use FF\Factories\ClassLocators\BaseNamespaceClassLocator;
use FF\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());
}
}

View File

@ -8,10 +8,10 @@
*/
declare(strict_types=1);
namespace FF\Tests\Services;
namespace FF\Tests\Factories;
use FF\Services\ServicesFactory;
use FF\Services\SF;
use FF\Factories\ServicesFactory;
use FF\Factories\SF;
use PHPUnit\Framework\TestCase;
/**

View File

@ -0,0 +1,146 @@
<?php
/**
* Definition of ServicesFactoryTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
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\Tests\Services\ServiceOne;
use FF\Tests\Services\ServiceTwo;
use PHPUnit\Framework\TestCase;
/**
* Test ServicesFactoryTest
*
* @package FF\Tests
*/
class ServicesFactoryTest extends TestCase
{
const TEST_OPTIONS = ['ServiceOne' => ['foo' => 'bar']];
/**
* {@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('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');
}
}
}
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
{
}
}

View File

@ -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

View File

@ -0,0 +1,288 @@
<?php
/**
* Definition of EventBrokerTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @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
{
}

View File

@ -1,126 +0,0 @@
<?php
/**
* Definition of ServicesFactoryTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Tests\Services;
use FF\Factories\Exceptions\ClassNotFoundException;
use FF\Services\AbstractService;
use FF\Services\Exceptions\ConfigurationException;
use FF\Services\ServicesFactory;
use PHPUnit\Framework\TestCase;
/**
* Test ServicesFactoryTest
*
* @package FF\Tests
*/
class ServicesFactoryTest extends TestCase
{
const TEST_OPTIONS = ['ServiceOne' => ['foo' => 'bar']];
/**
* {@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
*
* @depends testSetGetInstance
*/
public function testSetServiceOptions()
{
$this->assertEquals(
self::TEST_OPTIONS['ServiceOne'],
ServicesFactory::getInstance()->getServiceOptions('ServiceOne')
);
$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', '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;
}
return parent::validateOptions($options, $errors);
}
}
class ServiceTwo extends AbstractService
{
}