Más contenido relacionado La actualidad más candente (20) Similar a Decoupling with Design Patterns and Symfony2 DIC (20) Decoupling with Design Patterns and Symfony2 DIC2. @everzet
· Spent more than 7
years writing so!ware
· Spent more than 4
years learning
businesses
· Now filling the gaps
between the two as a
BDD Practice Manager
@Inviqa
7. “Extensibility is a so!ware design
principle defined as a system’s ability
to have new functionality extended, in
which the system’s internal structure
and data flow are minimally or not
affected”
11. behat 3
- extensibility as the core concept
- BC through extensibility
13. Symfony Bundles & Behat extensions
1. Framework creates a temporary
container
2. Framework asks the bundle to add its
services
3. Framework merges all temporary
containers
4. Framework compiles merged
container
14. interface CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
*
* @param ContainerBuilder $container
*
* @api
*/
public function process(ContainerBuilder $container);
}
15. class YourSuperBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new YourCompilerPass());
}
}
19. class HookDispatcher extends DispatchingService implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
EventInterface::BEFORE_SUITE => array('dispatchHooks', 10),
EventInterface::AFTER_SUITE => array('dispatchHooks', 10),
EventInterface::BEFORE_FEATURE => array('dispatchHooks', 10),
...
);
}
public function dispatchHooks(LifecycleEventInterface $event)
{
$hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool());
$this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider);
foreach ($hooksProvider->getHooksForEvent($event) as $hook) {
$this->dispatchHook($hook, $event);
}
}
...
}
20. class HooksCarrierEvent extends Event implements LifecycleEventInterface
{
public function addHook(HookInterface $hook)
{
$this->hooks[] = $hook;
}
public function getHooksForEvent(Event $event)
{
return array_filter(
$this->hooks,
function ($hook) use ($event) {
$eventName = $event->getName();
if ($eventName !== $hook->getEventName()) {
return false;
}
return $hook;
}
);
}
...
}
21. class DictionaryReader implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
EventInterface::LOAD_HOOKS => array('loadHooks', 0),
...
);
}
public function loadHooks(HooksCarrierEvent $event)
{
foreach ($this->read($event->getSuite(), $event->getContextPool()) as $callback) {
if ($callback instanceof HookInterface) {
$event->addHook($callback);
}
}
}
...
}
23. <container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="...">
<services>
<service id="event_dispatcher"
class="SymfonyComponentEventDispatcherEventDispatcher"/>
<service id="hook.hook_dispatcher"
class="BehatBehatHookEventSubscriberHookDispatcher">
<argument type="service" id="event_dispatcher"/>
<tag name="event_subscriber"/>
</service>
<service id="context.dictionary_reader"
class="BehatBehatContextEventSubscriberDictionaryReader">
<tag name="event_subscriber"/>
</service>
</services>
</container>
24. class EventSubscribersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$dispatcherDefinition = $container->getDefinition('event_dispatcher');
foreach ($container->findTaggedServiceIds('event_subscriber') as $id => $attributes) {
$dispatcherDefinition->addMethodCall('addSubscriber', array(new Reference($id)));
}
}
}
27. “Coupling is a degree to which each
program module relies on each one of
the other modules”
28. “Cohesion is a degree to which the
elements of a module belong together”
29. “Coupling is a degree to which each
program module relies on each one of
the other modules”
public function dispatchHooks(LifecycleEventInterface $event)
{
$hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool());
$this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider);
foreach ($hooksProvider->getHooksForEvent($event) as $hook) {
$this->dispatchHook($hook, $event);
}
}
30. “Cohesion is a degree to which the
elements of a module belong together”
public function dispatchHooks(LifecycleEventInterface $event)
{
$hooksProvider = new HooksCarrierEvent($event->getSuite(), $event->getContextPool());
$this->dispatch(EventInterface::LOAD_HOOKS, $hooksProvider);
foreach ($hooksProvider->getHooksForEvent($event) as $hook) {
$this->dispatchHook($hook, $event);
}
}
34. There is no single solution for
extensibility. Because extensibility is
not a single problem
38. final class EnvironmentManager
{
private $handlers = array();
public function registerEnvironmentHandler(EnvironmentHandler $handler)
{
$this->handlers[] = $handler;
}
public function buildEnvironment(Suite $suite)
{
foreach ($this->handlers as $handler) {
...
}
}
public function isolateEnvironment(Environment $environment, $testSubject = null)
{
foreach ($this->handlers as $handler) {
...
}
}
}
39. interface EnvironmentHandler
{
public function supportsSuite(Suite $suite);
public function buildEnvironment(Suite $suite);
public function supportsEnvironmentAndSubject(Environment $environment, $testSubject = null);
public function isolateEnvironment(Environment $environment, $testSubject = null);
}
42. final class EnvironmentHandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, ‘environment.handler’);
$definition = $container->getDefinition(‘environment.manager’);
foreach ($references as $reference) {
$definition->addMethodCall('registerEnvironmentHandler', array($reference));
}
}
}
44. behat testers
There are 5 testers in behat core:
1. FeatureTester
2. ScenarioTester
3. OutlineTester
4. BackgroundTester
5. StepTester
48. final class RuntimeScenarioTester implements ScenarioTester
{
public function setUp(Environment $env, FeatureNode $feature,
Scenario $example, $skip)
{
return new SuccessfulSetup();
}
public function test(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip = false)
{
...
}
public function tearDown(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip, TestResult $result)
{
return new SuccessfulTeardown();
}
}
49. interface ScenarioTester
{
public function setUp(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip);
public function test(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip);
public function tearDown(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip, TestResult $result);
}
50. final class EventDispatchingScenarioTester implements ScenarioTester
{
public function __construct(ScenarioTester $baseTester, EventDispatcherInterface $eventDispatcher)
{
$this->baseTester = $baseTester;
$this->eventDispatcher = $eventDispatcher;
}
public function setUp(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip)
{
$event = new BeforeScenarioTested($env, $feature, $scenario);
$this->eventDispatcher->dispatch($this->beforeEventName, $event);
$setup = $this->baseTester->setUp($env, $feature, $scenario, $skip);
return $setup;
}
public function test(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip)
{
return $this->baseTester->test($env, $feature, $scenario, $skip);
}
public function tearDown(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip, TestResult $result)
{
$teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result);
$event = new AfterScenarioTested($env, $feature, $scenario, $result, $teardown);
$this->eventDispatcher->dispatch($event);
return $teardown;
}
}
51. final class HookableScenarioTester implements ScenarioTester
{
public function __construct(ScenarioTester $baseTester, HookDispatcher $hookDispatcher)
{
$this->baseTester = $baseTester;
$this->hookDispatcher = $hookDispatcher;
}
public function setUp(Environment $env, FeatureNode $feature,
Scenario $example, $skip)
{
$setup = $this->baseTester->setUp($env, $feature, $scenario, $skip);
$hookCallResults = $this->hookDispatcher->dispatchScopeHooks($setup);
return new HookedSetup($setup, $hookCallResults);
}
public function test(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip = false)
{
return $this->baseTester->test($env, $feature, $scenario, $skip);
}
public function tearDown(Environment $env, FeatureNode $feature,
Scenario $scenario, $skip, TestResult $result)
{
$teardown = $this->baseTester->tearDown($env, $feature, $scenario, $skip, $result);
$hookCallResults = $this->hookDispatcher->dispatchScopeHooks($teardown);
return new HookedTeardown($teardown, $hookCallResults);
}
}
54. final class ScenarioTesterWrappersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$references = $this->findAndReorderTaggedServices($container, ‘tester.scenario_wrapper’);
foreach ($references as $reference) {
$id = (string) $reference;
$renamedId = $id . '.inner';
// This logic is based on SymfonyComponentDependencyInjectionCompilerDecoratorServicePass
$definition = $container->getDefinition(‘tester.scenario’);
$container->setDefinition($renamedId, $definition);
$container->setAlias('tester.scenario', new Alias($id, $public));
$wrappingService = $container->getDefinition($id);
$wrappingService->replaceArgument(0, new Reference($renamedId));
}
}
...
}
63. final class NodeEventListeningFormatter implements Formatter
{
public function __construct(EventListener $listener)
{
$this->listener = $listener;
}
public static function getSubscribedEvents()
{
return array(TestworkEventDispatcher::BEFORE_ALL_EVENTS => 'listenEvent');
}
public function listenEvent(Event $event, $eventName = null)
{
$eventName = $eventName ?: $event->getName();
$this->listener->listenEvent($this, $event, $eventName);
}
}
64. final class ChainEventListener implements EventListener, Countable, IteratorAggregate
{
private $listeners;
public function __construct(array $listeners)
{
$this->listeners = $listeners;
}
public function listenEvent(Formatter $formatter, Event $event, $eventName)
{
foreach ($this->listeners as $listener) {
$listener->listenEvent($formatter, $event, $eventName);
}
}
...
}
66. final class StepListener implements EventListener
{
public function listenEvent(Formatter $formatter, Event $event, $eventName)
{
$this->captureScenarioOnScenarioEvent($event);
$this->forgetScenarioOnAfterEvent($eventName);
$this->printStepSetupOnBeforeEvent($formatter, $event);
$this->printStepOnAfterEvent($formatter, $event);
}
...
}
68. class FirstBackgroundFiresFirstListener implements EventListener
{
public function __construct(EventListener $descendant)
{
$this->descendant = $descendant;
}
public function listenEvent(Formatter $formatter, Event $event, $eventName)
{
$this->flushStatesIfBeginningOfTheFeature($eventName);
$this->markFirstBackgroundPrintedAfterBackground($eventName);
if ($this->isEventDelayedUntilFirstBackgroundPrinted($event)) {
$this->delayedUntilBackgroundEnd[] = array($event, $eventName);
return;
}
$this->descendant->listenEvent($formatter, $event, $eventName);
$this->fireDelayedEventsOnAfterBackground($formatter, $eventName);
}
}
71. interface StepTester
{
public function setUp(Environment $env, FeatureNode $feature,
StepNode $step, $skip);
public function test(Environment $env, FeatureNode $feature,
StepNode $step, $skip);
public function tearDown(Environment $env, FeatureNode $feature,
StepNode $step, $skip, StepResult $result);
}
74. interface ScenarioStepTester
{
public function setUp(Environment $env, FeatureNode $feature,
ScenarioNode $scenario, StepNode $step, $skip);
public function test(Environment $env, FeatureNode $feature,
ScenarioNode $scenario, StepNode $step, $skip);
public function tearDown(Environment $env, FeatureNode $feature,
ScenarioNode $scenario, StepNode $step, $skip,
StepResult $result);
}
75. final class StepToScenarioTesterAdapter implements ScenarioStepTester
{
public function __construct(StepTester $stepTester) { ... }
public function setUp(Environment $env, FeatureNode $feature,
ScenarioNode $scenario, StepNode $step, $skip)
{
return $this->stepTester->setUp($env, $feature, $step, $skip);
}
public function test(Environment $env, FeatureNode $feature,
ScenarioNode $scenario, StepNode $step, $skip)
{
return $this->stepTester->test($env, $feature, $step, $skip);
}
public function tearDown(Environment $env, FeatureNode $feature,
ScenarioNode $scenario, StepNode $step, $skip,
StepResult $result)
{
return $this->stepTester-> tearDown($env, $feature, $step, $skip);
}
}
76. final class StepTesterAdapterPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$references = $this->processor->findAndSortTaggedServices($container, ‘tester.step_wrapper’);
foreach ($references as $reference) {
$id = (string) $reference;
$renamedId = $id . ‘.adaptee’;
$adapteeDefinition = $container->getDefinition($id);
$reflection = new ReflectionClass($adapteeDefinition->getClass());
if (!$reflection->implementsInterface(‘StepTester’)) {
return;
}
$container->removeDefinition($id);
$container->setDefinition(
$id,
new Definition(‘StepToScenarioTesterAdapter’, array(
$adapteeDefinition
));
);
}
}
}
80. backwards compatibility
Backwards compatibility in Behat
comes from the extensibility.
1. Everything is extension
2. New features are extensions too
3. New features could be toggled on/off
82. performance implications
· 2x more objects in v3 than in v2
· Value objects are used instead of
simple types
· A lot of additional concepts
throughout
· It must be slow
90. Step1: Close the doors
Assume you have no extension points
by default.
1. Private properties
2. Final classes
91. Step 2: Open doors properly when you need them
1. Identify the need for extension points
2. Make extension points explicit
94. class BundleFeatureLocator extends FilesystemFeatureLocator
{
public function locateSpecifications(Suite $suite, $locator)
{
if (!$suite instanceof SymfonyBundleSuite) {
return new noSpecificationsIterator($suite);
}
$bundle = $suite->getBundle();
if (0 !== strpos($locator, '@' . $bundle->getName())) {
return new NoSpecificationsIterator($suite);
}
$locatorSuffix = substr($locator, strlen($bundle->getName()) + 1);
return parent::locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix);
}
}
95. final class BundleFeatureLocator implements SpecificationLocator
{
public function __construct(SpecificationLocator $baseLocator) { ... }
public function locateSpecifications(Suite $suite, $locator)
{
if (!$suite instanceof SymfonyBundleSuite) {
return new noSpecificationsIterator($suite);
}
$bundle = $suite->getBundle();
if (0 !== strpos($locator, '@' . $bundle->getName())) {
return new NoSpecificationsIterator($suite);
}
$locatorSuffix = substr($locator, strlen($bundle->getName()) + 1);
return $this->baseLocator->locateSpecifications($suite, $bundle->getPath() . '/Features' . $locatorSuffix);
}
}