--- /dev/null
+<?php
+
+/*
+ * This file is part of the Behat.
+ * (c) Konstantin Kudryashov <ever.zet@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Behat\Behat\Context\Suite\Setup;
+
+use Behat\Behat\Context\ContextClass\ClassGenerator;
+use Behat\Behat\Context\Exception\ContextNotFoundException;
+use Behat\Testwork\Filesystem\FilesystemLogger;
+use Behat\Testwork\Suite\Exception\SuiteConfigurationException;
+use Behat\Testwork\Suite\Setup\SuiteSetup;
+use Behat\Testwork\Suite\Suite;
+use Symfony\Component\ClassLoader\ClassLoader;
+
+/**
+ * Generates classes for all contexts in the suite using autoloader.
+ *
+ * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ */
+final class SuiteWithContextsSetup implements SuiteSetup
+{
+ /**
+ * @var ClassLoader
+ */
+ private $autoloader;
+ /**
+ * @var null|FilesystemLogger
+ */
+ private $logger;
+ /**
+ * @var ClassGenerator[]
+ */
+ private $classGenerators = array();
+
+ /**
+ * Initializes setup.
+ *
+ * @param ClassLoader $autoloader
+ * @param null|FilesystemLogger $logger
+ */
+ public function __construct(ClassLoader $autoloader, FilesystemLogger $logger = null)
+ {
+ $this->autoloader = $autoloader;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Registers class generator.
+ *
+ * @param ClassGenerator $generator
+ */
+ public function registerClassGenerator(ClassGenerator $generator)
+ {
+ $this->classGenerators[] = $generator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsSuite(Suite $suite)
+ {
+ return $suite->hasSetting('contexts');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setupSuite(Suite $suite)
+ {
+ foreach ($this->getNormalizedContextClasses($suite) as $class) {
+ if (class_exists($class)) {
+ continue;
+ }
+
+ $this->ensureContextDirectory($path = $this->findClassFile($class));
+
+ if ($content = $this->generateClass($suite, $class)) {
+ $this->createContextFile($path, $content);
+ }
+ }
+ }
+
+ /**
+ * Returns normalized context classes.
+ *
+ * @param Suite $suite
+ *
+ * @return string[]
+ */
+ private function getNormalizedContextClasses(Suite $suite)
+ {
+ return array_map(
+ function ($context) {
+ return is_array($context) ? current(array_keys($context)) : $context;
+ },
+ $this->getSuiteContexts($suite)
+ );
+ }
+
+ /**
+ * Returns array of context classes configured for the provided suite.
+ *
+ * @param Suite $suite
+ *
+ * @return string[]
+ *
+ * @throws SuiteConfigurationException If `contexts` setting is not an array
+ */
+ private function getSuiteContexts(Suite $suite)
+ {
+ $contexts = $suite->getSetting('contexts');
+
+ if (!is_array($contexts)) {
+ throw new SuiteConfigurationException(
+ sprintf('`contexts` setting of the "%s" suite is expected to be an array, `%s` given.',
+ $suite->getName(),
+ gettype($contexts)
+ ),
+ $suite->getName()
+ );
+ }
+
+ return $contexts;
+ }
+
+ /**
+ * Creates context directory in the filesystem.
+ *
+ * @param string $path
+ */
+ private function createContextDirectory($path)
+ {
+ mkdir($path, 0777, true);
+
+ if ($this->logger) {
+ $this->logger->directoryCreated($path, 'place your context classes here');
+ }
+ }
+
+ /**
+ * Creates context class file in the filesystem.
+ *
+ * @param string $path
+ * @param string $content
+ */
+ private function createContextFile($path, $content)
+ {
+ file_put_contents($path, $content);
+
+ if ($this->logger) {
+ $this->logger->fileCreated($path, 'place your definitions, transformations and hooks here');
+ }
+ }
+
+ /**
+ * Finds file to store a class.
+ *
+ * @param string $class
+ *
+ * @return string
+ *
+ * @throws ContextNotFoundException If class file could not be determined
+ */
+ private function findClassFile($class)
+ {
+ list($classpath, $classname) = $this->findClasspathAndClass($class);
+ $classpath .= str_replace('_', DIRECTORY_SEPARATOR, $classname) . '.php';
+
+ foreach ($this->autoloader->getPrefixes() as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ return current($dirs) . DIRECTORY_SEPARATOR . $classpath;
+ }
+ }
+
+ if ($dirs = $this->autoloader->getFallbackDirs()) {
+ return current($dirs) . DIRECTORY_SEPARATOR . $classpath;
+ }
+
+ throw new ContextNotFoundException(sprintf(
+ 'Could not find where to put "%s" class. Have you configured autoloader properly?',
+ $class
+ ), $class);
+ }
+
+ /**
+ * Generates class using registered class generators.
+ *
+ * @param Suite $suite
+ * @param string $class
+ *
+ * @return null|string
+ */
+ private function generateClass(Suite $suite, $class)
+ {
+ $content = null;
+ foreach ($this->classGenerators as $generator) {
+ if ($generator->supportsSuiteAndClass($suite, $class)) {
+ $content = $generator->generateClass($suite, $class);
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Ensures that directory for a classpath exists.
+ *
+ * @param string $classpath
+ */
+ private function ensureContextDirectory($classpath)
+ {
+ if (!is_dir(dirname($classpath))) {
+ $this->createContextDirectory(dirname($classpath));
+ }
+ }
+
+ /**
+ * Finds classpath and classname from class.
+ *
+ * @param string $class
+ *
+ * @return array
+ */
+ private function findClasspathAndClass($class)
+ {
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $classpath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR;
+ $classname = substr($class, $pos + 1);
+
+ return array($classpath, $classname);
+ }
+
+ // PEAR-like class name
+ $classpath = null;
+ $classname = $class;
+
+ return array($classpath, $classname);
+ }
+}