#!/usr/bin/env php * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ define('BEHAT_PHP_BIN_PATH', 'php'); define('BEHAT_BIN_PATH', __FILE__); define('BEHAT_VERSION', '2.4.5'); Phar::mapPhar('behat.phar'); require_once 'phar://behat.phar/vendor/autoload.php'; // internal encoding to utf8 mb_internal_encoding('utf8'); if (!defined('BEHAT_PHAR_CLI') || true === BEHAT_PHAR_CLI) { // check, that we are in CLI if ('cli' === php_sapi_name()) { $app = new Behat\Behat\Console\BehatApplication(BEHAT_VERSION); $app->run(); } else { throw new RuntimeException('Behat can be runned only as CLI utility'); } exit(0); } __HALT_COMPILER(); ?> h behat.phar)src/Behat/Behat/Annotation/Annotation.php9Q)#2src/Behat/Behat/Annotation/AnnotationInterface.phps9Qs_(ٶ,src/Behat/Behat/Console/BehatApplication.php9QgI/src/Behat/Behat/Console/Command/BaseCommand.php 9Q rU{0src/Behat/Behat/Console/Command/BehatCommand.php9Q 5src/Behat/Behat/Console/Formatter/OutputFormatter.php` 9Q` 01src/Behat/Behat/Console/Input/InputDefinition.php9QK8src/Behat/Behat/Console/Processor/AggregateProcessor.php9Q2=<src/Behat/Behat/Console/Processor/ContextReaderProcessor.php.9Q.=>5src/Behat/Behat/Console/Processor/FormatProcessor.phpR,9QR,B6src/Behat/Behat/Console/Processor/GherkinProcessor.php 9Q >3src/Behat/Behat/Console/Processor/HelpProcessor.phpa 9Qa ]3src/Behat/Behat/Console/Processor/InitProcessor.php@9Q@96src/Behat/Behat/Console/Processor/LocatorProcessor.phpj 9Qj /src/Behat/Behat/Console/Processor/Processor.phpl9QlGF8src/Behat/Behat/Console/Processor/ProcessorInterface.php9QfO"2src/Behat/Behat/Console/Processor/RunProcessor.php 9Q (B(src/Behat/Behat/Context/BehatContext.phpB 9QB d>src/Behat/Behat/Context/ClassGuesser/ClassGuesserInterface.php9Qz?src/Behat/Behat/Context/ClassGuesser/PredefinedClassGuesser.php9QCJ4src/Behat/Behat/Context/ClosuredContextInterface.php9Qm-src/Behat/Behat/Context/ContextDispatcher.phpI9QI i,src/Behat/Behat/Context/ContextInterface.php|9Q|[)src/Behat/Behat/Context/ContextReader.php9Q4src/Behat/Behat/Context/ExtendedContextInterface.php9Qsrc/Behat/Behat/Definition/Loader/ClosuredDefinitionLoader.php 9Q 7Ķ?src/Behat/Behat/Definition/Loader/DefinitionLoaderInterface.php$9Q$GRCsrc/Behat/Behat/Definition/Proposal/AnnotatedDefinitionProposal.phpS9QS0Bsrc/Behat/Behat/Definition/Proposal/ClosuredDefinitionProposal.php 9Q jOEDsrc/Behat/Behat/Definition/Proposal/DefinitionProposalDispatcher.php9QlsICsrc/Behat/Behat/Definition/Proposal/DefinitionProposalInterface.php9Q96src/Behat/Behat/Definition/TransformationInterface.php9Qͽ}6src/Behat/Behat/DependencyInjection/BehatExtension.phpa'9Qa'˶Fsrc/Behat/Behat/DependencyInjection/Compiler/ConsoleProcessorsPass.php9QEMIsrc/Behat/Behat/DependencyInjection/Compiler/ContextClassGuessersPass.php9QE sHsrc/Behat/Behat/DependencyInjection/Compiler/ContextInitializersPass.php9QBCsrc/Behat/Behat/DependencyInjection/Compiler/ContextLoadersPass.php9Q0Hsrc/Behat/Behat/DependencyInjection/Compiler/DefinitionProposalsPass.php9Q┶Esrc/Behat/Behat/DependencyInjection/Compiler/EventSubscribersPass.php9Q[?src/Behat/Behat/DependencyInjection/Compiler/FormattersPass.php9Q3ACsrc/Behat/Behat/DependencyInjection/Compiler/GherkinLoadersPass.php9Qv[ܶ4src/Behat/Behat/DependencyInjection/config/behat.xmlK9QK Csrc/Behat/Behat/DependencyInjection/Configuration/Configuration.phpu9Quh\w<src/Behat/Behat/DependencyInjection/Configuration/Loader.php/9Q/st7F)src/Behat/Behat/Event/BackgroundEvent.php9Q"+src/Behat/Behat/Event/BaseScenarioEvent.php9Q7\(src/Behat/Behat/Event/EventInterface.php|9Q|CWr&src/Behat/Behat/Event/FeatureEvent.php9Qݴ&src/Behat/Behat/Event/OutlineEvent.phpl9Ql0x-src/Behat/Behat/Event/OutlineExampleEvent.php9Qwz'src/Behat/Behat/Event/ScenarioEvent.phpn9Qn#src/Behat/Behat/Event/StepEvent.php9QP<'Ƕ$src/Behat/Behat/Event/SuiteEvent.php9Qʼ0src/Behat/Behat/Exception/AmbiguousException.php9Q0p/src/Behat/Behat/Exception/BehaviorException.php9QVwٶ,src/Behat/Behat/Exception/ErrorException.php9Q5'src/Behat/Behat/Exception/Exception.php9Qũ0src/Behat/Behat/Exception/FormatterException.php9Q씟.src/Behat/Behat/Exception/PendingException.php9Q<ߨ0src/Behat/Behat/Exception/RedundantException.php9QOݶ0src/Behat/Behat/Exception/UndefinedException.php9Q\'src/Behat/Behat/Extension/Extension.php 9Q gJ0src/Behat/Behat/Extension/ExtensionInterface.phpj9Qj߼KM.src/Behat/Behat/Extension/ExtensionManager.php 9Q 9.src/Behat/Behat/Formatter/ConsoleFormatter.php 9Q 孿6src/Behat/Behat/Formatter/FailedScenariosFormatter.phpp 9Qp 71src/Behat/Behat/Formatter/FormatterDispatcher.php9QPƶ0src/Behat/Behat/Formatter/FormatterInterface.php9Q M.src/Behat/Behat/Formatter/FormatterManager.phpV9QVJ+src/Behat/Behat/Formatter/HtmlFormatter.php;q9Q;qRQ,src/Behat/Behat/Formatter/JUnitFormatter.php9Q("-src/Behat/Behat/Formatter/PrettyFormatter.phpMp9QMpة^/src/Behat/Behat/Formatter/ProgressFormatter.php%59Q%5./src/Behat/Behat/Formatter/SnippetsFormatter.php9QZ5src/Behat/Behat/Gherkin/Loader/FeatureSuiteLoader.php9Q2src/Behat/Behat/HelpPrinter/DefinitionsPrinter.php@ 9Q@ fu(2src/Behat/Behat/HelpPrinter/StorySyntaxPrinter.php-9Q-00src/Behat/Behat/Hook/Annotation/AfterFeature.php 9Q g]vڶ1src/Behat/Behat/Hook/Annotation/AfterScenario.php 9Q -)-src/Behat/Behat/Hook/Annotation/AfterStep.php9Q014.src/Behat/Behat/Hook/Annotation/AfterSuite.php9Q!϶1src/Behat/Behat/Hook/Annotation/BeforeFeature.php 9Q rw2src/Behat/Behat/Hook/Annotation/BeforeScenario.php9Q@.src/Behat/Behat/Hook/Annotation/BeforeStep.php9Q/YU#/src/Behat/Behat/Hook/Annotation/BeforeSuite.php9Q ɮ/src/Behat/Behat/Hook/Annotation/FeatureHook.php9Qk2src/Behat/Behat/Hook/Annotation/FilterableHook.phpx9Qx@ɶ(src/Behat/Behat/Hook/Annotation/Hook.php9Q u70src/Behat/Behat/Hook/Annotation/ScenarioHook.php@9Q@r,src/Behat/Behat/Hook/Annotation/StepHook.php9Qݶ-src/Behat/Behat/Hook/Annotation/SuiteHook.php9Q'-'src/Behat/Behat/Hook/HookDispatcher.phpT9QTr輶&src/Behat/Behat/Hook/HookInterface.php9QC Bp2src/Behat/Behat/Hook/Loader/ClosuredHookLoader.php9QWcWX3src/Behat/Behat/Hook/Loader/HookLoaderInterface.php9Qu+src/Behat/Behat/Tester/BackgroundTester.php] 9Q] ƚx(src/Behat/Behat/Tester/FeatureTester.phpL 9QL $(src/Behat/Behat/Tester/OutlineTester.phpN 9QN )src/Behat/Behat/Tester/ScenarioTester.php 9Q :F%src/Behat/Behat/Tester/StepTester.php9Q,9!src/Behat/Behat/Util/data/x00.php!9Q!4ei!src/Behat/Behat/Util/data/x01.phpL9QLCy!src/Behat/Behat/Util/data/x02.php9Qn!src/Behat/Behat/Util/data/x03.phpd9Qd!src/Behat/Behat/Util/data/x04.php9QE*!src/Behat/Behat/Util/data/x05.php9Q1!src/Behat/Behat/Util/data/x06.php9Q$!src/Behat/Behat/Util/data/x07.php?9Q?E"Ķ!src/Behat/Behat/Util/data/x09.php(9Q(A'!src/Behat/Behat/Util/data/x0a.php 9Q !src/Behat/Behat/Util/data/x0b.php>9Q>hfB !src/Behat/Behat/Util/data/x0c.phpJ9QJ S2S!src/Behat/Behat/Util/data/x0d.php.9Q.C8G!src/Behat/Behat/Util/data/x0e.phpA9QAʶ!src/Behat/Behat/Util/data/x0f.php#9Q#*A!src/Behat/Behat/Util/data/x10.php9Q03!src/Behat/Behat/Util/data/x11.php19Q1!src/Behat/Behat/Util/data/x12.phpO9QOB+!src/Behat/Behat/Util/data/x13.php9QΒ !src/Behat/Behat/Util/data/x14.php9QΪͶ!src/Behat/Behat/Util/data/x15.php9Q{m!src/Behat/Behat/Util/data/x16.php9QE'!src/Behat/Behat/Util/data/x17.phpT9QTr'!src/Behat/Behat/Util/data/x18.php9QdB!src/Behat/Behat/Util/data/x1e.php*9Q*q!src/Behat/Behat/Util/data/x1f.php(9Q(T!src/Behat/Behat/Util/data/x20.php(9Q(.!src/Behat/Behat/Util/data/x21.php(9Q(n!src/Behat/Behat/Util/data/x22.php(9Q(#!src/Behat/Behat/Util/data/x23.php(9Q(m!src/Behat/Behat/Util/data/x24.php69Q6r[v!src/Behat/Behat/Util/data/x25.php\9Q\qK!src/Behat/Behat/Util/data/x26.php9Qc}!src/Behat/Behat/Util/data/x27.php9QJ1c!src/Behat/Behat/Util/data/x28.php"9Q"$!src/Behat/Behat/Util/data/x2e.php"9Q"N`(!src/Behat/Behat/Util/data/x2f.php9QcV`!src/Behat/Behat/Util/data/x30.phpG9QG.!src/Behat/Behat/Util/data/x31.php9Q>CpB!src/Behat/Behat/Util/data/x32.php9Q,Q!src/Behat/Behat/Util/data/x33.php9Q^䦶!src/Behat/Behat/Util/data/x4d.php(9Q(t5Z!src/Behat/Behat/Util/data/x4e.php(9Q(2;!src/Behat/Behat/Util/data/x4f.php(9Q(!src/Behat/Behat/Util/data/x50.php(9Q(z!src/Behat/Behat/Util/data/x51.php(9Q(E!src/Behat/Behat/Util/data/x52.php9Q kӶ!src/Behat/Behat/Util/data/x53.php9Q !!src/Behat/Behat/Util/data/x54.php9QÏ|!src/Behat/Behat/Util/data/x55.phpE9QEMݶ!src/Behat/Behat/Util/data/x56.php9QpݦC!src/Behat/Behat/Util/data/x57.php9Q3}!src/Behat/Behat/Util/data/x58.php(9Q(}!src/Behat/Behat/Util/data/x59.php(9Q( tӶ!src/Behat/Behat/Util/data/x5a.php(9Q(!#h!src/Behat/Behat/Util/data/x5b.php(9Q(ӗmA!src/Behat/Behat/Util/data/x5c.php(9Q(B!src/Behat/Behat/Util/data/x5d.php(9Q(7!src/Behat/Behat/Util/data/x5e.php(9Q(o!src/Behat/Behat/Util/data/x5f.php(9Q(T\!src/Behat/Behat/Util/data/x60.php(9Q(P!]!src/Behat/Behat/Util/data/x61.php(9Q(f!src/Behat/Behat/Util/data/x62.php(9Q(3ڶ!src/Behat/Behat/Util/data/x63.php(9Q(t!src/Behat/Behat/Util/data/x64.php(9Q(m?!src/Behat/Behat/Util/data/x65.php(9Q(FW'!src/Behat/Behat/Util/data/x66.php(9Q(H!src/Behat/Behat/Util/data/x67.php(9Q(%!src/Behat/Behat/Util/data/x68.php(9Q(.!src/Behat/Behat/Util/data/x69.php(9Q(/u!src/Behat/Behat/Util/data/x6a.php(9Q(x?;!src/Behat/Behat/Util/data/x6b.php(9Q(W!src/Behat/Behat/Util/data/x6c.php(9Q(]!src/Behat/Behat/Util/data/x6d.php(9Q(fA!src/Behat/Behat/Util/data/x6e.php(9Q("4!src/Behat/Behat/Util/data/x6f.php(9Q(Ѐƶ!src/Behat/Behat/Util/data/x70.php(9Q(ڶ!src/Behat/Behat/Util/data/x71.php9Qfuy!src/Behat/Behat/Util/data/x72.phpD9QDHDv۶!src/Behat/Behat/Util/data/x73.php9Q !src/Behat/Behat/Util/data/x74.phpg9Qgk!src/Behat/Behat/Util/data/x75.phpj9QjS<!src/Behat/Behat/Util/data/x76.phpK9QK~!src/Behat/Behat/Util/data/x77.php%9Q%s!src/Behat/Behat/Util/data/x78.php9Q-!src/Behat/Behat/Util/data/x79.php9Q!src/Behat/Behat/Util/data/x7a.php$9Q$oR!src/Behat/Behat/Util/data/x7b.php49Q4Աl!src/Behat/Behat/Util/data/x7c.phpc9Qce6!src/Behat/Behat/Util/data/x7d.phpA9QAk!src/Behat/Behat/Util/data/x7e.php99Q9 !src/Behat/Behat/Util/data/x7f.phpY9QY!src/Behat/Behat/Util/data/x80.php!9Q!%!src/Behat/Behat/Util/data/x81.php[9Q[c8!src/Behat/Behat/Util/data/x82.phpY9QYPK!src/Behat/Behat/Util/data/x83.phpQ9QQ!src/Behat/Behat/Util/data/x84.php?9Q?C!src/Behat/Behat/Util/data/x85.phpS9QS;J!src/Behat/Behat/Util/data/x86.php)9Q)^s|!src/Behat/Behat/Util/data/x87.phpM9QML!src/Behat/Behat/Util/data/x88.phpL9QL !src/Behat/Behat/Util/data/x89.php;9Q;t!src/Behat/Behat/Util/data/x8a.phpb9QbR!src/Behat/Behat/Util/data/x8b.php89Q8(!src/Behat/Behat/Util/data/x8c.php_9Q_P׶!src/Behat/Behat/Util/data/x8d.php^9Q^(M!src/Behat/Behat/Util/data/x8e.php_9Q_rǬ!src/Behat/Behat/Util/data/x8f.php9Q !src/Behat/Behat/Util/data/x90.php@9Q@ҋ'ض!src/Behat/Behat/Util/data/x91.phpH9QHr!src/Behat/Behat/Util/data/x92.php=9Q=Z!src/Behat/Behat/Util/data/x93.phpG9QG )!src/Behat/Behat/Util/data/x94.phpr9Qru;pն!src/Behat/Behat/Util/data/x95.php[9Q[UM톶!src/Behat/Behat/Util/data/x96.phpP9QPjc!src/Behat/Behat/Util/data/x97.phpC9QC%'eF!src/Behat/Behat/Util/data/x98.phpu9Qum !src/Behat/Behat/Util/data/x99.php`9Q`d_!src/Behat/Behat/Util/data/x9a.php<9Q<a8!src/Behat/Behat/Util/data/x9b.php`9Q`'!src/Behat/Behat/Util/data/x9c.phpE9QEv!src/Behat/Behat/Util/data/x9d.php9QeZ!src/Behat/Behat/Util/data/x9e.phpZ9QZ!src/Behat/Behat/Util/data/x9f.phpZ9QZ K!src/Behat/Behat/Util/data/xa0.phpT9QT[/!src/Behat/Behat/Util/data/xa1.phpc9Qc G!src/Behat/Behat/Util/data/xa2.phpg9Qgqw!src/Behat/Behat/Util/data/xa3.phpU9QUՃ!src/Behat/Behat/Util/data/xa4.phpH9QHǮ:!src/Behat/Behat/Util/data/xac.php^9Q^a!src/Behat/Behat/Util/data/xad.phpF9QFnGl!src/Behat/Behat/Util/data/xae.php@9Q@`4p<!src/Behat/Behat/Util/data/xaf.phpC9QCY*!src/Behat/Behat/Util/data/xb0.php99Q9+!src/Behat/Behat/Util/data/xb1.php#9Q#!src/Behat/Behat/Util/data/xb2.phpF9QFvh!src/Behat/Behat/Util/data/xb3.phpB9QB:W!src/Behat/Behat/Util/data/xb4.php19Q1n!src/Behat/Behat/Util/data/xb5.phpD9QDh!src/Behat/Behat/Util/data/xb6.php@9Q@/!src/Behat/Behat/Util/data/xb7.php39Q3U!src/Behat/Behat/Util/data/xb8.php99Q9Hs!src/Behat/Behat/Util/data/xb9.phpP9QP7u!src/Behat/Behat/Util/data/xba.phpH9QH3' !src/Behat/Behat/Util/data/xbb.php49Q4<!src/Behat/Behat/Util/data/xbc.phpL9QLy+!src/Behat/Behat/Util/data/xbd.phpE9QE!src/Behat/Behat/Util/data/xbe.phpW9QW5q@!src/Behat/Behat/Util/data/xbf.phpR9QR2SÑ!src/Behat/Behat/Util/data/xc0.php\9Q\>`N!src/Behat/Behat/Util/data/xc1.php9QX'!src/Behat/Behat/Util/data/xc2.php@9Q@!src/Behat/Behat/Util/data/xc6.phpL9QL;~!src/Behat/Behat/Util/data/xc7.phpP9QPƶ!src/Behat/Behat/Util/data/xc8.php59Q5:!src/Behat/Behat/Util/data/xc9.php$9Q$|!src/Behat/Behat/Util/data/xca.php9Q|&!src/Behat/Behat/Util/data/xcb.phpi9Qi8 !src/Behat/Behat/Util/data/xcc.php9QZ!src/Behat/Behat/Util/data/xcd.php9QUw!src/Behat/Behat/Util/data/xce.php9Q!t!src/Behat/Behat/Util/data/xcf.php}9Q};*p!src/Behat/Behat/Util/data/xd0.php(9Q(M%ö!src/Behat/Behat/Util/data/xd1.php(9Q(Mm!src/Behat/Behat/Util/data/xd2.php(9Q(}hD!src/Behat/Behat/Util/data/xd3.php(9Q(!src/Behat/Behat/Util/data/xd4.php(9Q(!src/Behat/Behat/Util/data/xd5.php(9Q(|!src/Behat/Behat/Util/data/xd6.php(9Q($!src/Behat/Behat/Util/data/xd7.php9Q+*L!src/Behat/Behat/Util/data/xf9.php9Q !src/Behat/Behat/Util/data/xfa.php(9Q(a[!src/Behat/Behat/Util/data/xfb.php9QS!src/Behat/Behat/Util/data/xfc.php9Qگ!src/Behat/Behat/Util/data/xfd.php9Qh=µ!src/Behat/Behat/Util/data/xfe.phpe9Qe_ !src/Behat/Behat/Util/data/xff.php9QY?src/Behat/Behat/Util/LICENSE9Q{U^j'src/Behat/Behat/Util/Transliterator.phpK9QKֶvendor/autoload.php9Q!vendor/behat/gherkin/autoload.php09Q00gyvendor/behat/gherkin/i18n.phpR9QR2  vendor/behat/gherkin/libpath.php9QtrnǶvendor/behat/gherkin/LICENSEC9QC<?vendor/behat/gherkin/src/Behat/Gherkin/Cache/CacheInterface.php>9Q>/:vendor/behat/gherkin/src/Behat/Gherkin/Cache/FileCache.php9Q?3?vendor/behat/gherkin/src/Behat/Gherkin/Dumper/GherkinDumper.php9Q}t>vendor/behat/gherkin/src/Behat/Gherkin/Exception/Exception.php=9Q= Cvendor/behat/gherkin/src/Behat/Gherkin/Exception/LexerException.phpA9QAZNDvendor/behat/gherkin/src/Behat/Gherkin/Exception/ParserException.phpB9QB6ֶAvendor/behat/gherkin/src/Behat/Gherkin/Filter/FilterInterface.phpE9QE!4p;<vendor/behat/gherkin/src/Behat/Gherkin/Filter/LineFilter.php 9Q .-Avendor/behat/gherkin/src/Behat/Gherkin/Filter/LineRangeFilter.php 9Q N۱<vendor/behat/gherkin/src/Behat/Gherkin/Filter/NameFilter.php?9Q?#b>vendor/behat/gherkin/src/Behat/Gherkin/Filter/SimpleFilter.php9Q;vendor/behat/gherkin/src/Behat/Gherkin/Filter/TagFilter.php9QI2vendor/behat/gherkin/src/Behat/Gherkin/Gherkin.phph9QhLAvendor/behat/gherkin/src/Behat/Gherkin/Keywords/ArrayKeywords.php9QOGvendor/behat/gherkin/src/Behat/Gherkin/Keywords/CachedArrayKeywords.php9Q-,=Dvendor/behat/gherkin/src/Behat/Gherkin/Keywords/CucumberKeywords.php9QBEBvendor/behat/gherkin/src/Behat/Gherkin/Keywords/KeywordsDumper.phpY"9QY"iHEvendor/behat/gherkin/src/Behat/Gherkin/Keywords/KeywordsInterface.php9QؿNvendor/behat/gherkin/src/Behat/Gherkin/Keywords/SymfonyTranslationKeywords.php9Q0vendor/behat/gherkin/src/Behat/Gherkin/Lexer.phpm49Qm4lkDvendor/behat/gherkin/src/Behat/Gherkin/Loader/AbstractFileLoader.php9Q# =vendor/behat/gherkin/src/Behat/Gherkin/Loader/ArrayLoader.php9Q ֶAvendor/behat/gherkin/src/Behat/Gherkin/Loader/DirectoryLoader.php9Q<Evendor/behat/gherkin/src/Behat/Gherkin/Loader/FileLoaderInterface.php49Q4/5ɶCvendor/behat/gherkin/src/Behat/Gherkin/Loader/GherkinFileLoader.php 9Q Avendor/behat/gherkin/src/Behat/Gherkin/Loader/LoaderInterface.php 9Q ﳶ@vendor/behat/gherkin/src/Behat/Gherkin/Loader/YamlFileLoader.php 9Q <vendor/behat/gherkin/src/Behat/Gherkin/Node/AbstractNode.php9QݶDvendor/behat/gherkin/src/Behat/Gherkin/Node/AbstractScenarioNode.php9Q>vendor/behat/gherkin/src/Behat/Gherkin/Node/BackgroundNode.php9QȮKRCvendor/behat/gherkin/src/Behat/Gherkin/Node/ExamplePyStringNode.php9Qtֶ?vendor/behat/gherkin/src/Behat/Gherkin/Node/ExampleStepNode.php9QKF@vendor/behat/gherkin/src/Behat/Gherkin/Node/ExampleTableNode.php79Q78P;vendor/behat/gherkin/src/Behat/Gherkin/Node/FeatureNode.phpX9QX^Dvendor/behat/gherkin/src/Behat/Gherkin/Node/NodeVisitorInterface.phpF9QF;vendor/behat/gherkin/src/Behat/Gherkin/Node/OutlineNode.php9Q1<vendor/behat/gherkin/src/Behat/Gherkin/Node/PyStringNode.phpz9Qz2<vendor/behat/gherkin/src/Behat/Gherkin/Node/ScenarioNode.php9Q:WڃIvendor/behat/gherkin/src/Behat/Gherkin/Node/StepArgumentNodeInterface.phpv9Qv(b,J8vendor/behat/gherkin/src/Behat/Gherkin/Node/StepNode.php9Q;Q9vendor/behat/gherkin/src/Behat/Gherkin/Node/TableNode.php9Qz񹃶1vendor/behat/gherkin/src/Behat/Gherkin/Parser.php49Q4y%vendor/composer/autoload_classmap.php9Q^V'vendor/composer/autoload_namespaces.php9Q>!vendor/composer/autoload_real.php9QTvendor/composer/ClassLoader.php9Q;yi>vendor/symfony/config/Symfony/Component/Config/ConfigCache.php 9Q %-Gvendor/symfony/config/Symfony/Component/Config/Definition/ArrayNode.php+9Q+<Fvendor/symfony/config/Symfony/Component/Config/Definition/BaseNode.php!9Q!H2۶Ivendor/symfony/config/Symfony/Component/Config/Definition/BooleanNode.php9QYvendor/symfony/config/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php99Q9mO,r[vendor/symfony/config/Symfony/Component/Config/Definition/Builder/BooleanNodeDefinition.php9QPXvendor/symfony/config/Symfony/Component/Config/Definition/Builder/EnumNodeDefinition.php9Q\X< Qvendor/symfony/config/Symfony/Component/Config/Definition/Builder/ExprBuilder.php9QYvendor/symfony/config/Symfony/Component/Config/Definition/Builder/FloatNodeDefinition.php9Qtp[vendor/symfony/config/Symfony/Component/Config/Definition/Builder/IntegerNodeDefinition.php9Q'nڭRvendor/symfony/config/Symfony/Component/Config/Definition/Builder/MergeBuilder.php9Q/_lQvendor/symfony/config/Symfony/Component/Config/Definition/Builder/NodeBuilder.php9Q }Tvendor/symfony/config/Symfony/Component/Config/Definition/Builder/NodeDefinition.phpR9QR .!UYvendor/symfony/config/Symfony/Component/Config/Definition/Builder/NodeParentInterface.php9QU=Zvendor/symfony/config/Symfony/Component/Config/Definition/Builder/NormalizationBuilder.php9Q[vendor/symfony/config/Symfony/Component/Config/Definition/Builder/NumericNodeDefinition.phpX9QX~Gcvendor/symfony/config/Symfony/Component/Config/Definition/Builder/ParentNodeDefinitionInterface.php_9Q_3dZvendor/symfony/config/Symfony/Component/Config/Definition/Builder/ScalarNodeDefinition.php9QmcQvendor/symfony/config/Symfony/Component/Config/Definition/Builder/TreeBuilder.php9QVhUWvendor/symfony/config/Symfony/Component/Config/Definition/Builder/ValidationBuilder.phpr9QrӶ\vendor/symfony/config/Symfony/Component/Config/Definition/Builder/VariableNodeDefinition.php9QSTvendor/symfony/config/Symfony/Component/Config/Definition/ConfigurationInterface.phpd9Qd*Fvendor/symfony/config/Symfony/Component/Config/Definition/EnumNode.phpY9QY"5߶]vendor/symfony/config/Symfony/Component/Config/Definition/Exception/DuplicateKeyException.phpE9QE|Qvendor/symfony/config/Symfony/Component/Config/Definition/Exception/Exception.php9QCжcvendor/symfony/config/Symfony/Component/Config/Definition/Exception/ForbiddenOverwriteException.phpQ9QQ:2&evendor/symfony/config/Symfony/Component/Config/Definition/Exception/InvalidConfigurationException.php9Q5-bvendor/symfony/config/Symfony/Component/Config/Definition/Exception/InvalidDefinitionException.php9Q]F4\vendor/symfony/config/Symfony/Component/Config/Definition/Exception/InvalidTypeException.php9Q@Yvendor/symfony/config/Symfony/Component/Config/Definition/Exception/UnsetKeyException.php9QGvendor/symfony/config/Symfony/Component/Config/Definition/FloatNode.php9Qm+Ivendor/symfony/config/Symfony/Component/Config/Definition/IntegerNode.phpk9QkjKvendor/symfony/config/Symfony/Component/Config/Definition/NodeInterface.php9QfJIvendor/symfony/config/Symfony/Component/Config/Definition/NumericNode.php9Qx^*Gvendor/symfony/config/Symfony/Component/Config/Definition/Processor.php' 9Q' UQvendor/symfony/config/Symfony/Component/Config/Definition/PrototypedArrayNode.phpo(9Qo(Tvendor/symfony/config/Symfony/Component/Config/Definition/PrototypeNodeInterface.phpw9QweMvendor/symfony/config/Symfony/Component/Config/Definition/ReferenceDumper.php9QHLHvendor/symfony/config/Symfony/Component/Config/Definition/ScalarNode.phpq9QqX Jvendor/symfony/config/Symfony/Component/Config/Definition/VariableNode.php 9Q $¶gvendor/symfony/config/Symfony/Component/Config/Exception/FileLoaderImportCircularReferenceException.phpT9QT7 Tvendor/symfony/config/Symfony/Component/Config/Exception/FileLoaderLoadException.php) 9Q) Gȶ>vendor/symfony/config/Symfony/Component/Config/FileLocator.php 9Q IGvendor/symfony/config/Symfony/Component/Config/FileLocatorInterface.phpl9QlC3k6vendor/symfony/config/Symfony/Component/Config/LICENSE)9Q)NmQJvendor/symfony/config/Symfony/Component/Config/Loader/DelegatingLoader.php89Q8;v7%Dvendor/symfony/config/Symfony/Component/Config/Loader/FileLoader.php 9Q R߶@vendor/symfony/config/Symfony/Component/Config/Loader/Loader.php9QȶIvendor/symfony/config/Symfony/Component/Config/Loader/LoaderInterface.php99Q94Hvendor/symfony/config/Symfony/Component/Config/Loader/LoaderResolver.php9QrhqQvendor/symfony/config/Symfony/Component/Config/Loader/LoaderResolverInterface.php9Q¶Mvendor/symfony/config/Symfony/Component/Config/Resource/DirectoryResource.php 9Q  #Hvendor/symfony/config/Symfony/Component/Config/Resource/FileResource.php!9Q!TMvendor/symfony/config/Symfony/Component/Config/Resource/ResourceInterface.phpD9QDfKE@vendor/symfony/config/Symfony/Component/Config/Util/XmlUtils.php9QKM@vendor/symfony/console/Symfony/Component/Console/Application.php,9Q,-jDvendor/symfony/console/Symfony/Component/Console/Command/Command.phpC9QC}eHvendor/symfony/console/Symfony/Component/Console/Command/HelpCommand.php9Q#DHvendor/symfony/console/Symfony/Component/Console/Command/ListCommand.php 9Q LNvendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatter.php9QL̶Wvendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterInterface.php9QJp˶Svendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyle.php 9Q Ʃ\vendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleInterface.php9Q5Xvendor/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php 9Q dBHvendor/symfony/console/Symfony/Component/Console/Helper/DialogHelper.php<9Q<.@Kvendor/symfony/console/Symfony/Component/Console/Helper/FormatterHelper.php 9Q Bvendor/symfony/console/Symfony/Component/Console/Helper/Helper.php9QDqӶKvendor/symfony/console/Symfony/Component/Console/Helper/HelperInterface.php9Qz׶Evendor/symfony/console/Symfony/Component/Console/Helper/HelperSet.phpv 9Qv Jvendor/symfony/console/Symfony/Component/Console/Helper/ProgressHelper.php *9Q *'Dvendor/symfony/console/Symfony/Component/Console/Input/ArgvInput.phpb%9Qb%C6Evendor/symfony/console/Symfony/Component/Console/Input/ArrayInput.php9Q`%F@vendor/symfony/console/Symfony/Component/Console/Input/Input.php9QH%~Hvendor/symfony/console/Symfony/Component/Console/Input/InputArgument.php 9Q 'Jvendor/symfony/console/Symfony/Component/Console/Input/InputDefinition.phpA9QAvPIvendor/symfony/console/Symfony/Component/Console/Input/InputInterface.php&9Q&o3޶Fvendor/symfony/console/Symfony/Component/Console/Input/InputOption.phpO9QOz/lFvendor/symfony/console/Symfony/Component/Console/Input/StringInput.phph 9Qh )j8vendor/symfony/console/Symfony/Component/Console/LICENSE)9Q)NmQIvendor/symfony/console/Symfony/Component/Console/Output/ConsoleOutput.php^ 9Q^ ,Rvendor/symfony/console/Symfony/Component/Console/Output/ConsoleOutputInterface.php9QFvendor/symfony/console/Symfony/Component/Console/Output/NullOutput.php9QdaBvendor/symfony/console/Symfony/Component/Console/Output/Output.php9QA9Kvendor/symfony/console/Symfony/Component/Console/Output/OutputInterface.phpD 9QD :Hvendor/symfony/console/Symfony/Component/Console/Output/StreamOutput.phph 9Qh jU:vendor/symfony/console/Symfony/Component/Console/Shell.php9QBaMvendor/symfony/console/Symfony/Component/Console/Tester/ApplicationTester.phpp 9Qp Ivendor/symfony/console/Symfony/Component/Console/Tester/CommandTester.php3 9Q3 `8Svendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Alias.php9Ql5svendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php9QۻǶrvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/CheckCircularReferencesPass.php:9Q:ׄIrvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php0 9Q0 {vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php69Q6b[_qvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/CheckReferenceValidityPass.php9Qd>J_vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/Compiler.php 9Q Flvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/CompilerPassInterface.php9Q_9svendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php9QPS~gvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php9Qvvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/MergeExtensionConfigurationPass.php\9Q\Xavendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/PassConfig.php 9Q 2btvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/RemoveAbstractDefinitionsPass.php'9Q'U*ovendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/RemovePrivateAliasesPass.php9Q!?rvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/RemoveUnusedDefinitionsPass.php 9Q iZnvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/RepeatablePassInterface.php9QEŶcvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/RepeatedPass.php#9Q#0єyvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ReplaceAliasByActualDefinitionPass.phpF9QFL׶uvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php9Q\vIsvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php9Q*jwvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.phpn9Qnuvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.phpP 9QP lvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraph.php 9Q ˮpvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphEdge.php9Qx7۶pvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Compiler/ServiceReferenceGraphNode.php" 9Q" \jWvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Container.php99Q9o?\vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ContainerAware.phpi9Qiuevendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ContainerAwareInterface.php9QFL^vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ContainerBuilder.phpj9QjEK`vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ContainerInterface.php9QE?Xvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Definition.php59Q5Xavendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/DefinitionDecorator.php9Q[vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Dumper/Dumper.php+9Q+N/dvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Dumper/DumperInterface.php9Q}cvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php%9Q%^vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php9Q^oR^vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php&9Q&{_vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php 9Q Onvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/BadMethodCallException.php9Q O,jvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/ExceptionInterface.php9Q9nvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/InactiveScopeException.php9Q*pvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/InvalidArgumentException.php9Qfvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/LogicException.php9Ql.lvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/OutOfBoundsException.php9Q{{vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/ParameterCircularReferenceException.phpu9Quorvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/ParameterNotFoundException.php(9Q(gnIhvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/RuntimeException.php9Q +wvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/ScopeCrossingInjectionException.phpz9Qz{Vwvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/ScopeWideningInjectionException.phpl9Qlxyvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/ServiceCircularReferenceException.php9Qpvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Exception/ServiceNotFoundException.phpX9QXDwvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Extension/ConfigurationExtensionInterface.php9QJavendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Extension/Extension.php9QQXjvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php9QJ0bĶqvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Extension/PrependExtensionInterface.phpM9QMConvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/IntrospectableContainerInterface.phpB9QBBtQvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/LICENSE)9Q)NmQbvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Loader/ClosureLoader.php9Q _vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Loader/FileLoader.php-9Q-Uabvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Loader/IniFileLoader.php9QӸBjbvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php&9Q&OOuvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd9Qqw闶bvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.phpb79Qb7tacvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php4'9Q4'2 Wvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Parameter.php9QF"mvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ParameterBag/FrozenParameterBag.php9Qךgvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ParameterBag/ParameterBag.php9QJ_:pvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ParameterBag/ParameterBagInterface.php 9Q ܮP8Wvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Reference.php59Q5y&(Svendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Scope.phpm9Qm,x\vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/ScopeInterface.php9Q_J^vendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/SimpleXMLElement.php 9Q ζfvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/TaggedContainerInterface.php9QVvendor/symfony/dependency-injection/Symfony/Component/DependencyInjection/Variable.php79Q7$cvendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php9QNmmvendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcherInterface.php9Q Kvendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/Event.php 9Q '~Uvendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php9Qώ^vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcherInterface.php 9Q UҶ^vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventSubscriberInterface.php19Q1cGRvendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/GenericEvent.phpO9QO?L^vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/ImmutableEventDispatcher.php9Qs Ivendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/LICENSE)9Q)NmQJvendor/symfony/finder/Symfony/Component/Finder/Adapter/AbstractAdapter.php9QMxNvendor/symfony/finder/Symfony/Component/Finder/Adapter/AbstractFindAdapter.php(9Q(xҶKvendor/symfony/finder/Symfony/Component/Finder/Adapter/AdapterInterface.php 9Q &Ivendor/symfony/finder/Symfony/Component/Finder/Adapter/BsdFindAdapter.php 9Q 2: xIvendor/symfony/finder/Symfony/Component/Finder/Adapter/GnuFindAdapter.php 9Q ٶEvendor/symfony/finder/Symfony/Component/Finder/Adapter/PhpAdapter.phpV 9QV 8yFHvendor/symfony/finder/Symfony/Component/Finder/Comparator/Comparator.php 9Q |FzLvendor/symfony/finder/Symfony/Component/Finder/Comparator/DateComparator.php9Q Nvendor/symfony/finder/Symfony/Component/Finder/Comparator/NumberComparator.php 9Q PTvendor/symfony/finder/Symfony/Component/Finder/Exception/AdapterFailureException.php9Q>@Ovendor/symfony/finder/Symfony/Component/Finder/Exception/ExceptionInterface.php9QeZvendor/symfony/finder/Symfony/Component/Finder/Exception/OperationNotPermitedException.php9QPYvendor/symfony/finder/Symfony/Component/Finder/Exception/ShellCommandFailureException.php99Q9Hvendor/symfony/finder/Symfony/Component/Finder/Expression/Expression.php$ 9Q$ Bvendor/symfony/finder/Symfony/Component/Finder/Expression/Glob.php9QECvendor/symfony/finder/Symfony/Component/Finder/Expression/Regex.php9Q[ǐLvendor/symfony/finder/Symfony/Component/Finder/Expression/ValueInterface.phpH9QH"9vendor/symfony/finder/Symfony/Component/Finder/Finder.phpT9QTx(&7vendor/symfony/finder/Symfony/Component/Finder/Glob.phpb 9Qb g@MPvendor/symfony/finder/Symfony/Component/Finder/Iterator/CustomFilterIterator.php9Qz"Svendor/symfony/finder/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php9Q UTvendor/symfony/finder/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php9QM1Zvendor/symfony/finder/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php9Q߶Uvendor/symfony/finder/Symfony/Component/Finder/Iterator/FilecontentFilterIterator.php9QʽRvendor/symfony/finder/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php9Q{^ǶMvendor/symfony/finder/Symfony/Component/Finder/Iterator/FilePathsIterator.php 9Q ɔRvendor/symfony/finder/Symfony/Component/Finder/Iterator/FileTypeFilterIterator.phpb9Qbn Jvendor/symfony/finder/Symfony/Component/Finder/Iterator/FilterIterator.phpr9QrC Vvendor/symfony/finder/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php*9Q*nNvendor/symfony/finder/Symfony/Component/Finder/Iterator/PathFilterIterator.php9Q)Vvendor/symfony/finder/Symfony/Component/Finder/Iterator/RecursiveDirectoryIterator.phpY9QYSvendor/symfony/finder/Symfony/Component/Finder/Iterator/SizeRangeFilterIterator.php9QxPLvendor/symfony/finder/Symfony/Component/Finder/Iterator/SortableIterator.php 9Q j6vendor/symfony/finder/Symfony/Component/Finder/LICENSE)9Q)NmQ@vendor/symfony/finder/Symfony/Component/Finder/Shell/Command.php9Ql >vendor/symfony/finder/Symfony/Component/Finder/Shell/Shell.php9Q2>vendor/symfony/finder/Symfony/Component/Finder/SplFileInfo.php9Qs{Svendor/symfony/translation/Symfony/Component/Translation/Dumper/XliffFileDumper.phpr9Qre Rvendor/symfony/translation/Symfony/Component/Translation/Dumper/YamlFileDumper.php69Q6Yvendor/symfony/translation/Symfony/Component/Translation/Exception/ExceptionInterface.php9Q6"_vendor/symfony/translation/Symfony/Component/Translation/Exception/InvalidResourceException.php9QDŽ`vendor/symfony/translation/Symfony/Component/Translation/Exception/NotFoundResourceException.php9Q2BUvendor/symfony/translation/Symfony/Component/Translation/Extractor/ChainExtractor.php9QYvendor/symfony/translation/Symfony/Component/Translation/Extractor/ExtractorInterface.php9QArPOvendor/symfony/translation/Symfony/Component/Translation/IdentityTranslator.php9QFYEvendor/symfony/translation/Symfony/Component/Translation/Interval.php 9Q g?Z@vendor/symfony/translation/Symfony/Component/Translation/LICENSE)9Q)NmQOvendor/symfony/translation/Symfony/Component/Translation/Loader/ArrayLoader.php9QRQvendor/symfony/translation/Symfony/Component/Translation/Loader/CsvFileLoader.php" 9Q" i3Tvendor/symfony/translation/Symfony/Component/Translation/Loader/IcuDatFileLoader.php9QTöTvendor/symfony/translation/Symfony/Component/Translation/Loader/IcuResFileLoader.phpz 9Qz CSQvendor/symfony/translation/Symfony/Component/Translation/Loader/IniFileLoader.php9QN@ Svendor/symfony/translation/Symfony/Component/Translation/Loader/LoaderInterface.php-9Q-Pvendor/symfony/translation/Symfony/Component/Translation/Loader/MoFileLoader.php9Q0Qvendor/symfony/translation/Symfony/Component/Translation/Loader/PhpFileLoader.php;9Q;rPvendor/symfony/translation/Symfony/Component/Translation/Loader/PoFileLoader.php9QʶPvendor/symfony/translation/Symfony/Component/Translation/Loader/QtFileLoader.php 9Q r;(Xvendor/symfony/translation/Symfony/Component/Translation/Loader/QtTranslationsLoader.php!9Q!iŶovendor/symfony/translation/Symfony/Component/Translation/Loader/schema/dic/xliff-core/xliff-core-1.2-strict.xsd9QxO]vendor/symfony/translation/Symfony/Component/Translation/Loader/schema/dic/xliff-core/xml.xsd"9Q"ҶSvendor/symfony/translation/Symfony/Component/Translation/Loader/XliffFileLoader.php<9Q<v|8Rvendor/symfony/translation/Symfony/Component/Translation/Loader/YamlFileLoader.php 9Q Q;Mvendor/symfony/translation/Symfony/Component/Translation/MessageCatalogue.php9QѭVvendor/symfony/translation/Symfony/Component/Translation/MessageCatalogueInterface.phpp9QpHLvendor/symfony/translation/Symfony/Component/Translation/MessageSelector.php+ 9Q+ Svendor/symfony/translation/Symfony/Component/Translation/MetadataAwareInterface.phpT9QT@Ovendor/symfony/translation/Symfony/Component/Translation/PluralizationRules.php9QwGvendor/symfony/translation/Symfony/Component/Translation/Translator.phpe9Qe~Pvendor/symfony/translation/Symfony/Component/Translation/TranslatorInterface.php9QEdUvendor/symfony/translation/Symfony/Component/Translation/Writer/TranslationWriter.php}9Q}P?5vendor/symfony/yaml/Symfony/Component/Yaml/Dumper.php 9Q 8n6vendor/symfony/yaml/Symfony/Component/Yaml/Escaper.phpi 9Qi Fvendor/symfony/yaml/Symfony/Component/Yaml/Exception/DumpException.php9Qؙ՚Kvendor/symfony/yaml/Symfony/Component/Yaml/Exception/ExceptionInterface.php9Q+lGvendor/symfony/yaml/Symfony/Component/Yaml/Exception/ParseException.php9 9Q9 ^[Ivendor/symfony/yaml/Symfony/Component/Yaml/Exception/RuntimeException.php9Q|-5vendor/symfony/yaml/Symfony/Component/Yaml/Inline.php>9Q>h2vendor/symfony/yaml/Symfony/Component/Yaml/LICENSE)9Q)NmQ5vendor/symfony/yaml/Symfony/Component/Yaml/Parser.phpX9QXH?8vendor/symfony/yaml/Symfony/Component/Yaml/Unescaper.php9Qn3vendor/symfony/yaml/Symfony/Component/Yaml/Yaml.php9Qͷ6LICENSEC9QC * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat annotation class. * * @author Konstantin Kudryashov */ abstract class Annotation implements AnnotationInterface { private $closure; private $callback; private $path; private $description; /** * Constructs annotation. * * @param callback $callback * * @throws \InvalidArgumentException */ public function __construct($callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException(sprintf( 'Annotation callback should be a valid callable, but %s given', gettype($callback) )); } $this->closure = !is_array($callback); if (!$this->isClosure()) { $this->path = $callback[0] . '::' . $callback[1] . '()'; } else { $reflection = new \ReflectionFunction($callback); $this->path = $reflection->getFileName() . ':' . $reflection->getStartLine(); } $this->callback = $callback; } /** * Sets annotation description. * * @param string $description */ public function setDescription($description) { $this->description = $description; } /** * Returns description. * * @return string */ public function getDescription() { return $this->description; } /** * Returns path string for callback. * * @return string */ public function getPath() { return $this->path; } /** * Checks whether callback is closure. * * @return Boolean */ public function isClosure() { return $this->closure; } /** * Returns callback. * * @return callback */ public function getCallback() { return $this->callback; } /** * Returns callback, mapped to specific context (or it's subcontext) instance. * * @param ContextInterface $context * * @return callback * * @throws \RuntimeException */ public function getCallbackForContext(ContextInterface $context) { $callback = $this->getCallback(); if (!$this->isClosure()) { if ($callback[0] === get_class($context)) { $callback = array($context, $callback[1]); } elseif ($context instanceof SubcontextableContextInterface) { $subcontext = $context->getSubcontextByClassName($callback[0]); if (null === $subcontext) { throw new \RuntimeException(sprintf( '"%s" subcontext instance not found in context "%s".'."\n". 'Looks like something is wrong with getSubcontextByClassName() method in one of your contexts', $callback[0], get_class($context) )); } $reflection = new \ReflectionClass($subcontext); if (!$reflection->hasMethod($callback[1])) { throw new \RuntimeException(sprintf( 'Subcontext "%s" does not have "%s()" method.'."\n". 'Looks like something is wrong with getSubcontextByClassName() method in one of your contexts', get_class($subcontext), $callback[1] )); } $callback = array($subcontext, $callback[1]); } } return $callback; } /** * Returns callback reflection. * * @return \ReflectionFunction */ public function getCallbackReflection() { if (!$this->isClosure()) { return new \ReflectionMethod($this->callback[0], $this->callback[1]); } else { return new \ReflectionFunction($this->callback); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat annotation interface. * * @author Konstantin Kudryashov */ interface AnnotationInterface { /** * Returns path string for callback. * * @return string */ public function getPath(); /** * Checks whether callback is closure. * * @return Boolean */ public function isClosure(); /** * Returns callback. * * @return callback */ public function getCallback(); /** * Returns callback reflection. * * @return \ReflectionFunction */ public function getCallbackReflection(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat console application. * * @author Konstantin Kudryashov */ class BehatApplication extends Application { private $basePath; /** * {@inheritdoc} */ public function __construct($version) { parent::__construct('Behat', $version); } /** * {@inheritdoc} */ public function getDefinition() { return new InputDefinition(array( new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Increase verbosity of exceptions.'), new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this behat version.'), new InputOption('--config', '-c', InputOption::VALUE_REQUIRED, 'Specify config file to use.'), new InputOption('--profile', '-p', InputOption::VALUE_REQUIRED, 'Specify config profile to use.') )); } /** * Runs the current application. * * @param InputInterface $input * @param OutputInterface $output * * @return integer 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output) { $this->add($this->createCommand($input)); return parent::doRun($input, $output); } /** * Creates main command for application. * * @param InputInterface $input * * @return Command */ protected function createCommand(InputInterface $input) { return $this->createContainer($input)->get('behat.console.command'); } /** * Creates container instance, loads extensions and freezes it. * * @param InputInterface $input * * @return ContainerInterface */ protected function createContainer(InputInterface $input) { $container = new ContainerBuilder(); $this->loadCoreExtension($container, $this->loadConfiguration($container, $input)); $container->compile(); return $container; } /** * Configures container based on providen config file and profile. * * @param ContainerBuilder $container * @param InputInterface $input */ protected function loadConfiguration(ContainerBuilder $container, InputInterface $input) { // locate paths $this->basePath = getcwd(); if ($configPath = $this->getConfigurationFilePath($input)) { $this->basePath = realpath(dirname($configPath)); } // read configuration $loader = new Loader($configPath); $profile = $input->getParameterOption(array('--profile', '-p')) ?: 'default'; return $loader->loadConfiguration($profile); } /** * Finds configuration file and returns path to it. * * @param InputInterface $input * * @return string */ protected function getConfigurationFilePath(InputInterface $input) { // custom configuration file if ($file = $input->getParameterOption(array('--config', '-c'))) { if (is_file($file)) { return $file; } return; } // predefined config paths $cwd = rtrim(getcwd(), DIRECTORY_SEPARATOR); foreach (array_filter(array( $cwd.DIRECTORY_SEPARATOR.'behat.yml', $cwd.DIRECTORY_SEPARATOR.'behat.yml.dist', $cwd.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'behat.yml', $cwd.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'behat.yml.dist', ), 'is_file') as $path) { return $path; } } /** * Loads core extension into container. * * @param ContainerBuilder $container * @param $array $configs */ protected function loadCoreExtension(ContainerBuilder $container, array $configs) { if (null === $this->basePath) { throw new \RuntimeException( 'Suite basepath is not set. Seems you have forgot to load configuration first.' ); } $extension = new BehatExtension($this->basePath); $extension->load($configs, $container); $container->addObjectResource($extension); } /** * {@inheritdoc} */ protected function getCommandName(InputInterface $input) { return 'behat'; } /** * {@inheritdoc} */ protected function getTerminalWidth() { return PHP_INT_MAX; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base behat console command. * * @author Konstantin Kudryashov */ abstract class BaseCommand extends Command { private $processor; /** * Returns service container instance. * * @return ContainerInterface */ abstract protected function getContainer(); /** * Sets command processor. * * @param ProcessorInterface $processor * * @return Command */ protected function setProcessor(ProcessorInterface $processor) { $this->processor = $processor; $this->processor->configure($this); return $this; } /** * {@inheritdoc} */ protected function initialize(InputInterface $input, OutputInterface $output) { $this->processor->process($input, $output); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat console command. * * @author Konstantin Kudryashov */ class BehatCommand extends BaseCommand { private $container; private $featuresPaths; private $strict = true; private $dryRun = false; /** * Initializes command. * * @param ContainerInterface $container * @param ProcessorInterface $processor */ public function __construct(ContainerInterface $container, ProcessorInterface $processor) { parent::__construct('behat'); $this->container = $container; $this->setDefinition(new InputDefinition); $this->setProcessor($processor); } /** * Sets features/scenarios paths to run. * * @param array $paths */ public function setFeaturesPaths(array $paths) { $this->featuresPaths = $paths; } /** * Returns paths to the features of current suite (basepath). * * @return array */ public function getFeaturesPaths() { return $this->featuresPaths; } /** * Sets runner to be strict. * * @param Boolean $strict */ public function setStrict($strict = true) { $this->strict = (bool) $strict; } /** * Checks whether runner is strict. * * @return Boolean */ public function isStrict() { return $this->strict; } /** * Sets suite to dry-run mode (skip all steps). * * @param Boolean $dryRun */ public function setDryRun($dryRun = true) { $this->dryRun = (bool) $dryRun; } /** * Checks whether runner is in dry-run mode. * * @return Boolean */ public function isDryRun() { return $this->dryRun; } /** * Returns container instance. * * @return ContainerInterface */ protected function getContainer() { return $this->container; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $gherkin = $this->getContainer()->get('gherkin'); $this->beforeSuite(); $this->runFeatures($gherkin); $this->afterSuite(); return $this->getCliReturnCode(); } /** * Parses and runs provided features. * * @param Gherkin $gherkin gherkin parser/loader */ protected function runFeatures(Gherkin $gherkin) { foreach ($this->getFeaturesPaths() as $path) { // parse every feature with Gherkin $features = $gherkin->load((string) $path); // and run it in FeatureTester foreach ($features as $feature) { $tester = $this->getContainer()->get('behat.tester.feature'); $tester->setSkip($this->isDryRun()); $feature->accept($tester); } } } /** * Returns CLI code for finished suite. * * @return integer */ protected function getCliReturnCode() { $logger = $this->getContainer()->get('behat.logger'); if ($this->isStrict()) { return intval(0 < $logger->getSuiteResult()); } return intval(4 === $logger->getSuiteResult()); } /** * Fire beforeSuite event. */ protected function beforeSuite() { $dispatcher = $this->getContainer()->get('behat.event_dispatcher'); $logger = $this->getContainer()->get('behat.logger'); $parameters = $this->getContainer()->get('behat.context.dispatcher')->getContextParameters(); $dispatcher->dispatch('beforeSuite', new SuiteEvent($logger, $parameters, false)); // catch app interruption if (function_exists('pcntl_signal')) { declare(ticks = 1); pcntl_signal(SIGINT, function() use($dispatcher, $parameters, $logger) { $dispatcher->dispatch('afterSuite', new SuiteEvent($logger, $parameters, false)); exit(1); }); } } /** * Fire afterSuite event. */ protected function afterSuite() { $dispatcher = $this->getContainer()->get('behat.event_dispatcher'); $logger = $this->getContainer()->get('behat.logger'); $parameters = $this->getContainer()->get('behat.context.dispatcher')->getContextParameters(); $dispatcher->dispatch('afterSuite', new SuiteEvent($logger, $parameters, true)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat console output formatter. * * @author Konstantin Kudryashov */ class OutputFormatter extends BaseOutputFormatter { /** * {@inheritdoc} */ public function __construct($decorated = null, array $styles = array()) { parent::__construct($decorated, array_merge(array( 'undefined' => new OutputFormatterStyle('yellow'), 'pending' => new OutputFormatterStyle('yellow'), 'pending_param' => new OutputFormatterStyle('yellow', null, array('bold')), 'failed' => new OutputFormatterStyle('red'), 'failed_param' => new OutputFormatterStyle('red', null, array('bold')), 'passed' => new OutputFormatterStyle('green'), 'passed_param' => new OutputFormatterStyle('green', null, array('bold')), 'skipped' => new OutputFormatterStyle('cyan'), 'skipped_param' => new OutputFormatterStyle('cyan', null, array('bold')), 'comment' => new OutputFormatterStyle('black'), 'tag' => new OutputFormatterStyle('cyan') ), $styles)); } /** * Formats a message according to the given styles. * * @param string $message The message to style * * @return string The styled message */ public function format($message) { return preg_replace_callback( '/{\+([a-z-_]+)}(.*?){\-\\1}/si', array($this, 'replaceStyle'), $message ); } /** * Replaces style of the output. * * @param array $match * * @return string The replaced style */ private function replaceStyle($match) { if (!$this->isDecorated()) { return $match[2]; } if ($this->hasStyle($match[1])) { $style = $this->getStyle($match[1]); } else { return $match[0]; } return $style->apply($match[2]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extended InputDefinition, which supports switchers. * * @author Konstantin Kudryashov */ class InputDefinition extends BaseDefinition { /** * Gets the synopsis. * * @return string The synopsis */ public function getSynopsis() { $elements = array(); $isSwitch = false; $options = $this->getOptions(); foreach ($options as $option) { if ($isSwitch) { $isSwitch = false; continue; } // if switch if (array_key_exists('no-'.$option->getName(), $options)) { $elements[] = sprintf('[--[no-]%s]', $option->getName()); $isSwitch = true; } else { $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; $elements[] = sprintf('['.($option->isValueRequired() ? '%s--%s="..."' : ($option->isValueOptional() ? '%s--%s[="..."]' : '%s--%s')).']', $shortcut, $option->getName()); } } foreach ($this->getArguments() as $argument) { $elements[] = sprintf($argument->isRequired() ? '%s' : '[%s]', $argument->getName().($argument->isArray() ? '1' : '')); if ($argument->isArray()) { $elements[] = sprintf('... [%sN]', $argument->getName()); } } return implode(' ', $elements); } /** * Returns a textual representation of the InputDefinition. * * @return string A string representing the InputDefinition */ public function asText() { // find the largest option or argument name $max = 0; foreach ($this->getOptions() as $option) { $nameLength = strlen($option->getName()) + 2; if ($option->getShortcut()) { $nameLength += strlen($option->getShortcut()) + 3; } if ($this->hasOption('no-'.$option->getName())) { $nameLength += 5; } $max = max($max, $nameLength); } foreach ($this->getArguments() as $argument) { $max = max($max, strlen($argument->getName())); } ++$max; $text = array(); if ($this->getArguments()) { $text[] = 'Arguments:'; foreach ($this->getArguments() as $argument) { if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) { $default = sprintf(' (default: %s)', $this->subformatDefaultValue($argument->getDefault())); } else { $default = ''; } $description = str_replace("\n", "\n".str_pad('', $max + 2, ' '), $argument->getDescription()); $text[] = sprintf(" %-${max}s %s%s", $argument->getName(), $description, $default); } $text[] = ''; } if ($this->getOptions()) { $text[] = 'Options:'; $isSwitch = false; $options = $this->getOptions(); foreach ($options as $option) { if ($isSwitch) { $isSwitch = false; continue; } if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) { $default = sprintf(' (default: %s)', $this->subformatDefaultValue($option->getDefault())); } else { $default = ''; } if ($this->hasOption('no-'.$option->getName())) { $isSwitch = true; } $multiple = $option->isArray() ? ' (multiple values allowed)' : ''; $description = str_replace("\n", "\n".str_pad('', $max + 2, ' '), $option->getDescription()); $optionMax = $max - strlen($option->getName()) - 2; if ($isSwitch) { $optionMax -= 5; } $text[] = sprintf(" %s %-${optionMax}s%s%s%s", '--'.( $isSwitch ? '[no-]' : '' ) . $option->getName(), $option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '', $description, $default, $multiple ); } $text[] = ''; } return implode("\n", $text); } private function subformatDefaultValue($default) { if (is_array($default) && $default === array_values($default)) { return sprintf("array('%s')", implode("', '", $default)); } return str_replace("\n", '', var_export($default, true)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Processors manager. * * @author Konstantin Kudryashov */ class AggregateProcessor implements ProcessorInterface { private $processors = array(); /** * Adds processor to the manager. * * @param ProcessorInterface $processor Processor instance */ public function addProcessor(ProcessorInterface $processor) { $this->processors[] = $processor; } /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { foreach ($this->processors as $processor) { $processor->configure($command); } } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output */ public function process(InputInterface $input, OutputInterface $output) { foreach ($this->processors as $processor) { $processor->process($input, $output); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context reader processor. * * @author Konstantin Kudryashov */ class ContextReaderProcessor extends Processor { /** * Constructs processor. * * @param ContainerInterface $container Container instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output */ public function process(InputInterface $input, OutputInterface $output) { $this->container->get('behat.context.reader')->read(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Format processor. * * @author Konstantin Kudryashov */ class FormatProcessor extends Processor { private $container; /** * Constructs processor. * * @param ContainerInterface $container Container instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { $formatDispatchers = $this->container->get('behat.formatter.manager')->getDispatchers(); $command ->addOption('--format', '-f', InputOption::VALUE_REQUIRED, "How to format features. pretty is default.\n" . "Default formatters are:\n" . implode("\n", array_map(function($dispatcher) { $comment = '- '.$dispatcher->getName().': '; if ($dispatcher->getDescription()) { $comment .= $dispatcher->getDescription(); } else { $comment .= $dispatcher->getClass(); } return $comment; }, $formatDispatchers) ) . "\n" . "Can use multiple formats at once (splitted with \",\")" ) ->addOption('--out', null, InputOption::VALUE_REQUIRED, "Write formatter output to a file/directory\n" . "instead of STDOUT (output_path)." ) ->addOption('--lang', null, InputOption::VALUE_REQUIRED, 'Print formatter output in particular language.' ) // --[no-]ansi ->addOption('--ansi', null, InputOption::VALUE_NONE, "Whether or not to use ANSI color in the output.\n". "Behat decides based on your platform and the output\n". "destination if not specified." ) ->addOption('--no-ansi', null, InputOption::VALUE_NONE) // --[no-]time ->addOption('--time', null, InputOption::VALUE_NONE, "Whether or not to show timer in output." ) ->addOption('--no-time', null, InputOption::VALUE_NONE) // --[no-]paths ->addOption('--paths', null, InputOption::VALUE_NONE, "Whether or not to print sources paths." ) ->addOption('--no-paths', null, InputOption::VALUE_NONE) // --[no-]snippets ->addOption('--snippets', null, InputOption::VALUE_NONE, "Whether or not to print snippets for undefined steps." ) ->addOption('--no-snippets', null, InputOption::VALUE_NONE) // --[no-]snippets-paths ->addOption('--snippets-paths', null, InputOption::VALUE_NONE, "Whether or not to print details about undefined steps\n". "in their snippets." ) ->addOption('--no-snippets-paths', null, InputOption::VALUE_NONE) // --[no-]multiline ->addOption('--multiline', null, InputOption::VALUE_NONE, "Whether or not to print multiline arguments for steps." ) ->addOption('--no-multiline', null, InputOption::VALUE_NONE) // --[no-]expand ->addOption('--expand', null, InputOption::VALUE_NONE, "Whether or not to expand scenario outline examples\n". "tables.\n" ) ->addOption('--no-expand', null, InputOption::VALUE_NONE) ; } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output */ public function process(InputInterface $input, OutputInterface $output) { $manager = $this->container->get('behat.formatter.manager'); $formats = array_map('trim', explode(',', $input->getOption('format') ?: $this->container->getParameter('behat.formatter.name') )); $this->loadFormatterTranslations(); $this->loadCustomFormatters($manager); $this->initFormatters($manager, $formats); $this->configureFormatters($manager, $input, $output); $this->initMultipleOutputs($manager, $input); } /** * Loads formatter translations from behat.paths.i18n parameter file. */ protected function loadFormatterTranslations() { if (!is_file($i18nFile = $this->container->getParameter('behat.paths.i18n'))) { return; } $translator = $this->container->get('behat.translator'); foreach (require($i18nFile) as $lang => $messages) { $translator->addResource('array', $messages, $lang, 'behat'); } } /** * Loads custom formatters, defined in behat.yml. * * @param FormatterManager $manager */ protected function loadCustomFormatters(FormatterManager $manager) { foreach ($this->container->getParameter('behat.formatter.classes') as $name => $class) { $manager->addDispatcher(new FormatterDispatcher($class, $name)); } } /** * Inits formatters. * * @param FormatterManager $manager * @param $array $formats */ protected function initFormatters(FormatterManager $manager, array $formats) { foreach ($formats as $format) { $manager->initFormatter($format); } } /** * Configures formatters based on container, input and output configurations. * * @param FormatterManager $manager * @param InputInterface $input * @param OutputInterface $output */ protected function configureFormatters(FormatterManager $manager, InputInterface $input, OutputInterface $output) { $parameters = $this->container->getParameter('behat.formatter.parameters'); foreach ($parameters as $name => $value) { if ('output_path' === $name) { continue; } $manager->setFormattersParameter($name, $value); } $manager->setFormattersParameter('base_path', $this->container->getParameter('behat.paths.base') ); $manager->setFormattersParameter('support_path', $this->container->getParameter('behat.paths.bootstrap') ); $manager->setFormattersParameter('decorated', $output->isDecorated() ); if ($input->getOption('verbose')) { $manager->setFormattersParameter('verbose', true); } if ($input->getOption('lang')) { $manager->setFormattersParameter('language', $input->getOption('lang')); } if (null !== $ansi = $this->getSwitchValue($input, 'ansi')) { $output->setDecorated($ansi); $manager->setFormattersParameter('decorated', $ansi); } if (null !== $time = $this->getSwitchValue($input, 'time')) { $manager->setFormattersParameter('time', $time); } if (null !== $snippets = $this->getSwitchValue($input, 'snippets')) { $manager->setFormattersParameter('snippets', $snippets); } if (null !== $snippetsPaths = $this->getSwitchValue($input, 'snippets-paths')) { $manager->setFormattersParameter('snippets_paths', $snippetsPaths); } if (null !== $paths = $this->getSwitchValue($input, 'paths')) { $manager->setFormattersParameter('paths', $paths); } if (null !== $expand = $this->getSwitchValue($input, 'expand')) { $manager->setFormattersParameter('expand', $expand); } if (null !== $multiline = $this->getSwitchValue($input, 'multiline')) { $manager->setFormattersParameter('multiline_arguments', $multiline); } } /** * Initializes multiple formatters with different outputs. * * @param FormatterManager $manager * @param InputInterface $input */ protected function initMultipleOutputs(FormatterManager $manager, InputInterface $input) { $parameters = $this->container->getParameter('behat.formatter.parameters'); if ($input->getOption('out')) { $outputs = $input->getOption('out'); } elseif (isset($parameters['output_path'])) { $outputs = $parameters['output_path']; } else { return; } if (false === strpos($outputs, ',')) { $outputPath = $this->locateOutputPath($outputs); $manager->setFormattersParameter('output_path', $outputPath); $manager->setFormattersParameter('decorated', (bool) $this->getSwitchValue($input, 'ansi')); return; } foreach (array_map('trim', explode(',', $outputs)) as $i => $out) { if (!$out || 'null' === $out || 'false' === $out) { continue; } $outputPath = $this->locateOutputPath($out); $formatters = $manager->getFormatters(); if (isset($formatters[$i])) { $formatters[$i]->setParameter('output_path', $outputPath); $formatters[$i]->setParameter('decorated', (bool) $this->getSwitchValue($input, 'ansi')); } } } /** * Locates output path from relative one. * * @param string $out * * @return string */ private function locateOutputPath($out) { if ($this->isAbsolutePath($out)) { return $out; } $out = getcwd().DIRECTORY_SEPARATOR.$out; if (!file_exists($out)) { touch($out); $out = realpath($out); unlink($out); } else { $out = realpath($out); } return $out; } /** * Returns whether the file path is an absolute path. * * @param string $file A file path * * @return Boolean */ private function isAbsolutePath($file) { if ($file[0] == '/' || $file[0] == '\\' || (strlen($file) > 3 && ctype_alpha($file[0]) && $file[1] == ':' && ($file[2] == '\\' || $file[2] == '/') ) || null !== parse_url($file, PHP_URL_SCHEME) ) { return true; } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Gherkin processor. * * @author Konstantin Kudryashov */ class GherkinProcessor extends Processor { private $container; /** * Constructs processor. * * @param ContainerInterface $container Container instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { $command ->addOption('--name', null, InputOption::VALUE_REQUIRED, "Only execute the feature elements which match\n" . "part of the given name or regex." ) ->addOption('--tags', null, InputOption::VALUE_REQUIRED, "Only execute the features or scenarios with tags\n" . "matching tag filter expression.\n" ) ->addOption('--cache', null, InputOption::VALUE_REQUIRED, "Cache parsed features into specified path." ) ; } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output */ public function process(InputInterface $input, OutputInterface $output) { $gherkinParser = $this->container->get('gherkin'); $name = $input->getOption('name') ?: $this->container->getParameter('gherkin.filters.name'); if ($name) { $gherkinParser->addFilter(new NameFilter($name)); } $tags = $input->getOption('tags') ?: $this->container->getParameter('gherkin.filters.tags'); if ($tags) { $gherkinParser->addFilter(new TagFilter($tags)); } $path = $input->getOption('cache') ?: $this->container->getParameter('behat.options.cache'); if ($path) { $cache = new FileCache($path); $this->container->get('gherkin.loader.gherkin_file')->setCache($cache); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Help (story-syntax and definition printers) processor. * * @author Konstantin Kudryashov */ class HelpProcessor extends Processor { private $container; /** * Constructs processor. * * @param ContainerInterface $container Container instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { $command ->addOption('--story-syntax', null, InputOption::VALUE_NONE, "Print *.feature example.\n" . "Use --lang to see specific language." ) ->addOption('--definitions', '-d', InputOption::VALUE_REQUIRED, "Print all available step definitions:\n" . "- use -dl to just list definition expressions.\n" . "- use -di to show definitions with extended info.\n" . "- use -d 'needle' to find specific definitions.\n" . "Use --lang to see definitions in specific language.\n" ) ; } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output */ public function process(InputInterface $input, OutputInterface $output) { if ($input->getOption('story-syntax')) { $this->container->get('behat.help_printer.story_syntax')->printSyntax( $output, $input->getOption('lang') ?: 'en' ); exit(0); } if ($type = $input->getOption('definitions')) { if ('l' === $type) { $short = true; $search = null; } elseif ('i' === $type) { $short = false; $search = null; } else { $short = false; $search = $type; } $this->container->get('behat.help_printer.definitions')->printDefinitions( $output, $search, $input->getOption('lang') ?: 'en', $short ); exit(0); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Init operation processor. * * @author Konstantin Kudryashov */ class InitProcessor extends Processor { private $container; /** * Constructs processor. * * @param ContainerInterface $container Container instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { $command->addOption('--init', null, InputOption::VALUE_NONE, "Create features directory structure.\n" ); } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output */ public function process(InputInterface $input, OutputInterface $output) { if ($input->getOption('init')) { $this->initFeaturesDirectoryStructure($output); exit(0); } } /** * Creates features path structure (initializes behat tests structure). * * @param OutputInterface $output output console */ protected function initFeaturesDirectoryStructure(OutputInterface $output) { $basePath = $this->container->getParameter('behat.paths.base').DIRECTORY_SEPARATOR; $featuresPath = $this->container->getParameter('behat.paths.features'); $bootstrapPath = $this->container->getParameter('behat.paths.bootstrap'); if (!is_dir($featuresPath)) { mkdir($featuresPath, 0777, true); $output->writeln( '+d ' . str_replace($basePath, '', realpath($featuresPath)) . ' - place your *.feature files here' ); } if (!is_dir($bootstrapPath)) { mkdir($bootstrapPath, 0777, true); $output->writeln( '+d ' . str_replace($basePath, '', realpath($bootstrapPath)) . ' - place bootstrap scripts and static files here' ); file_put_contents( $bootstrapPath.DIRECTORY_SEPARATOR.'FeatureContext.php', $this->getFeatureContextSkelet() ); $output->writeln( '+f ' . str_replace($basePath, '', realpath($bootstrapPath)).DIRECTORY_SEPARATOR. 'FeatureContext.php - place your feature related code here' ); } } /** * Returns feature context skelet. * * @return string */ protected function getFeatureContextSkelet() { return <<<'PHP' * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Path locator processor. * * @author Konstantin Kudryashov */ class LocatorProcessor extends Processor { private $container; /** * Constructs processor. * * @param ContainerInterface $container Container instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { $command->addArgument('features', InputArgument::OPTIONAL, "Feature(s) to run. Could be:\n" . "- a dir (features/)\n" . "- a feature (*.feature)\n" . "- a scenario at specific line (*.feature:10).\n" . "- all scenarios at or after a specific line (*.feature:10-*).\n" . "- all scenarios at a line within a specific range (*.feature:10-20)." ); } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output * * @throws \InvalidArgumentException */ public function process(InputInterface $input, OutputInterface $output) { $this->container->get('behat.console.command')->setFeaturesPaths( array($input->getArgument('features')) ); if (is_dir($bootstrapPath = $this->container->getParameter('behat.paths.bootstrap'))) { $this->loadBootstrapScripts($bootstrapPath); } } /** * Requires *.php scripts from bootstrap/ folder. * * @param string $path */ protected function loadBootstrapScripts($path) { $iterator = Finder::create() ->files() ->name('*.php') ->sortByName() ->in($path) ; foreach ($iterator as $file) { require_once((string) $file); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract base processor. * * @author Konstantin Kudryashov */ abstract class Processor implements ProcessorInterface { /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { } /** * Returns correct value for input switch. * * @param InputInterface $input * @param string $name * * @return Boolean|null */ protected function getSwitchValue(InputInterface $input, $name) { if ($input->getOption($name)) { return true; } if ($input->getOption('no-'.$name)) { return false; } return null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat console processor interface. * * @author Konstantin Kudryashov */ interface ProcessorInterface { /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command); /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output */ public function process(InputInterface $input, OutputInterface $output); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * command configuration processor. * * @author Konstantin Kudryashov */ class RunProcessor extends Processor { private $container; /** * Constructs processor. * * @param ContainerInterface $container Container instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Configures command to be able to process it later. * * @param Command $command */ public function configure(Command $command) { $command ->addOption('--strict', null, InputOption::VALUE_NONE, 'Fail if there are any undefined or pending steps.' ) ->addOption('--dry-run', null, InputOption::VALUE_NONE, 'Invokes formatters without executing the steps & hooks.' ) ->addOption('--rerun', null, InputOption::VALUE_REQUIRED, "Save list of failed scenarios into new file\n" . "or use existing file to run only scenarios from it." ) ->addOption('--append-snippets', null, InputOption::VALUE_NONE, "Appends snippets for undefined steps into main context." ) ->addOption('--append-to', null, InputOption::VALUE_REQUIRED, "Appends snippets for undefined steps into specified class." ) ; } /** * Processes data from container and console input. * * @param InputInterface $input * @param OutputInterface $output * * @throws \RuntimeException */ public function process(InputInterface $input, OutputInterface $output) { $command = $this->container->get('behat.console.command'); $hookDispatcher = $this->container->get('behat.hook.dispatcher'); $command->setStrict( $input->getOption('strict') || $this->container->getParameter('behat.options.strict') ); $command->setDryRun( $input->getOption('dry-run') || $this->container->getParameter('behat.options.dry_run') ); $hookDispatcher->setDryRun( $input->getOption('dry-run') || $this->container->getParameter('behat.options.dry_run') ); if ($file = $input->getOption('rerun') ?: $this->container->getParameter('behat.options.rerun')) { if (file_exists($file)) { $command->setFeaturesPaths(explode("\n", trim(file_get_contents($file)))); } $this->container->get('behat.formatter.manager') ->initFormatter('failed') ->setParameter('output_path', $file); } if ($input->getOption('append-snippets')) { $this->initializeSnippetsAppender(); } elseif ($class = $input->getOption('append-to')) { $this->initializeSnippetsAppender($class); } elseif ($class = $this->container->getParameter('behat.options.append_snippets')) { $this->initializeSnippetsAppender(true !== $class ? $class : null); } } /** * Appends snippets to the main context after suite run. * * @param string $class */ protected function initializeSnippetsAppender($class = null) { $classname = (null !== $class) ? str_replace('/', '\\', $class) : $this->container->get('behat.context.dispatcher')->getContextClass() ; $contextRefl = new \ReflectionClass($classname); if ($contextRefl->implementsInterface('Behat\Behat\Context\ClosuredContextInterface')) { throw new \RuntimeException( '--append-snippets doesn\'t support closured contexts' ); } $formatManager = $this->container->get('behat.formatter.manager'); $formatManager->setFormattersParameter('snippets', false); $formatter = $formatManager->initFormatter('snippets'); $formatter->setParameter('decorated', false); $formatter->setParameter('output_decorate', false); $formatter->setParameter('output', $snippets = fopen('php://memory', 'rw')); $this->container->get('behat.event_dispatcher') ->addListener('afterSuite', function() use($contextRefl, $snippets) { rewind($snippets); $snippets = stream_get_contents($snippets); if (trim($snippets)) { $snippets = strtr($snippets, array('\\' => '\\\\', '$' => '\\$')); $context = file_get_contents($contextRefl->getFileName()); $context = preg_replace('/}[ \n]*$/', rtrim($snippets)."\n}\n", $context); file_put_contents($contextRefl->getFileName(), $context); } }, -5); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat basic context implementation. * * @author Konstantin Kudryashov */ class BehatContext implements ExtendedContextInterface { private $subcontexts = array(); private $parentContext; /** * Adds subcontext to current context. * * @param string $alias * @param ExtendedContextInterface $context */ public function useContext($alias, ExtendedContextInterface $context) { $context->setParentContext($this); $this->subcontexts[$alias] = $context; } /** * Sets parent context of current context. * * @param ExtendedContextInterface $parentContext */ public function setParentContext(ExtendedContextInterface $parentContext) { $this->parentContext = $parentContext; } /** * Returns main context. * * @return ExtendedContextInterface */ public function getMainContext() { if (null !== $this->parentContext) { return $this->parentContext->getMainContext(); } return $this; } /** * Find current context's subcontext by alias name. * * @param string $alias * * @return ExtendedContextInterface */ public function getSubcontext($alias) { // search in current context subcontexts if (isset($this->subcontexts[$alias])) { return $this->subcontexts[$alias]; } // search in subcontexts childs contexts foreach ($this->subcontexts as $subcontext) { if (null !== $context = $subcontext->getSubcontext($alias)) { return $context; } } } /** * Returns all added subcontexts. * * @return array */ public function getSubcontexts() { return $this->subcontexts; } /** * Finds subcontext by it's name. * * @param string $className * * @return ContextInterface */ public function getSubcontextByClassName($className) { foreach ($this->getSubcontexts() as $subcontext) { if (get_class($subcontext) === $className) { return $subcontext; } if ($context = $subcontext->getSubcontextByClassName($className)) { return $context; } } } /** * Prints beautified debug string. * * @param string $string debug string */ public function printDebug($string) { echo "\n\033[36m| " . strtr($string, array("\n" => "\n| ")) . "\033[0m\n\n"; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context class guesser interface. * * @author Konstantin Kudryashov */ interface ClassGuesserInterface { /** * Tries to guess context classname. * * @return string|null */ public function guess(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Predefined context class guesser. * * @author Konstantin Kudryashov */ class PredefinedClassGuesser implements ClassGuesserInterface { private $classname; private $force; /** * Initializes guesser. * * @param string $classname * @param Boolean $force */ public function __construct($classname, $force = false) { $this->classname = $classname; $this->force = (bool) $force; } /** * Tries to guess context classname. * * @return string|null */ public function guess() { return $this->force || class_exists($this->classname) ? $this->classname : null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Closured context interface. * * @author Konstantin Kudryashov */ interface ClosuredContextInterface extends ContextInterface { /** * Returns array of step definition files (*.php). * * @return array */ public function getStepDefinitionResources(); /** * Returns array of hook definition files (*.php). * * @return array */ public function getHookDefinitionResources(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context dispatcher. * * @author Konstantin Kudryashov */ class ContextDispatcher { private $classGuessers = array(); private $initializers = array(); private $parameters = array(); /** * Initialize dispatcher. * * @param array $parameters context parameters */ public function __construct(array $parameters = array()) { $this->parameters = $parameters; } /** * Adds context class guesser to the dispatcher. * * @param ClassGuesser\ClassGuesserInterface $guesser */ public function addClassGuesser(ClassGuesser\ClassGuesserInterface $guesser) { $this->classGuessers[] = $guesser; } /** * Adds context initializer to the dispatcher. * * @param Initializer\InitializerInterface $initializer */ public function addInitializer(Initializer\InitializerInterface $initializer) { $this->initializers[] = $initializer; } /** * Returns context classname. * * @return string */ public function getContextClass() { $classname = null; foreach ($this->classGuessers as $guesser) { if ($classname = $guesser->guess()) { break; } } if (null === $classname) { throw new \RuntimeException( 'Context class not found.'."\n". 'Maybe you have provided a wrong or no `bootstrap` path in your behat.yml:'."\n". 'http://docs.behat.org/guides/7.config.html#paths' ); } if (!class_exists($classname)) { throw new \RuntimeException(sprintf( 'Context class "%s" not found and can not be instantiated.', $classname )); } $contextClassRefl = new \ReflectionClass($classname); if (!$contextClassRefl->implementsInterface('Behat\Behat\Context\ContextInterface')) { throw new \RuntimeException(sprintf( 'Context class "%s" should implement ContextInterface', $classname )); } return $classname; } /** * Returns context parameters. * * @return array */ public function getContextParameters() { return $this->parameters; } /** * Creates new context instance. * * @return ContextInterface * * @throws \RuntimeException */ public function createContext() { $classname = $this->getContextClass(); $parameters = $this->getContextParameters(); $context = new $classname($parameters); $this->initializeContext($context); return $context; } /** * Initializes context with registered initializers. * * @param ContextInterface $context */ private function initializeContext(ContextInterface $context) { foreach ($this->initializers as $initializer) { if ($initializer->supports($context)) { $initializer->initialize($context); } } // if context has subcontexts - initialize them too if ($context instanceof SubcontextableContextInterface) { foreach ($context->getSubcontexts() as $subcontext) { $this->initializeContext($subcontext); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context interface. * * @author Konstantin Kudryashov */ interface ContextInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context reader. * * @author Konstantin Kudryashov */ class ContextReader { private $contextDispatcher; private $loaders = array(); /** * Initializes context reader. * * @param ContextDispatcher $contextDispatcher */ public function __construct(ContextDispatcher $contextDispatcher) { $this->contextDispatcher = $contextDispatcher; } /** * Adds context loader to the list of available loaders. * * @param Loader\LoaderInterface $loader */ public function addLoader(Loader\LoaderInterface $loader) { $this->loaders[] = $loader; } /** * Reads all definition data from main context. */ public function read() { $this->readFromContext($this->contextDispatcher->createContext()); } /** * Reads definition data from specific context class. * * @param ContextInterface $context */ private function readFromContext(ContextInterface $context) { foreach ($this->loaders as $loader) { if ($loader->supports($context)) { $loader->load($context); } } if ($context instanceof SubcontextableContextInterface) { foreach ($context->getSubcontexts() as $subcontext) { $this->readFromContext($subcontext); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extended context interface. * * @author Konstantin Kudryashov */ interface ExtendedContextInterface extends SubcontextableContextInterface { /** * Sets parent context of current context. * * @param ExtendedContextInterface $parentContext */ public function setParentContext(ExtendedContextInterface $parentContext); /** * Returns main context. * * @return ExtendedContextInterface */ public function getMainContext(); /** * Find current context's subcontext by alias name. * * @param string $alias * * @return ExtendedContextInterface */ public function getSubcontext($alias); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context initializer interface. * * @author Konstantin Kudryashov */ interface InitializerInterface { /** * Checks if initializer supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context); /** * Initializes provided context. * * @param ContextInterface $context */ public function initialize(ContextInterface $context); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Annotated contexts reader. * * @author Konstantin Kudryashov */ class AnnotatedLoader implements LoaderInterface { private $definitionDispatcher; private $hookDispatcher; private $annotationClasses = array( 'given' => 'Behat\Behat\Definition\Annotation\Given', 'when' => 'Behat\Behat\Definition\Annotation\When', 'then' => 'Behat\Behat\Definition\Annotation\Then', 'transform' => 'Behat\Behat\Definition\Annotation\Transformation', 'beforesuite' => 'Behat\Behat\Hook\Annotation\BeforeSuite', 'aftersuite' => 'Behat\Behat\Hook\Annotation\AfterSuite', 'beforefeature' => 'Behat\Behat\Hook\Annotation\BeforeFeature', 'afterfeature' => 'Behat\Behat\Hook\Annotation\AfterFeature', 'beforescenario' => 'Behat\Behat\Hook\Annotation\BeforeScenario', 'afterscenario' => 'Behat\Behat\Hook\Annotation\AfterScenario', 'beforestep' => 'Behat\Behat\Hook\Annotation\BeforeStep', 'afterstep' => 'Behat\Behat\Hook\Annotation\AfterStep' ); private $availableAnnotations; /** * Initializes context loader. * * @param DefinitionDispatcher $definitionDispatcher * @param HookDispatcher $hookDispatcher */ public function __construct(DefinitionDispatcher $definitionDispatcher, HookDispatcher $hookDispatcher) { $this->definitionDispatcher = $definitionDispatcher; $this->hookDispatcher = $hookDispatcher; $this->availableAnnotations = implode("|", array_keys($this->annotationClasses)); } /** * Checks if loader supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context) { return true; } /** * Loads definitions and translations from provided context. * * @param ContextInterface $context */ public function load(ContextInterface $context) { $reflection = new \ReflectionObject($context); foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodRefl) { foreach ($this->readMethodAnnotations($reflection->getName(), $methodRefl) as $annotation) { if ($annotation instanceof DefinitionInterface) { $this->definitionDispatcher->addDefinition($annotation); } elseif ($annotation instanceof TransformationInterface) { $this->definitionDispatcher->addTransformation($annotation); } elseif ($annotation instanceof HookInterface) { $this->hookDispatcher->addHook($annotation); } } } } /** * Reads all supported method annotations. * * @param stirng $className * @param \ReflectionMethod $method * * @return array */ private function readMethodAnnotations($className, \ReflectionMethod $method) { $annotations = array(); // read parent annotations try { $prototype = $method->getPrototype(); $annotations = array_merge($annotations, $this->readMethodAnnotations($className, $prototype)); } catch (\ReflectionException $e) {} // read method annotations if ($docBlock = $method->getDocComment()) { $description = null; foreach (explode("\n", $docBlock) as $docLine) { $docLine = preg_replace('/^\/\*\*\s*|^\s*\*\s*|\s*\*\/$|\s*$/', '', $docLine); if (preg_match('/^\@('.$this->availableAnnotations.')\s*(.*)?$/i', $docLine, $matches)) { $class = $this->annotationClasses[strtolower($matches[1])]; $callback = array($className, $method->getName()); if (isset($matches[2]) && !empty($matches[2])) { $annotation = new $class($callback, $matches[2]); } else { $annotation = new $class($callback); } if (null !== $description) { $annotation->setDescription($description); } $annotations[] = $annotation; } elseif (null === $description && '' !== $docLine && false === strpos($docLine, '@')) { $description = $docLine; } } } return $annotations; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Closured contexts reader. * * @author Konstantin Kudryashov */ class ClosuredLoader implements LoaderInterface { private $definitionLoader; private $hookLoader; /** * Initializes context loader. * * @param ClosuredDefinitionLoader $definitionLoader * @param ClosuredHookLoader $hookLoader */ public function __construct(ClosuredDefinitionLoader $definitionLoader, ClosuredHookLoader $hookLoader) { $this->definitionLoader = $definitionLoader; $this->hookLoader = $hookLoader; } /** * Checks if loader supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context) { return $context instanceof ClosuredContextInterface; } /** * Loads definitions and translations from provided context. * * @param ContextInterface $context */ public function load(ContextInterface $context) { foreach ($context->getStepDefinitionResources() as $path) { $this->definitionLoader->load($path); } foreach ($context->getHookDefinitionResources() as $path) { $this->hookLoader->load($path); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context loader interface. * * @author Konstantin Kudryashov */ interface LoaderInterface { /** * Checks if loader supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context); /** * Loads definitions and translations from provided context. * * @param ContextInterface $context */ public function load(ContextInterface $context); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Translated contexts reader. * * @author Konstantin Kudryashov */ class TranslatedLoader implements LoaderInterface { private $translator; /** * Initializes context loader. * * @param TranslatorInterface $translator */ public function __construct(TranslatorInterface $translator) { $this->translator = $translator; } /** * Checks if loader supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context) { return $context instanceof TranslatedContextInterface; } /** * Loads definitions and translations from provided context. * * @param ContextInterface $context * * @throws \InvalidArgumentException */ public function load(ContextInterface $context) { foreach ($context->getTranslationResources() as $path) { $extension = pathinfo($path, PATHINFO_EXTENSION); if ('yml' === $extension) { $this->translator->addResource( 'yaml', $path, basename($path, '.yml'), 'behat.definitions' ); } elseif ('xliff' === $extension) { $this->translator->addResource( 'xliff', $path, basename($path, '.xliff'), 'behat.definitions' ); } elseif ('php' === $extension) { $this->translator->addResource( 'php', $path, basename($path, '.php'), 'behat.definitions' ); } else { throw new \InvalidArgumentException(sprintf( 'Can not read definitions translations from file "%s". File is not supported', $path )); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Given substep. * * @author Konstantin Kudryashov */ class Given implements SubstepInterface { private $node; /** * Initializes Given substep. */ public function __construct() { $arguments = func_get_args(); $text = array_shift($arguments); $node = new StepNode('Given', $text); $node->setArguments($arguments); $this->node = $node; } /** * Returns substep node. * * @return StepNode */ public function getStepNode() { return $this->node; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Substep interface. * * @author Konstantin Kudryashov */ interface SubstepInterface { /** * Returns substep node. * * @return StepNode */ public function getStepNode(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Then substep. * * @author Konstantin Kudryashov */ class Then implements SubstepInterface { private $node; /** * Initializes Then substep. */ public function __construct() { $arguments = func_get_args(); $text = array_shift($arguments); $node = new StepNode('Then', $text); $node->setArguments($arguments); $this->node = $node; } /** * Returns substep node. * * @return StepNode */ public function getStepNode() { return $this->node; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * When substep. * * @author Konstantin Kudryashov */ class When implements SubstepInterface { private $node; /** * Initializes When substep. */ public function __construct() { $arguments = func_get_args(); $text = array_shift($arguments); $node = new StepNode('When', $text); $node->setArguments($arguments); $this->node = $node; } /** * Returns substep node. * * @return StepNode */ public function getStepNode() { return $this->node; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Context interface with subcontexts support. * * @author Konstantin Kudryashov */ interface SubcontextableContextInterface extends ContextInterface { /** * Returns all added subcontexts. * * @return array */ public function getSubcontexts(); /** * Finds subcontext by it's name. * * @param string $className * * @return ContextInterface */ public function getSubcontextByClassName($className); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Translated context interface. * * @author Konstantin Kudryashov */ interface TranslatedContextInterface extends ContextInterface { /** * Returns array of i18n XLIFF files paths. * * @return array */ public function getTranslationResources(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat run logger. * * @author Konstantin Kudryashov */ class LoggerDataCollector implements EventSubscriberInterface { private $startTime; private $finishTime; private $statuses = array( StepEvent::PASSED => 'passed', StepEvent::SKIPPED => 'skipped', StepEvent::PENDING => 'pending', StepEvent::UNDEFINED => 'undefined', StepEvent::FAILED => 'failed' ); private $suiteResult = 0; private $featuresCount = 0; private $featuresStatuses = array(); private $scenariosCount = 0; private $scenariosStatuses = array(); private $stepsCount = 0; private $stepsStatuses = array(); private $definitionsSnippets = array(); private $failedStepsEvents = array(); private $pendingStepsEvents = array(); /** * Initializes logger. */ public function __construct() { $this->featuresStatuses = array_combine( array_values($this->statuses), array_fill(0, count($this->statuses), 0) ); $this->scenariosStatuses = array_combine( array_values($this->statuses), array_fill(0, count($this->statuses), 0) ); $this->stepsStatuses = array_combine( array_values($this->statuses), array_fill(0, count($this->statuses), 0) ); } /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to */ public static function getSubscribedEvents() { $events = array( 'beforeSuite', 'afterSuite', 'afterFeature', 'afterScenario', 'afterOutlineExample', 'afterStep' ); return array_combine($events, $events); } /** * Listens to "suite.before" event. * * @param SuiteEvent $event * * @uses startTimer() */ public function beforeSuite(SuiteEvent $event) { $this->startTimer(); } /** * Listens to "suite.after" event. * * @param SuiteEvent $event * * @uses finishTimer() */ public function afterSuite(SuiteEvent $event) { $this->finishTimer(); } /** * Listens to "feature.after" event. * * @param FeatureEvent $event * * @uses collectFeatureResult() */ public function afterFeature(FeatureEvent $event) { $this->collectFeatureResult($event->getResult()); } /** * Listens to "scenario.after" event. * * @param ScenarioEvent $event * * @uses collectScenarioResult() */ public function afterScenario(ScenarioEvent $event) { $this->collectScenarioResult($event->getResult()); } /** * Listens to "outline.example.after" event. * * @param OutlineExampleEvent $event * * @uses collectScenarioResult() */ public function afterOutlineExample(OutlineExampleEvent $event) { $this->collectScenarioResult($event->getResult()); } /** * Listens to "step.after" event. * * @param StepEvent $event * * @uses collectStepStats() */ public function afterStep(StepEvent $event) { $this->collectStepStats($event); } /** * Returns suite total execution time. * * @return float miliseconds */ public function getTotalTime() { return $this->finishTime - $this->startTime; } /** * Returns overall suites result. * * @return integer */ public function getSuiteResult() { return $this->suiteResult; } /** * Returns overall features count. * * @return integer */ public function getFeaturesCount() { return $this->featuresCount; } /** * Returns hash of features statuses count. * * @return array hash (ex: passed => 10, failed => 2) */ public function getFeaturesStatuses() { return $this->featuresStatuses; } /** * Returns overall scenarios count. * * @return integer */ public function getScenariosCount() { return $this->scenariosCount; } /** * Returns hash of scenarios statuses count. * * @return array hash (ex: passed => 10, failed => 2) */ public function getScenariosStatuses() { return $this->scenariosStatuses; } /** * Returns overall steps count. * * @return integer */ public function getStepsCount() { return $this->stepsCount; } /** * Returns hash of steps statuses count. * * @return array hash (ex: passed => 10, failed => 2) */ public function getStepsStatuses() { return $this->stepsStatuses; } /** * Returns hash of definition snippets for undefined steps. * * @return array hash with md5 as key and snippet as value */ public function getDefinitionsSnippets() { return $this->definitionsSnippets; } /** * Returns array of failed steps events. * * @return array */ public function getFailedStepsEvents() { return $this->failedStepsEvents; } /** * Returns array of pending steps events; * * @return array */ public function getPendingStepsEvents() { return $this->pendingStepsEvents; } /** * Starts suite timer. */ private function startTimer() { $this->startTime = microtime(true); } /** * Stops suite timer. */ private function finishTimer() { $this->finishTime = microtime(true); } /** * Collects feature result status. * * @param integer $result status code */ private function collectFeatureResult($result) { ++$this->featuresCount; ++$this->featuresStatuses[$this->statuses[$result]]; $this->suiteResult = max($this->suiteResult, $result); } /** * Collects scenario result status. * * @param integer $result status code */ private function collectScenarioResult($result) { ++$this->scenariosCount; ++$this->scenariosStatuses[$this->statuses[$result]]; } /** * Collects step statistics. * * @param StepEvent $event */ private function collectStepStats(StepEvent $event) { ++$this->stepsCount; ++$this->stepsStatuses[$this->statuses[$event->getResult()]]; switch ($event->getResult()) { case StepEvent::UNDEFINED: $hash = $event->getSnippet()->getHash(); if (!isset($this->definitionsSnippets[$hash])) { $this->definitionsSnippets[$hash] = $event->getSnippet(); } else { $this->definitionsSnippets[$hash]->addStep($event->getSnippet()->getLastStep()); } break; case StepEvent::FAILED: $this->failedStepsEvents[] = $event; break; case StepEvent::PENDING: $this->pendingStepsEvents[] = $event; break; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Step definition. * * @author Konstantin Kudryashov */ abstract class Definition extends Annotation implements DefinitionInterface { private $regex; private $matchedText; private $values = array(); /** * Initializes definition. * * @param callback $callback definition callback * @param string $regex definition regular expression * * @throws \InvalidArgumentException */ public function __construct($callback, $regex) { if (!is_callable($callback)) { throw new \InvalidArgumentException(sprintf( '"%s" definition callback should be a valid callable, but %s given', basename(str_replace('\\', '/', get_class($this))), gettype($callback) )); } parent::__construct($callback); $this->regex = $regex; } /** * Returns definition regex to match. * * @return string */ public function getRegex() { return $this->regex; } /** * Saves matched step text to definition. * * @param string $text */ public function setMatchedText($text) { $this->matchedText = $text; } /** * Returns matched step text. * * @return string */ public function getMatchedText() { return $this->matchedText; } /** * Sets step parameters for step run. * * @param array $values */ public function setValues(array $values) { $this->values = $values; } /** * Returns step parameters for step run. * * @return array */ public function getValues() { return $this->values; } /** * Custom error handler. * * This method used as custom error handler when step is running. * * @see set_error_handler() * * @param integer $level * @param string $message * @param string $file * @param integer $line * * @return Boolean * * @throws ErrorException */ public function errorHandler($level, $message, $file, $line) { if (0 !== error_reporting()) { throw new ErrorException($level, $message, $file, $line); } // error reporting turned off or more likely suppressed with @ return false; } /** * Runs definition callback. * * @param ContextInterface $context * * @return mixed * * @throws BehaviorException */ public function run(ContextInterface $context) { if (defined('BEHAT_ERROR_REPORTING')) { $errorLevel = BEHAT_ERROR_REPORTING; } else { $errorLevel = E_ALL ^ E_WARNING; } $callback = $this->getCallbackForContext($context); $values = $this->getValues(); if ($this->isClosure()) { array_unshift($values, $context); } set_error_handler(array($this, 'errorHandler'), $errorLevel); try { $return = call_user_func_array($callback, $values); } catch (\Exception $e) { restore_error_handler(); throw $e; } restore_error_handler(); return $return; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Given type step definition. * * @author Konstantin Kudryashov */ class Given extends Definition { /** * Returns definition type (Given|When|Then). * * @return string */ public function getType() { return 'Given'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Then type step definition. * * @author Konstantin Kudryashov */ class Then extends Definition { /** * Returns definition type (Given|When|Then). * * @return string */ public function getType() { return 'Then'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Step arguments transformation. * * @author Konstantin Kudryashov */ class Transformation extends Annotation implements TransformationInterface { private $regex; /** * Initializes transformation. * * @param callback $callback definition callback * @param string $regex definition regular expression * * @throws \InvalidArgumentException */ public function __construct($callback, $regex) { if (!is_callable($callback)) { throw new \InvalidArgumentException(sprintf( 'Transformation callback should be a valid callable, but %s given', gettype($callback) )); } parent::__construct($callback); $this->regex = $regex; } /** * Returns transformation regex. * * @return string */ public function getRegex() { return $this->regex; } /** * Transforms provided argument. * * @param string $translatedRegex * @param ContextInterface $context * @param mixed $argument * * @return mixed */ public function transform($translatedRegex, ContextInterface $context, $argument) { $callback = $this->getCallbackForContext($context); if ($argument instanceof TableNode) { $tableMatching = 'table:' . implode(',', $argument->getRow(0)); if (preg_match($translatedRegex, $tableMatching) || preg_match($this->regex, $tableMatching)) { return call_user_func($callback, $argument); } } elseif (is_string($argument) || $argument instanceof PyStringNode) { if (preg_match($translatedRegex, (string) $argument, $transformArguments) || preg_match($this->regex, (string) $argument, $transformArguments)) { array_shift($transformArguments); return call_user_func_array($callback, $transformArguments); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * When type step definition. * * @author Konstantin Kudryashov */ class When extends Definition { /** * Returns definition type (Given|When|Then). * * @return string */ public function getType() { return 'When'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Definition dispatcher. * * @author Konstantin Kudryashov */ class DefinitionDispatcher { private $transformations = array(); private $definitions = array(); private $proposalDispatcher; private $translator; /** * Initializes definition dispatcher. * * @param DefinitionProposalDispatcher $proposalDispatcher * @param TranslatorInterface $translator */ public function __construct(DefinitionProposalDispatcher $proposalDispatcher, TranslatorInterface $translator) { $this->proposalDispatcher = $proposalDispatcher; $this->translator = $translator; } /** * Adds definition to dispatcher. * * @param DefinitionInterface $definition * * @throws RedundantException */ public function addDefinition(DefinitionInterface $definition) { $regex = $definition->getRegex(); if (isset($this->definitions[$regex])) { throw new RedundantException($definition, $this->definitions[$regex]); } $this->definitions[$regex] = $definition; } /** * Returns array of available definitions. * * @return array array of hashes => array(regex => definition) */ public function getDefinitions() { return $this->definitions; } /** * Adds transformation to dispatcher. * * @param TransformationInterface $transformation */ public function addTransformation(TransformationInterface $transformation) { $this->transformations[] = $transformation; } /** * Returns array of available transformations. * * @return array array of argument transformers */ public function getTransformations() { return $this->transformations; } /** * Cleans dispatcher. */ public function clean() { $this->definitions = array(); $this->transformations = array(); } /** * Finds step definition, that match specified step. * * @param ContextInterface $context * @param StepNode $step * * @return Definition * * @uses loadDefinitions() * * @throws AmbiguousException if step description is ambiguous * @throws UndefinedException if step definition not found */ public function findDefinition(ContextInterface $context, StepNode $step) { $text = $step->getText(); $multiline = $step->getArguments(); $matches = array(); // find step to match foreach ($this->getDefinitions() as $origRegex => $definition) { $transRegex = $this->translateDefinitionRegex($origRegex, $step->getLanguage()); // if not regex really (string) - transform into it if (0 !== strpos($origRegex, '/')) { $origRegex = '/^'.preg_quote($origRegex, '/').'$/'; $transRegex = '/^'.preg_quote($transRegex, '/').'$/'; } if (preg_match($origRegex, $text, $arguments) || ($origRegex !== $transRegex && preg_match($transRegex, $text, $arguments))) { // prepare callback arguments $arguments = $this->prepareCallbackArguments( $context, $definition->getCallbackReflection(), array_slice($arguments, 1), $multiline ); // transform arguments foreach ($arguments as $num => $argument) { foreach ($this->getTransformations() as $trans) { $transRegex = $this->translateDefinitionRegex( $trans->getRegex(), $step->getLanguage() ); $newArgument = $trans->transform($transRegex, $context, $argument); if (null !== $newArgument) { $arguments[$num] = $newArgument; } } } // set matched definition $definition->setMatchedText($text); $definition->setValues($arguments); $matches[] = $definition; } } if (count($matches) > 1) { throw new AmbiguousException($text, $matches); } if (0 === count($matches)) { throw new UndefinedException($text); } return $matches[0]; } /** * Returns step definition for step node. * * @param ContextInterface $context * @param StepNode $step * * @return DefinitionSnippet */ public function proposeDefinition(ContextInterface $context, StepNode $step) { return $this->proposalDispatcher->propose($context, $step); } /** * Translates definition regex to provided language (if possible). * * @param string $regex regex to translate * @param string $language language * * @return string */ public function translateDefinitionRegex($regex, $language) { return $this->translator->trans($regex, array(), 'behat.definitions', $language); } /** * Merges found arguments with multiliners and maps them to the function callback signature. * * @param ContextInterface $context context instance * @param \ReflectionFunctionAbstract $refl callback reflection * @param array $arguments found arguments * @param array $multiline multiline arguments of the step * * @return array */ private function prepareCallbackArguments(ContextInterface $context, \ReflectionFunctionAbstract $refl, array $arguments, array $multiline) { $parametersRefl = $refl->getParameters(); if ($refl->isClosure()) { array_shift($parametersRefl); } $resulting = array(); foreach ($parametersRefl as $num => $parameterRefl) { if (isset($arguments[$parameterRefl->getName()])) { $resulting[] = $arguments[$parameterRefl->getName()]; } elseif (isset($arguments[$num])) { $resulting[] = $arguments[$num]; } } foreach ($multiline as $argument) { $resulting[] = $argument; } return $resulting; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Step definition. * * @author Konstantin Kudryashov */ interface DefinitionInterface { /** * Returns definition type (Given|When|Then). * * @return string */ public function getType(); /** * Runs definition callback. * * @param ContextInterface $context * * @return mixed * * @throws BehaviorException */ public function run(ContextInterface $context); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Definition snippet. * * @author Konstantin Kudryashov */ class DefinitionSnippet { private $type; private $template; private $steps = array(); /** * Initializes definition snippet. * * @param StepNode $step Step interested in snippet * @param string $template Definition snippet template */ public function __construct(StepNode $step, $template) { $type = $step->getType(); $this->type = in_array($type, array('Given', 'When', 'Then')) ? $type : 'Given'; $this->template = $template; $this->steps[] = $step; } /** * Adds step interested in this snippet. * * @param StepNode $step Step interested in snippet */ public function addStep(StepNode $step) { $this->steps[] = $step; } /** * Returns last step in list of steps. * * @return StepNode */ public function getLastStep() { return end($this->steps); } /** * Returns list of steps interested in this snippet. * * @return array */ public function getSteps() { return $this->steps; } /** * Returns snippet unique hash (ignoring step type). * * @return string */ public function getHash() { return md5($this->template); } /** * Returns definition snippet text. * * @return string */ public function getSnippet() { return sprintf($this->template, $this->type); } /** * Returns definition snippet text. * * @return string */ public function __toString() { return $this->getSnippet(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Closured step definitions loader. * * @author Konstantin Kudryashov */ class ClosuredDefinitionLoader implements DefinitionLoaderInterface { private $dispatcher; /** * Initializes loader. * * @param DefinitionDispatcher $dispatcher */ public function __construct(DefinitionDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } /** * Loads definitions from provided resource. * * @param mixed $resource */ public function load($resource) { $steps = $this; require_once($resource); } /** * Defines argument transformation. * * @param string $regex transformation regex (to find specific argument) * @param callback $callback transformation callback (must return transformed argument) */ public function Transform($regex, $callback) { $this->dispatcher->addTransformation(new Transformation($callback, $regex)); } /** * Defines a step with ->Given|When|Then|...('/regex/', callback) or * call a step with ->Given|When|Then|...('I enter "12" in the field', $world) or * even with arguments ->Given|When|Then|...('I fill up fields', $world, $table). * * @param string $type step type (Given|When|Then|...) * @param string $arguments step regex & callback * * @return ClosuredDefinitionLoader * * @throws RedundantException if step definition is already exists */ public function __call($type, $arguments) { if (2 == count($arguments) && is_callable($arguments[1])) { switch (strtolower($type)) { case 'when': $definition = new When($arguments[1], $arguments[0]); break; case 'then': $definition = new Then($arguments[1], $arguments[0]); break; case 'given': default: $definition = new Given($arguments[1], $arguments[0]); break; } $this->dispatcher->addDefinition($definition); } else { $text = array_shift($arguments); $world = array_shift($arguments); $step = new StepNode($type, $text); $step->setArguments($arguments); $definition = $this->dispatcher->findDefinition($world, $step); $definition->run($world); } return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Definition loader interface. * * @author Konstantin Kudryashov */ interface DefinitionLoaderInterface { /** * Loads definitions from provided resource. * * @param mixed $resource */ public function load($resource); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Annotated definitions proposal. * * @author Konstantin Kudryashov */ class AnnotatedDefinitionProposal implements DefinitionProposalInterface { private static $proposedMethods = array(); /** * Checks if loader supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context) { return true; } /** * Loads definitions and translations from provided context. * * @param ContextInterface $context * @param StepNode $step * * @return DefinitionSnippet */ public function propose(ContextInterface $context, StepNode $step) { $contextRefl = new \ReflectionObject($context); $contextClass = $contextRefl->getName(); $replacePatterns = array( "/(?<= |^)\\\'(?:((?!\\').)*)\\\'(?= |$)/", // Single quoted strings '/(?<= |^)\"(?:[^\"]*)\"(?= |$)/', // Double quoted strings '/(\d+)/', // Numbers ); $text = $step->getText(); $text = preg_replace('/([\/\[\]\(\)\\\^\$\.\|\?\*\+\'])/', '\\\\$1', $text); $regex = preg_replace( $replacePatterns, array( "\\'([^\']*)\\'", "\"([^\"]*)\"", "(\\d+)", ), $text ); preg_match('/' . $regex . '/', $step->getText(), $matches); $count = count($matches) - 1; $methodName = preg_replace($replacePatterns, '', $text); $methodName = Transliterator::transliterate($methodName, ' '); $methodName = preg_replace('/[^a-zA-Z\_\ ]/', '', $methodName); $methodName = str_replace(' ', '', ucwords($methodName)); if (0 !== strlen($methodName)) { $methodName[0] = strtolower($methodName[0]); } else { $methodName = 'stepDefinition1'; } // get method number from method name $methodNumber = 2; if (preg_match('/(\d+)$/', $methodName, $matches)) { $methodNumber = intval($matches[1]); } // check that proposed method name isn't arelady defined in context while ($contextRefl->hasMethod($methodName)) { $methodName = preg_replace('/\d+$/', '', $methodName); $methodName .= $methodNumber++; } // check that proposed method name haven't been proposed earlier if (isset(self::$proposedMethods[$contextClass])) { foreach (self::$proposedMethods[$contextClass] as $proposedRegex => $proposedMethod) { if ($proposedRegex !== $regex) { while ($proposedMethod === $methodName) { $methodName = preg_replace('/\d+$/', '', $methodName); $methodName .= $methodNumber++; } } } } self::$proposedMethods[$contextClass][$regex] = $methodName; $args = array(); for ($i = 0; $i < $count; $i++) { $args[] = "\$arg" . ($i + 1); } foreach ($step->getArguments() as $argument) { if ($argument instanceof PyStringNode) { $args[] = "PyStringNode \$string"; } elseif ($argument instanceof TableNode) { $args[] = "TableNode \$table"; } } $description = $this->generateSnippet($regex, $methodName, $args); return new DefinitionSnippet($step, $description); } protected function generateSnippet($regex, $methodName, array $args) { return sprintf(<< * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Closured definitions proposal. * * @author Konstantin Kudryashov */ class ClosuredDefinitionProposal implements DefinitionProposalInterface { /** * Checks if loader supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context) { return $context instanceof ClosuredContextInterface; } /** * Loads definitions and translations from provided context. * * @param ContextInterface $context * @param StepNode $step * * @return DefinitionSnippet */ public function propose(ContextInterface $context, StepNode $step) { $text = $step->getText(); $regex = preg_replace('/([\/\[\]\(\)\\\^\$\.\|\?\*\+\'])/', '\\\\$1', $text); $regex = preg_replace( array( "/(?<= |^)\\\'(?:((?!\\').)*)\\\'(?= |$)/", // Single quoted strings '/(?<= |^)\"(?:[^\"]*)\"(?= |$)/', // Double quoted strings '/(\d+)/', // Numbers ), array( "\\'([^\']*)\\'", "\"([^\"]*)\"", "(\\d+)", ), $regex ); preg_match('/' . $regex . '/', $text, $matches); $count = count($matches) - 1; $args = array("\$world"); for ($i = 0; $i < $count; $i++) { $args[] = "\$arg".($i + 1); } foreach ($step->getArguments() as $argument) { if ($argument instanceof PyStringNode) { $args[] = "\$string"; } elseif ($argument instanceof TableNode) { $args[] = "\$table"; } } $description = sprintf(<<%s('/^%s$/', function(%s) { throw new \Behat\Behat\Exception\PendingException(); }); PHP , '%s', $regex, implode(', ', $args) ); return new DefinitionSnippet($step, $description); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Definition proposals dispatcher. * * @author Konstantin Kudryashov */ class DefinitionProposalDispatcher { private $proposals = array(); /** * Adds proposal object to the dispatcher. * * @param DefinitionProposalInterface $proposal */ public function addProposal(DefinitionProposalInterface $proposal) { $this->proposals[] = $proposal; } /** * Returns step definition for step node. * * @param ContextInterface $context * @param StepNode $step * * @return DefinitionSnippet */ public function propose(ContextInterface $context, StepNode $step) { foreach ($this->proposals as $proposal) { if ($proposal->supports($context)) { return $proposal->propose($context, $step); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Definition proposal interface. * * @author Konstantin Kudryashov */ interface DefinitionProposalInterface { /** * Checks if loader supports provided context. * * @param ContextInterface $context * * @return Boolean */ public function supports(ContextInterface $context); /** * Loads definitions and translations from provided context. * * @param ContextInterface $context * @param StepNode $step * * @return DefinitionSnippet */ public function propose(ContextInterface $context, StepNode $step); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Step transformation. * * @author Konstantin Kudryashov */ interface TransformationInterface { /** * Transforms provided argument. * * @param string $translatedRegex * @param ContextInterface $context * @param mixed $argument * * @return mixed */ public function transform($translatedRegex, ContextInterface $context, $argument); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat service container extension. * * @author Konstantin Kudryashov */ class BehatExtension implements ExtensionInterface { protected $basePath; protected $processor; protected $configuration; protected $extensionManager; /** * Initializes configuration. */ public function __construct($basePath) { $this->basePath = $basePath; $this->processor = new Processor(); $this->configuration = new Configuration\Configuration(); $this->extensionManager = new ExtensionManager($basePath); } /** * {@inheritdoc} */ public function load(array $configs, ContainerBuilder $container) { $this->loadDefaults($container); $container->setParameter('behat.paths.base', $this->basePath); // set internal encoding to UTF-8 if ('UTF-8' !== mb_internal_encoding()) { mb_internal_encoding('UTF-8'); } // activate and normalize specified by user extensions foreach ($configs as $i => $config) { if (isset($config['extensions'])) { $extensions = array(); foreach ($config['extensions'] as $id => $extensionConfig) { $activationId = $this->extensionManager->activateExtension($id); $extensions[$activationId] = $extensionConfig; } $configs[$i]['extensions'] = $extensions; } } // set list of extensions to container $container->setParameter('behat.extension.classes', $this->extensionManager->getExtensionClasses() ); // normalize and merge the actual configuration $tree = $this->configuration->getConfigTree($this->extensionManager); $config = $this->processor->process($tree, $configs); if (isset($config['paths'])) { $this->loadPathsConfiguration($config['paths'], $container); } if (isset($config['context'])) { $this->loadContextConfiguration($config['context'], $container); } if (isset($config['formatter'])) { $this->loadFormatterConfiguration($config['formatter'], $container); } if (isset($config['options'])) { $this->loadOptionsConfiguration($config['options'], $container); } if (isset($config['filters'])) { $this->loadFiltersConfiguration($config['filters'], $container); } if (isset($config['extensions'])) { $this->loadExtensionsConfiguration($config['extensions'], $container); } $this->resolveRelativePaths($container); $this->addCompilerPasses($container); } /** * Loads paths configuration. * * @param array $config * @param ContainerBuilder $container */ protected function loadPathsConfiguration(array $config, ContainerBuilder $container) { foreach ($config as $key => $path) { $container->setParameter('behat.paths.'.$key, $path); } } /** * Loads context configuration. * * @param array $config * @param ContainerBuilder $container */ protected function loadContextConfiguration(array $config, ContainerBuilder $container) { if ('FeatureContext' !== $config['class']) { $container->setParameter('behat.context.class.force', true); } foreach ($config as $key => $value) { $container->setParameter('behat.context.'.$key, $value); } } /** * Loads formatter(s) configuration. * * @param array $config * @param ContainerBuilder $container */ protected function loadFormatterConfiguration(array $config, ContainerBuilder $container) { foreach ($config as $key => $value) { $container->setParameter('behat.formatter.'.$key, $value); } } /** * Loads behat options configuration. * * @param array $config * @param ContainerBuilder $container */ protected function loadOptionsConfiguration(array $config, ContainerBuilder $container) { foreach ($config as $key => $value) { $container->setParameter('behat.options.'.$key, $value); } } /** * Loads gherkin filters configuration. * * @param array $config * @param ContainerBuilder $container */ protected function loadFiltersConfiguration(array $config, ContainerBuilder $container) { foreach ($config as $key => $filter) { $container->setParameter('gherkin.filters.'.$key, $filter); } } /** * Loads extensions configuration. * * @param array $config * @param ContainerBuilder $container */ protected function loadExtensionsConfiguration(array $config, ContainerBuilder $container) { foreach ($config as $id => $extensionConfig) { // load extension from manager $extension = $this->extensionManager->getExtension($id); // create temporary container $tempContainer = new ContainerBuilder(new ParameterBag(array( 'behat.paths.base' => $container->getParameter('behat.paths.base'), 'behat.extension.classes' => $container->getParameter('behat.extension.classes'), ))); $tempContainer->addObjectResource($extension); // load extension into temporary container $extension->load($extensionConfig, $tempContainer); // merge temporary container into normal one $container->merge($tempContainer); // add extension compiler passes array_map(array($container, 'addCompilerPass'), $extension->getCompilerPasses()); } } /** * Resolves relative behat.paths.* parameters in container. * * @param ContainerBuilder $container */ protected function resolveRelativePaths(ContainerBuilder $container) { $featuresPath = $container->getParameter('behat.paths.features'); $bootstrapPath = $container->getParameter('behat.paths.bootstrap'); $parameterBag = $container->getParameterBag(); $featuresPath = $parameterBag->resolveValue($featuresPath); $bootstrapPath = $parameterBag->resolveValue($bootstrapPath); if (!$this->isAbsolutePath($featuresPath)) { $featuresPath = $this->basePath.DIRECTORY_SEPARATOR.$featuresPath; $container->setParameter('behat.paths.features', $featuresPath); } if (!$this->isAbsolutePath($bootstrapPath)) { $bootstrapPath = $this->basePath.DIRECTORY_SEPARATOR.$bootstrapPath; $container->setParameter('behat.paths.bootstrap', $bootstrapPath); } } /** * Adds core compiler passes to container. * * @param ContainerBuilder $container */ protected function addCompilerPasses(ContainerBuilder $container) { $container->addCompilerPass(new Compiler\ConsoleProcessorsPass()); $container->addCompilerPass(new Compiler\GherkinLoadersPass()); $container->addCompilerPass(new Compiler\ContextLoadersPass()); $container->addCompilerPass(new Compiler\ContextClassGuessersPass()); $container->addCompilerPass(new Compiler\ContextInitializersPass()); $container->addCompilerPass(new Compiler\DefinitionProposalsPass()); $container->addCompilerPass(new Compiler\FormattersPass()); $container->addCompilerPass(new Compiler\EventSubscribersPass()); } /** * {@inheritdoc} */ public function getXsdValidationBasePath() { return __DIR__ . '/config/schema'; } /** * {@inheritdoc} */ public function getNamespace() { return 'http://behat.com/schema/dic/behat'; } /** * {@inheritdoc} */ public function getAlias() { return 'behat'; } /** * {@inheritdoc} */ protected function loadDefaults($container) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/config')); $loader->load('behat.xml'); $behatClassLoaderReflection = new \ReflectionClass('Behat\Behat\Console\BehatApplication'); $gherkinParserReflection = new \ReflectionClass('Behat\Gherkin\Parser'); $behatLibPath = dirname($behatClassLoaderReflection->getFilename()) . '/../../../../'; $gherkinLibPath = dirname($gherkinParserReflection->getFilename()) . '/../../../'; $container->setParameter('gherkin.paths.lib', $gherkinLibPath); $container->setParameter('behat.paths.lib', $behatLibPath); } /** * Returns whether the file path is an absolute path. * * @param string $file A file path * * @return Boolean */ private function isAbsolutePath($file) { if ($file[0] == '/' || $file[0] == '\\' || (strlen($file) > 3 && ctype_alpha($file[0]) && $file[1] == ':' && ($file[2] == '\\' || $file[2] == '/') ) || null !== parse_url($file, PHP_URL_SCHEME) ) { return true; } return false; } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Command pass - registers all available command processors. * * @author Konstantin Kudryashov */ class ConsoleProcessorsPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('behat.console.processor.aggregate')) { return; } $aggregator = $container->getDefinition('behat.console.processor.aggregate'); foreach ($container->findTaggedServiceIds('behat.console.processor') as $id => $attributes) { $aggregator->addMethodCall('addProcessor', array(new Reference($id))); } } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Context class guessers pass - registers all available context class guessers. * * @author Konstantin Kudryashov */ class ContextClassGuessersPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('behat.context.dispatcher')) { return; } $dispatcher = $container->getDefinition('behat.context.dispatcher'); // Sorts guessers by priority (0 by default). // Guessers with higher priority go first. // If two guessers has same priority, then the // lastly added one goes first. Means guessers // from later extensions have more priority. $prioritizedGuessers = array(); foreach ($container->findTaggedServiceIds('behat.context.class_guesser') as $id => $attributes) { $attributes = isset($attributes[0]) ? $attributes[0] : array(); $priority = intval(isset($attributes['priority']) ? $attributes['priority'] : 0); if (!isset($prioritizedGuessers[$priority])) { $prioritizedGuessers[$priority] = array(); } array_unshift($prioritizedGuessers[$priority], new Reference($id)); } krsort($prioritizedGuessers); foreach ($prioritizedGuessers as $guessers) { foreach ($guessers as $guesser) { $dispatcher->addMethodCall('addClassGuesser', array($guesser)); } } } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Context initializers pass - registers all available context initializers. * * @author Konstantin Kudryashov */ class ContextInitializersPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('behat.context.dispatcher')) { return; } $dispatcher = $container->getDefinition('behat.context.dispatcher'); foreach ($container->findTaggedServiceIds('behat.context.initializer') as $id => $attributes) { $dispatcher->addMethodCall('addInitializer', array(new Reference($id))); } } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Context loaders pass - registers all available context loaders. * * @author Konstantin Kudryashov */ class ContextLoadersPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('behat.context.reader')) { return; } $readerDefinition = $container->getDefinition('behat.context.reader'); foreach ($container->findTaggedServiceIds('behat.context.loader') as $id => $attributes) { $readerDefinition->addMethodCall('addLoader', array(new Reference($id))); } } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Definition proposals pass - registers all available definition proposals. * * @author Konstantin Kudryashov */ class DefinitionProposalsPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('behat.definition.proposal_dispatcher')) { return; } $dispatcher = $container->getDefinition('behat.definition.proposal_dispatcher'); foreach ($container->findTaggedServiceIds('behat.definition.proposal') as $id => $attributes) { $dispatcher->addMethodCall('addProposal', array(new Reference($id))); } } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Event subscribers pass - registers all available event subscribers. * * @author Konstantin Kudryashov */ class EventSubscribersPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('behat.event_dispatcher')) { return; } $dispatcherDefinition = $container->getDefinition('behat.event_dispatcher'); foreach ($container->findTaggedServiceIds('behat.event_subscriber') as $id => $attributes) { foreach ($attributes as $attribute) { $priority = isset($attribute['priority']) ? intval($attribute['priority']) : 0; $dispatcherDefinition->addMethodCall( 'addSubscriber', array(new Reference($id), $priority) ); } } } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Formatters pass - registers all available formatters. * * @author Konstantin Kudryashov */ class FormattersPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('behat.formatter.manager')) { return; } $manager = $container->getDefinition('behat.formatter.manager'); foreach ($container->findTaggedServiceIds('behat.formatter.dispatcher') as $id => $attributes) { $manager->addMethodCall('addDispatcher', array(new Reference($id))); } } } * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ /** * Gherkin loaders pass - registers all available Gherkin loaders. * * @author Konstantin Kudryashov */ class GherkinLoadersPass implements CompilerPassInterface { /** * Processes container. * * @param ContainerBuilder $container */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('gherkin')) { return; } $gherkinDefinition = $container->getDefinition('gherkin'); foreach ($container->findTaggedServiceIds('gherkin.loader') as $id => $attributes) { $gherkinDefinition->addMethodCall('addLoader', array(new Reference($id))); } } } null %gherkin.paths.lib%/i18n.php null %behat.paths.lib%/i18n.php %behat.paths.base%/features %behat.paths.features%/bootstrap pretty Behat\Behat\Formatter\FormatterDispatcher Behat\Behat\Formatter\FormatterManager null null null null null Behat\Gherkin\Gherkin Behat\Gherkin\Parser Behat\Gherkin\Lexer Behat\Gherkin\Loader\GherkinFileLoader Behat\Gherkin\Loader\DirectoryLoader Behat\Behat\Gherkin\Loader\FeatureSuiteLoader Behat\Gherkin\Keywords\CachedArrayKeywords %gherkin.paths.i18n% Behat\Gherkin\Keywords\KeywordsDumper null null Behat\Behat\Console\Command\BehatCommand Behat\Behat\Console\Processor\AggregateProcessor Behat\Behat\Console\Processor\LocatorProcessor Behat\Behat\Console\Processor\ContextReaderProcessor Behat\Behat\Console\Processor\InitProcessor Behat\Behat\Console\Processor\FormatProcessor Behat\Behat\Console\Processor\HelpProcessor Behat\Behat\Console\Processor\GherkinProcessor Behat\Behat\Console\Processor\RunProcessor FeatureContext false Behat\Behat\Context\ContextDispatcher Behat\Behat\Context\ContextReader Behat\Behat\Context\ClassGuesser\PredefinedClassGuesser Behat\Behat\Context\Loader\TranslatedLoader Behat\Behat\Context\Loader\AnnotatedLoader Behat\Behat\Context\Loader\ClosuredLoader Behat\Behat\Definition\DefinitionDispatcher Behat\Behat\Definition\Loader\ClosuredDefinitionLoader Behat\Behat\Definition\Proposal\DefinitionProposalDispatcher Behat\Behat\Definition\Proposal\ClosuredDefinitionProposal Behat\Behat\Definition\Proposal\AnnotatedDefinitionProposal Behat\Behat\Hook\HookDispatcher Behat\Behat\Hook\Loader\ClosuredHookLoader Behat\Behat\Tester\FeatureTester Behat\Behat\Tester\BackgroundTester Behat\Behat\Tester\ScenarioTester Behat\Behat\Tester\OutlineTester Behat\Behat\Tester\StepTester Symfony\Component\Translation\Translator Symfony\Component\Translation\MessageSelector Symfony\Component\Translation\Loader\XliffFileLoader Symfony\Component\Translation\Loader\YamlFileLoader Symfony\Component\Translation\Loader\PhpFileLoader Symfony\Component\Translation\Loader\ArrayLoader Symfony\Component\EventDispatcher\EventDispatcher Behat\Behat\DataCollector\LoggerDataCollector Behat\Behat\HelpPrinter\DefinitionsPrinter Behat\Behat\HelpPrinter\StorySyntaxPrinter %gherkin.paths.lib%/i18n.php %behat.paths.features% %behat.context.parameters% %behat.context.class% %behat.context.class.force% Behat\Behat\Formatter\PrettyFormatter pretty Prints the feature as is. Behat\Behat\Formatter\ProgressFormatter progress Prints one character per step. Behat\Behat\Formatter\HtmlFormatter html Generates a nice looking HTML report. Behat\Behat\Formatter\JUnitFormatter junit Generates a report similar to Ant+JUnit. Behat\Behat\Formatter\FailedScenariosFormatter failed Prints list of failed scenarios. Behat\Behat\Formatter\SnippetsFormatter snippets Prints only snippets for undefined steps. en en xliff yaml php array * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * This class contains the configuration information for the Behat * * This information is solely responsible for how the different configuration * sections are normalized, and merged. * * @author Konstantin Kudryashov */ class Configuration { /** * Generates the configuration tree. * * @param ExtensionManager $extensionManager * * @return NodeInterface */ public function getConfigTree(ExtensionManager $extensionManager) { $tree = new TreeBuilder(); $root = $this->appendConfigChildrens($tree); $extensionsNode = $root->fixXmlConfig('extension')->children()->arrayNode('extensions')->children(); foreach ($extensionManager->getExtensions() as $id => $extension) { $extensionNode = $extensionsNode->arrayNode($id); $extension->getConfig($extensionNode); } return $tree->buildTree(); } /** * Appends config childrens to configuration tree. * * @param TreeBuilder $tree tree builder * * @return ArrayNodeDefinition */ protected function appendConfigChildrens(TreeBuilder $tree) { return $tree->root('behat')-> children()-> arrayNode('paths')-> children()-> scalarNode('features')-> defaultValue('%behat.paths.base%/features')-> end()-> scalarNode('bootstrap')-> defaultValue('%behat.paths.features%/bootstrap')-> end()-> end()-> end()-> end()-> children()-> arrayNode('filters')-> children()-> scalarNode('name')->defaultNull()->end()-> scalarNode('tags')->defaultNull()->end()-> end()-> end()-> end()-> children()-> arrayNode('formatter')-> fixXmlConfig('parameter')-> children()-> scalarNode('name')-> defaultValue('pretty')-> end()-> arrayNode('classes')-> useAttributeAsKey('name')-> prototype('scalar')->end()-> end()-> arrayNode('parameters')-> useAttributeAsKey('name')-> prototype('variable')->end()-> end()-> end()-> end()-> end()-> children()-> arrayNode('options')-> fixXmlConfig('option')-> children()-> scalarNode('cache')-> defaultNull()-> end()-> booleanNode('strict')-> defaultNull()-> end()-> booleanNode('dry_run')-> defaultNull()-> end()-> scalarNode('rerun')-> defaultNull()-> end()-> scalarNode('append_snippets')-> defaultNull()-> end()-> end()-> end()-> end()-> children()-> arrayNode('context')-> fixXmlConfig('parameter')-> children()-> scalarNode('class')-> defaultValue('FeatureContext')-> end()-> arrayNode('parameters')-> useAttributeAsKey('name')-> prototype('variable')->end()-> end()-> end()-> end()-> end() ; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat configuration reader. * * @author Konstantin Kudryashov */ class Loader { private $configPath; private $profileFound; /** * Constructs reader. * * @param string $configPath Configuration file path */ public function __construct($configPath = null) { $this->configPath = $configPath; } /** * Reads configuration sequense for specific profile. * * @param string $profile Profile name * * @return array */ public function loadConfiguration($profile = 'default') { $configs = array(); $this->profileFound = false; // first is ENV config foreach ($this->loadEnvironmentConfiguration() as $config) { $configs[] = $config; } // second is file configuration (if there is some) if ($this->configPath) { foreach ($this->loadFileConfiguration($this->configPath, $profile) as $config) { $configs[] = $config; } } // if specific profile has not been found if ('default' !== $profile && !$this->profileFound) { throw new \RuntimeException(sprintf( 'Configuration for profile "%s" can not be found.', $profile )); } return $configs; } /** * Loads information from ENV variable. * * @return array */ protected function loadEnvironmentConfiguration() { $configs = array(); if ($envConfig = getenv('BEHAT_PARAMS')) { if (null === $config = @json_decode($envConfig, true)) { @parse_str($envConfig, $config); $config = $this->normalizeRawConfiguration($config); } $configs[] = $config; } return $configs; } /** * Loads information from YAML configuration file. * * @param string $configPath Config file path * @param string $profile Profile name * * @return array * * @throws \RuntimeException */ protected function loadFileConfiguration($configPath, $profile) { if (!is_file($configPath) || !is_readable($configPath)) { throw new \RuntimeException("Config file \"$configPath\" not found"); } $basePath = rtrim(dirname($configPath), DIRECTORY_SEPARATOR); $config = Yaml::parse($configPath); $configs = array(); // first load default profile from current config, but only if custom profile requested if ('default' !== $profile && isset($config['default'])) { $configs[] = $config['default']; } // then recursively load profiles from imports if (isset($config['imports']) && is_array($config['imports'])) { foreach ($config['imports'] as $path) { foreach ($this->parseImport($basePath, $path, $profile) as $importConfig) { $configs[] = $importConfig; } } } // then load specific profile from current config if (isset($config[$profile])) { $configs[] = $config[$profile]; $this->profileFound = true; } return $configs; } private function parseImport($basePath, $path, $profile) { if (!file_exists($path) && file_exists($basePath.DIRECTORY_SEPARATOR.$path)) { $path = $basePath.DIRECTORY_SEPARATOR.$path; } if (!file_exists($path)) { throw new \RuntimeException(sprintf( 'Can not import config "%s": file not found.', $path )); } return $this->loadFileConfiguration($path, $profile); } private function normalizeRawConfiguration(array $config) { $normalize = function($value) { if ('true' === $value || 'false' === $value) { return 'true' === $value; } if (is_numeric($value)) { return ctype_digit($value) ? intval($value) : floatval($value); } return $value; }; if (isset($config['formatter']['parameters'])) { $config['formatter']['parameters'] = array_map( $normalize, $config['formatter']['parameters'] ); } if (isset($config['context']['parameters'])) { $config['context']['parameters'] = array_map( $normalize, $config['context']['parameters'] ); } return $config; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Background event. * * @author Konstantin Kudryashov */ class BackgroundEvent extends Event implements EventInterface { private $background; private $result; private $skipped; /** * Initializes background event. * * @param BackgroundNode $background * @param integer $result * @param Boolean $skipped */ public function __construct(BackgroundNode $background, $result = null, $skipped = false) { $this->background = $background; $this->result = $result; $this->skipped = $skipped; } /** * Returns background node. * * @return BackgroundNode */ public function getBackground() { return $this->background; } /** * Return background tester result code. * * @return integer */ public function getResult() { return $this->result; } /** * Checks whether background were skipped. * * @return Boolean */ public function isSkipped() { return $this->skipped; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base scenario event. * * @author Konstantin Kudryashov */ abstract class BaseScenarioEvent extends Event implements EventInterface { private $context; private $result; private $skipped; /** * Initializes scenario event. * * @param ContextInterface $context * @param integer $result * @param Boolean $skipped */ public function __construct(ContextInterface $context, $result = null, $skipped = false) { $this->context = $context; $this->result = $result; $this->skipped = $skipped; } /** * Returns context object. * * @return ContextInterface */ public function getContext() { return $this->context; } /** * Returns scenario tester result code. * * @return integer */ public function getResult() { return $this->result; } /** * Checks whether scenario were skipped. * * @return Boolean */ public function isSkipped() { return $this->skipped; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat event interface. * * @author Konstantin Kudryashov */ interface EventInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Feature event. * * @author Konstantin Kudryashov */ class FeatureEvent extends Event implements EventInterface { private $feature; private $result; private $parameters; /** * Initializes feature event. * * @param FeatureNode $feature * @param mixed $parameters * @param integer $result */ public function __construct(FeatureNode $feature, $parameters, $result = null) { $this->feature = $feature; $this->parameters = $parameters; $this->result = $result; } /** * Returns feature node. * * @return FeatureNode */ public function getFeature() { return $this->feature; } /** * Returns context parameters. * * @return mixed */ public function getContextParameters() { return $this->parameters; } /** * Returns feature tester result code. * * @return integer */ public function getResult() { return $this->result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Outline event. * * @author Konstantin Kudryashov */ class OutlineEvent extends Event implements EventInterface { private $outline; private $result; /** * Initializes outline event. * * @param OutlineNode $outline * @param integer $result */ public function __construct(OutlineNode $outline, $result = null) { $this->outline = $outline; $this->result = $result; } /** * Returns outline node. * * @return OutlineNode */ public function getOutline() { return $this->outline; } /** * Returns outline tester result code. * * @return integer */ public function getResult() { return $this->result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Outline example event. * * @author Konstantin Kudryashov */ class OutlineExampleEvent extends BaseScenarioEvent { private $outline; private $iteration; /** * Initializes outline example event. * * @param OutlineNode $outline * @param integer $iteration iteration number * @param ContextInterface $context * @param integer $result * @param Boolean $skipped */ public function __construct(OutlineNode $outline, $iteration, ContextInterface $context, $result = null, $skipped = false) { parent::__construct($context, $result, $skipped); $this->outline = $outline; $this->iteration = $iteration; } /** * Returns outline node. * * @return OutlineNode */ public function getOutline() { return $this->outline; } /** * Returns example number on which event occurs. * * @return integer */ public function getIteration() { return $this->iteration; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Scenario event. * * @author Konstantin Kudryashov */ class ScenarioEvent extends BaseScenarioEvent { private $scenario; /** * Initializes scenario event. * * @param ScenarioNode $scenario * @param ContextInterface $context * @param integer $result * @param Boolean $skipped */ public function __construct(ScenarioNode $scenario, ContextInterface $context, $result = null, $skipped = false) { $this->scenario = $scenario; parent::__construct($context, $result, $skipped); } /** * Returns scenario node. * * @return ScenarioNode */ public function getScenario() { return $this->scenario; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Step event. * * @author Konstantin Kudryashov */ class StepEvent extends Event implements EventInterface { const PASSED = 0; const SKIPPED = 1; const PENDING = 2; const UNDEFINED = 3; const FAILED = 4; private $step; private $parent; private $context; private $result; private $definition; private $exception; private $snippet; /** * Initializes step event. * * @param StepNode $step * @param ScenarioNode $parent * @param ContextInterface $context * @param integer $result * @param DefinitionInterface $definition * @param \Exception $exception * @param DefinitionSnippet $snippet */ public function __construct(StepNode $step, ScenarioNode $parent, ContextInterface $context, $result = null, DefinitionInterface $definition = null, \Exception $exception = null, DefinitionSnippet $snippet = null) { $this->step = $step; $this->parent = $parent; $this->context = $context; $this->result = $result; $this->definition = $definition; $this->exception = $exception; $this->snippet = $snippet; } /** * Returns step node. * * @return StepNode */ public function getStep() { return $this->step; } /** * Returns logical parent to the step, which is always a ScenarioNode. * * @return ScenarioNode */ public function getLogicalParent() { return $this->parent; } /** * Returns context object. * * @return ContextInterface */ public function getContext() { return $this->context; } /** * Returns step tester result code. * * @return integer */ public function getResult() { return $this->result; } /** * Returns step definition object. * * @return DefinitionInterface */ public function getDefinition() { return $this->definition; } /** * Checks whether event contains step definition. * * @return Boolean */ public function hasDefinition() { return null !== $this->getDefinition(); } /** * Returns step tester exception. * * @return \Exception */ public function getException() { return $this->exception; } /** * Checks whether event contains exception. * * @return Boolean */ public function hasException() { return null !== $this->getException(); } /** * Returns step snippet. * * @return DefinitionSnippet */ public function getSnippet() { return $this->snippet; } /** * Checks whether event contains snippet. * * @return Boolean */ public function hasSnippet() { return null !== $this->getSnippet(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Suite event. * * @author Konstantin Kudryashov */ class SuiteEvent extends Event implements EventInterface { private $logger; private $completed; private $parameters; /** * Initializes suite event. * * @param LoggerDataCollector $logger suite logger * @param mixed $parameters context parameters * @param Boolean $completed is suite completed */ public function __construct(LoggerDataCollector $logger, $parameters, $completed) { $this->logger = $logger; $this->parameters = $parameters; $this->completed = (Boolean) $completed; } /** * Returns suite logger. * * @return LoggerDataCollector */ public function getLogger() { return $this->logger; } /** * Returns context parameters. * * @return mixed */ public function getContextParameters() { return $this->parameters; } /** * Checks whether test suite was completed entirely. * * @return Boolean */ public function isCompleted() { return $this->completed; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Ambiguous exception. * * @author Konstantin Kudryashov */ class AmbiguousException extends BehaviorException { protected $text; protected $matches = array(); /** * Initializes ambiguous exception. * * @param string $text step description * @param array $matches ambigious matches (array of Definition's) */ public function __construct($text, array $matches) { $this->text = $text; $this->matches = $matches; $message = sprintf("Ambiguous match of \"%s\":", $text); foreach ($matches as $definition) { $message .= sprintf("\nto `%s` from %s", $definition->getRegex(), $definition->getPath()); } parent::__construct($message); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract Behat exception. * * @author Konstantin Kudryashov */ class BehaviorException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Error handler exception. * * @author Konstantin Kudryashov */ class ErrorException extends BehaviorException { private $levels = array( E_WARNING => 'Warning', E_NOTICE => 'Notice', E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', E_STRICT => 'Runtime Notice', E_RECOVERABLE_ERROR => 'Catchable Fatal Error', ); /** * Initializes error handler exception. * * @param string $level error level * @param string $message error message * @param string $file error file * @param string $line error line */ public function __construct($level, $message, $file, $line) { parent::__construct(sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line )); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat exception. * * @author Konstantin Kudryashov */ abstract class Exception extends \Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat Formatter exception. * * @author Konstantin Kudryashov */ class FormatterException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Pending exception (throw this to mark step as "pending"). * * @author Konstantin Kudryashov */ class PendingException extends BehaviorException { /** * Initializes pending exception. * * @param string $text TODO text */ public function __construct($text = 'write pending definition') { parent::__construct(sprintf('TODO: %s', $text)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Redundant exception. * * @author Konstantin Kudryashov */ class RedundantException extends BehaviorException { /** * Initializes redundant exception. * * @param DefinitionInterface $step2 duplicate step definition * @param DefinitionInterface $step1 firstly matched step definition */ public function __construct(DefinitionInterface $step2, DefinitionInterface $step1) { $message = sprintf("Step \"%s\" is already defined in %s\n\n%s\n%s", $step2->getRegex(), $step1->getPath(), $step1->getPath(), $step2->getPath() ); parent::__construct($message); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Undefined exception. * * @author Konstantin Kudryashov */ class UndefinedException extends BehaviorException { protected $text; /** * Initialize undefined exception. * * @param string $text step text */ public function __construct($text) { $this->text = $text; parent::__construct(sprintf('Undefined step "%s"', $text)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat base extension class. * * @author Konstantin Kudryashov */ class Extension implements ExtensionInterface { /** * Loads a specific configuration. * * @param array $config Extension configuration hash (from behat.yml) * @param ContainerBuilder $container ContainerBuilder instance */ public function load(array $config, ContainerBuilder $container) { $path = rtrim($this->getServiceDefinitionsPath(), DIRECTORY_SEPARATOR); $name = $this->getServiceDefinitionsName(); if (file_exists($path.DIRECTORY_SEPARATOR.($file = $name.'.xml'))) { $loader = new XmlFileLoader($container, new FileLocator($path)); $loader->load($file); } if (file_exists($path.DIRECTORY_SEPARATOR.($file = $name.'.yml'))) { $loader = new YamlFileLoader($container, new FileLocator($path)); $loader->load($file); } $container->setParameter($this->getExtensionName().'.parameters', $config); } /** * Setups configuration for current extension. * * @param ArrayNodeDefinition $builder */ public function getConfig(ArrayNodeDefinition $builder) { $builder ->useAttributeAsKey('name') ->prototype('variable') ; } /** * Returns compiler passes used by this extension. * * @return array */ public function getCompilerPasses() { return array(); } /** * Returns extension name used to store extension parameters in DIC. * * @return string */ protected function getExtensionName() { return strtolower(ltrim(preg_replace('/[A-Z]/', "_$0", get_class($this)), '_')); } /** * Returns name of the service definition config without extension and path. * * @return string */ protected function getServiceDefinitionsName() { return get_class($this).'Services'; } /** * Returns service definition configs path. * * @return string */ protected function getServiceDefinitionsPath() { $refl = new \ReflectionClass($this); return dirname($refl->getFileName()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Behat extension interface. * * @author Konstantin Kudryashov */ interface ExtensionInterface { /** * Loads a specific configuration. * * @param array $config Extension configuration hash (from behat.yml) * @param ContainerBuilder $container ContainerBuilder instance */ public function load(array $config, ContainerBuilder $container); /** * Setups configuration for current extension. * * @param ArrayNodeDefinition $builder */ public function getConfig(ArrayNodeDefinition $builder); /** * Returns compiler passes used by this extension. * * @return array */ public function getCompilerPasses(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extensions manager. * * @author Konstantin Kudryashov */ class ExtensionManager { private $basePath; private $extensions = array(); /** * Initializes manager. * * @param string $basePath base path where to search extension files */ public function __construct($basePath) { $this->basePath = $basePath; } /** * Activate extension by its id. * * @param string $id phar file name, php file name, class name */ public function activateExtension($id) { $extensionId = strtolower(preg_replace('/[^a-zA-Z0-9]/', '_', $id)); if (!isset($this->extensions[$extensionId])) { $this->extensions[$extensionId] = $this->initializeExtension($id); } return $extensionId; } /** * Returns specific extension by its id. * * @param string $id * * @return ExtensionInterface * * @throws \RuntimeException */ public function getExtension($id) { if (!isset($this->extensions[$id])) { throw new \RuntimeException( sprintf('Extension "%s" has not been activated.', $id) ); } return $this->extensions[$id]; } /** * Returns all activated extensions. * * @return array */ public function getExtensions() { return $this->extensions; } /** * Returns activated extension classes. * * @return array */ public function getExtensionClasses() { return array_unique( array_map( function($extension) { return get_class($extension); }, $this->extensions ) ); } /** * Initializes extension by id. * * @param string $id * * @return ExtensionInterface * * @throws \RuntimeException */ protected function initializeExtension($id) { $extension = null; if (class_exists($id)) { $extension = new $id; } elseif (file_exists($this->basePath.DIRECTORY_SEPARATOR.$id)) { $extension = require($this->basePath.DIRECTORY_SEPARATOR.$id); } else { $extension = require($id); } if (null === $extension) { throw new \RuntimeException(sprintf( '"%s" extension could not be found.', $id )); } if (!is_object($extension)) { throw new \RuntimeException(sprintf( '"%s" extension could not be initialized.', $id )); } if (!$extension instanceof ExtensionInterface) { throw new \RuntimeException(sprintf( '"%s" extension class should implement ExtensionInterface.', get_class($extension) )); } return $extension; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Console formatter. * * @author Konstantin Kudryashov */ abstract class ConsoleFormatter implements FormatterInterface { /** * Formatter parameters. * * @var ParameterBag */ protected $parameters; private $translator; private $console; /** * Initialize formatter. * * @uses getDefaultParameters() */ public function __construct() { $defaultLanguage = null; if (($locale = getenv('LANG')) && preg_match('/^([a-z]{2})/', $locale, $matches)) { $defaultLanguage = $matches[1]; } $this->parameters = new ParameterBag(array_merge(array( 'language' => $defaultLanguage, 'verbose' => false, 'decorated' => true, 'time' => true, 'base_path' => null, 'support_path' => null, 'output' => null, 'output_path' => null, 'output_styles' => array(), 'output_decorate' => null, 'snippets' => true, 'snippets_paths' => false, 'paths' => true, 'expand' => false, 'multiline_arguments' => true, ), $this->getDefaultParameters())); } /** * Set formatter translator. * * @param Translator $translator */ final public function setTranslator(Translator $translator) { $this->translator = $translator; } /** * Returns default parameters to construct ParameterBag. * * @return array */ abstract protected function getDefaultParameters(); /** * Checks if current formatter has parameter. * * @param string $name * * @return Boolean */ final public function hasParameter($name) { return $this->parameters->has($name); } /** * Sets formatter parameter. * * @param string $name * @param mixed $value */ final public function setParameter($name, $value) { $this->parameters->set($name, $value); } /** * Returns parameter value. * * @param string $name * * @return mixed */ final public function getParameter($name) { return $this->parameters->get($name); } /** * Returns color code from tester result status code. * * @param integer $result tester result status code * * @return string passed|pending|skipped|undefined|failed */ final protected function getResultColorCode($result) { switch ($result) { case StepEvent::PASSED: return 'passed'; case StepEvent::SKIPPED: return 'skipped'; case StepEvent::PENDING: return 'pending'; case StepEvent::UNDEFINED: return 'undefined'; case StepEvent::FAILED: return 'failed'; } } /** * Writes message(s) to output console. * * @param string|array $messages message or array of messages * @param Boolean $newline do we need to append newline after messages * * @uses getWritingConsole() */ final protected function write($messages, $newline = false) { $this->getWritingConsole()->write($messages, $newline); } /** * Writes newlined message(s) to output console. * * @param string|array $messages message or array of messages */ final protected function writeln($messages = '') { $this->write($messages, true); } /** * Returns console instance, prepared to write. * * @return StreamOutput * * @uses createOutputConsole() * @uses configureOutputConsole() */ final protected function getWritingConsole() { if (null === $this->console) { $this->console = $this->createOutputConsole(); } $this->configureOutputConsole($this->console); return $this->console; } /** * Returns new output stream for console. * * Override this method & call flushOutputConsole() to write output in another stream * * @return resource * * @throws FormatterException */ protected function createOutputStream() { if (is_resource($stream = $this->parameters->get('output'))) { return $stream; } $outputPath = $this->parameters->get('output_path'); if (null === $outputPath) { $stream = fopen('php://stdout', 'w'); } elseif (!is_dir($outputPath)) { $stream = fopen($outputPath, 'w'); } else { throw new FormatterException(sprintf( 'Filename expected as "output_path" parameter of "%s" formatter, but got: "%s"', basename(str_replace('\\', '/', get_class($this))), $outputPath )); } return $stream; } /** * Returns new output console. * * @return StreamOutput * * @uses createOutputStream() */ protected function createOutputConsole() { $stream = $this->createOutputStream(); $format = new OutputFormatter(); // set user-defined styles foreach ($this->parameters->get('output_styles') as $name => $options) { $style = new OutputFormatterStyle(); if (isset($options[0])) { $style->setForeground($options[0]); } if (isset($options[1])) { $style->setBackground($options[1]); } if (isset($options[2])) { $style->setOptions($options[2]); } $format->setStyle($name, $style); } return new StreamOutput( $stream, StreamOutput::VERBOSITY_NORMAL, $this->parameters->get('output_decorate'), $format ); } /** * Configure output console parameters. * * @param StreamOutput $console */ protected function configureOutputConsole(StreamOutput $console) { $console->setVerbosity( $this->parameters->get('verbose') ? StreamOutput::VERBOSITY_VERBOSE : StreamOutput::VERBOSITY_NORMAL ); $console->getFormatter()->setDecorated( $this->parameters->get('decorated') ); } /** * Clear output console, so on next write formatter will need to init (create) it again. * * @see createOutputConsole() */ final protected function flushOutputConsole() { $this->console = null; } /** * Translates message to output language. * * @param string $message message to translate * @param array $parameters message parameters * * @return string */ final protected function translate($message, array $parameters = array()) { return $this->translator->trans( $message, $parameters, 'behat', $this->parameters->get('language') ); } /** * Translates numbered message to output language. * * @param string $message message specification to translate * @param string $number choice number * @param array $parameters message parameters * * @return string */ final protected function translateChoice($message, $number, array $parameters = array()) { return $this->translator->transChoice( $message, $number, $parameters, 'behat', $this->parameters->get('language') ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Failed scenarios formatter. * * @author Konstantin Kudryashov */ class FailedScenariosFormatter extends ConsoleFormatter { /** * {@inheritdoc} */ protected function getDefaultParameters() { return array(); } /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to */ public static function getSubscribedEvents() { $events = array('afterScenario', 'afterOutlineExample'); return array_combine($events, $events); } /** * Listens to "scenario.after" event. * * @param ScenarioEvent $event */ public function afterScenario(ScenarioEvent $event) { if (StepEvent::FAILED === $event->getResult()) { $scenario = $event->getScenario(); $this->writeln($scenario->getFile().':'.$scenario->getLine()); } } /** * Listens to "outline.example.after" event. * * @param ScenarioEvent $event */ public function afterOutlineExample(OutlineExampleEvent $event) { if (StepEvent::FAILED === $event->getResult()) { $outline = $event->getOutline(); $examples = $outline->getExamples(); $lines = $examples->getRowLines(); $this->writeln($outline->getFile().':'.$lines[$event->getIteration() + 1]); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Formatter dispatcher. * * @author Konstantin Kudryashov */ class FormatterDispatcher { private $class; private $name; private $description; /** * Initializes formatter dispatcher. * * @param string $class Formatter class * @param string $name Name of the formatter * @param string $description Formatter description * * @throws \RuntimeException */ public function __construct($class, $name = null, $description = null) { $refClass = new \ReflectionClass($class); if (!$refClass->implementsInterface('Behat\Behat\Formatter\FormatterInterface')) { throw new \RuntimeException(sprintf( 'Formatter class "%s" should implement FormatterInterface', $class )); } $this->class = $class; $this->name = null !== $name ? strtolower($name) : null; $this->description = $description; } /** * Returns formatter name. * * @return string */ public function getName() { return $this->name; } /** * Returns formatter description. * * @return string */ public function getDescription() { return $this->description; } /** * Returns formatter class. * * @return string */ public function getClass() { return $this->class; } /** * Initializes formatter instance. * * @return FormatterInterface */ public function createFormatter() { $class = $this->class; return new $class(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Formatter interface. * * @author Konstantin Kudryashov */ interface FormatterInterface extends EventSubscriberInterface { /** * Set formatter translator. * * @param Translator $translator */ public function setTranslator(Translator $translator); /** * Checks if current formatter has parameter. * * @param string $name * * @return Boolean */ public function hasParameter($name); /** * Sets formatter parameter. * * @param string $name * @param mixed $value */ public function setParameter($name, $value); /** * Returns parameter value. * * @param string $name * * @return mixed */ public function getParameter($name); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Formatters manager. * * @author Konstantin Kudryashov */ class FormatterManager { private $translator; private $eventDispatcher; private $dispatchers = array(); private $formatters = array(); /** * Initializes format manager. * * @param Translator $translator * @param EventDispatcher $eventDispatcher */ public function __construct(Translator $translator, EventDispatcher $eventDispatcher) { $this->translator = $translator; $this->eventDispatcher = $eventDispatcher; } /** * Adds formatter dispatcher to the manager. * * @param FormatterDispatcher $dispatcher Formatter dispatcher */ public function addDispatcher(FormatterDispatcher $dispatcher) { $this->dispatchers[$dispatcher->getName()] = $dispatcher; } /** * Returns registered formatter dispatchers. * * @return array */ public function getDispatchers() { return $this->dispatchers; } /** * Inits specific formatter class by format name. * * @param string $name * * @return array * * @throws \RuntimeException */ public function initFormatter($name) { if (isset($this->dispatchers[strtolower($name)])) { $dispatcher = $this->dispatchers[strtolower($name)]; } elseif (class_exists($name)) { $dispatcher = new FormatterDispatcher($name); } else { throw new \RuntimeException("Unknown formatter: \"$name\". " . 'Available formatters are: ' . implode(', ', array_keys($this->dispatchers)) ); } $formatter = $dispatcher->createFormatter(); $formatter->setTranslator($this->translator); $this->eventDispatcher->addSubscriber($formatter, -5); return $this->formatters[] = $formatter; } /** * Sets specific parameter in all initialized formatters. * * @param string $param * @param mixed $value */ public function setFormattersParameter($param, $value) { foreach ($this->formatters as $formatter) { $formatter->setParameter($param, $value); } } /** * Returns all initialized formatters. * * @return array */ public function getFormatters() { return $this->formatters; } /** * Disables formatter. * * @param FormatterInterface $formatter */ public function disableFormatter(FormatterInterface $formatter) { $unsubscribe = array($this->eventDispatcher, 'removeSubscriber'); $this->formatters = array_filter($this->formatters, function($registered) use($formatter, $unsubscribe) { if ($registered === $formatter) { call_user_func($unsubscribe, $registered); return false; } return true; } ); } /** * Disables all initialized formatters. */ public function disableFormatters() { foreach ($this->getFormatters() as $formatter) { $this->disableFormatter($formatter); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * HTML formatter. * * @author Konstantin Kudryashov */ class HtmlFormatter extends PrettyFormatter { /** * Deffered footer template part. * * @var string */ protected $footer; /** * {@inheritdoc} */ protected function getDefaultParameters() { return array( 'template_path' => null ); } /** * {@inheritdoc} */ protected function printSuiteHeader(LoggerDataCollector $logger) { $this->parameters->set('decorated', false); $template = $this->getHtmlTemplate(); $header = mb_substr($template, 0, mb_strpos($template, '{{content}}')); $this->footer = mb_substr($template, mb_strpos($template, '{{content}}') + 11); $this->writeln($header); } /** * {@inheritdoc} */ protected function printSuiteFooter(LoggerDataCollector $logger) { $this->printSummary($logger); $this->writeln($this->footer); } /** * {@inheritdoc} */ protected function printFeatureHeader(FeatureNode $feature) { $this->writeln('
'); parent::printFeatureHeader($feature); } /** * {@inheritdoc} */ protected function printFeatureOrScenarioTags(AbstractNode $node) { if (count($tags = $node->getOwnTags())) { $this->writeln('
    '); foreach ($tags as $tag) { $this->writeln("
  • @$tag
  • "); } $this->writeln('
'); } } /** * {@inheritdoc} */ protected function printFeatureName(FeatureNode $feature) { $this->writeln('

'); $this->writeln('' . $feature->getKeyword() . ': '); $this->writeln('' . $feature->getTitle() . ''); $this->writeln('

'); } /** * {@inheritdoc} */ protected function printFeatureDescription(FeatureNode $feature) { $lines = explode("\n", $feature->getDescription()); $this->writeln('

'); foreach ($lines as $line) { $this->writeln(htmlspecialchars($line) . "
"); } $this->writeln('

'); } /** * {@inheritdoc} */ protected function printFeatureFooter(FeatureNode $feature) { $this->writeln('
'); } /** * {@inheritdoc} */ protected function printBackgroundHeader(BackgroundNode $background) { $this->writeln('
'); $this->printScenarioName($background); } /** * {@inheritdoc} */ protected function printBackgroundFooter(BackgroundNode $background) { $this->writeln(''); $this->writeln('
'); } /** * {@inheritdoc} */ protected function printScenarioHeader(ScenarioNode $scenario) { $this->writeln('
'); $this->printFeatureOrScenarioTags($scenario); $this->printScenarioName($scenario); } /** * {@inheritdoc} */ protected function printScenarioName(AbstractScenarioNode $scenario) { $this->writeln('

'); $this->writeln('' . $scenario->getKeyword() . ': '); if ($scenario->getTitle()) { $this->writeln('' . $scenario->getTitle() . ''); } $this->printScenarioPath($scenario); $this->writeln('

'); $this->writeln('
    '); } /** * {@inheritdoc} */ protected function printScenarioFooter(ScenarioNode $scenario) { $this->writeln('
'); $this->writeln('
'); } /** * {@inheritdoc} */ protected function printOutlineHeader(OutlineNode $outline) { $this->writeln('
'); $this->printFeatureOrScenarioTags($outline); $this->printScenarioName($outline); } /** * {@inheritdoc} */ protected function printOutlineSteps(OutlineNode $outline) { parent::printOutlineSteps($outline); $this->writeln(''); } /** * {@inheritdoc} */ protected function printOutlineExamplesSectionHeader(TableNode $examples) { $this->writeln('
'); if (!$this->getParameter('expand')) { $this->writeln('

' . $examples->getKeyword() . '

'); $this->writeln(''); $this->writeln(''); $this->printColorizedTableRow($examples->getRow(0), 'skipped'); $this->writeln(''); $this->writeln(''); } } /** * {@inheritdoc} */ protected function printOutlineExampleResult(TableNode $examples, $iteration, $result, $isSkipped) { if (!$this->getParameter('expand')) { $color = $this->getResultColorCode($result); $this->printColorizedTableRow($examples->getRow($iteration + 1), $color); $this->printOutlineExampleResultExceptions($examples, $this->delayedStepEvents); } else { $this->write('

' . $examples->getKeyword() . ': '); foreach ($examples->getRow($iteration + 1) as $value) { $this->write('' . $value . ''); } $this->writeln('

'); foreach ($this->delayedStepEvents as $event) { $this->writeln('
    '); $this->printStep( $event->getStep(), $event->getResult(), $event->getDefinition(), $event->getSnippet(), $event->getException() ); $this->writeln('
'); } } } /** * {@inheritdoc} */ protected function printOutlineExampleResultExceptions(TableNode $examples, array $events) { $colCount = count($examples->getRow(0)); foreach ($events as $event) { $exception = $event->getException(); if ($exception && !$exception instanceof UndefinedException) { $error = $this->relativizePathsInString($exception->getMessage()); $this->writeln(''); $this->writeln(''); $this->writeln(''); } } } /** * {@inheritdoc} */ protected function printOutlineFooter(OutlineNode $outline) { if (!$this->getParameter('expand')) { $this->writeln(''); $this->writeln('
'); $this->writeln('
' . htmlspecialchars($error) . '
'); $this->writeln('
'); } $this->writeln('
'); $this->writeln('
'); } /** * {@inheritdoc} */ protected function printStep(StepNode $step, $result, DefinitionInterface $definition = null, $snippet = null, \Exception $exception = null) { $this->writeln('
  • '); parent::printStep($step, $result, $definition, $snippet, $exception); $this->writeln('
  • '); } /** * {@inheritdoc} */ protected function printStepBlock(StepNode $step, DefinitionInterface $definition = null, $color) { $this->writeln('
    '); $this->printStepName($step, $definition, $color); if (null !== $definition) { $this->printStepDefinitionPath($step, $definition); } $this->writeln('
    '); } /** * {@inheritdoc} */ protected function printStepName(StepNode $step, DefinitionInterface $definition = null, $color) { $type = $step->getType(); $text = $this->inOutlineSteps ? $step->getCleanText() : $step->getText(); if (null !== $definition) { $text = $this->colorizeDefinitionArguments($text, $definition, $color); } $this->writeln('' . $type . ' '); $this->writeln('' . $text . ''); } /** * {@inheritdoc} */ protected function printStepDefinitionPath(StepNode $step, DefinitionInterface $definition) { if ($this->getParameter('paths')) { if ($this->hasParameter('paths_base_url')) { $this->printPathLink($definition); } else { $this->printPathComment($this->relativizePathsInString($definition->getPath())); } } } /** * {@inheritdoc} */ protected function printStepPyStringArgument(PyStringNode $pystring, $color = null) { $this->writeln('
    ' . htmlspecialchars((string) $pystring) . '
    '); } /** * {@inheritdoc} */ protected function printStepTableArgument(TableNode $table, $color = null) { $this->writeln(''); $this->writeln(''); $headers = $table->getRow(0); $this->printColorizedTableRow($headers, 'row'); $this->writeln(''); $this->writeln(''); foreach ($table->getHash() as $row) { $this->printColorizedTableRow($row, 'row'); } $this->writeln(''); $this->writeln('
    '); } /** * {@inheritdoc} */ protected function printStepException(\Exception $exception, $color) { $error = $this->relativizePathsInString($exception->getMessage()); $this->writeln('
    ' . htmlspecialchars($error) . '
    '); } /** * {@inheritdoc} */ protected function printStepSnippet(DefinitionSnippet $snippet) { $this->writeln('
    ' . htmlspecialchars($snippet) . '
    '); } /** * {@inheritdoc} */ protected function colorizeDefinitionArguments($text, DefinitionInterface $definition, $color) { $regex = $definition->getRegex(); $paramColor = $color . '_param'; // If it's just a string - skip if ('/' !== substr($regex, 0, 1)) { return $text; } // Find arguments with offsets $matches = array(); preg_match($regex, $text, $matches, PREG_OFFSET_CAPTURE); array_shift($matches); // Replace arguments with colorized ones $shift = 0; $lastReplacementPosition = 0; foreach ($matches as $key => $match) { if (!is_numeric($key) || -1 === $match[1] || false !== strpos($match[0], '<')) { continue; } $offset = $match[1] + $shift; $value = $match[0]; // Skip inner matches if ($lastReplacementPosition > $offset) { continue; } $lastReplacementPosition = $offset + strlen($value); $begin = substr($text, 0, $offset); $end = substr($text, $offset + strlen($value)); $format = "{+strong class=\"$paramColor\"-}%s{+/strong-}"; $text = sprintf('%s'.$format.'%s', $begin, $value, $end); // Keep track of how many extra characters are added $shift += strlen($format) - 2; $lastReplacementPosition += strlen($format) - 2; } // Replace "<", ">" with colorized ones $text = preg_replace('/(<[^>]+>)/', "{+strong class=\"$paramColor\"-}\$1{+/strong-}", $text); $text = htmlspecialchars($text, ENT_NOQUOTES); $text = strtr($text, array('{+' => '<', '-}' => '>')); return $text; } /** * {@inheritdoc} */ protected function printColorizedTableRow($row, $color) { $this->writeln(''); foreach ($row as $column) { $this->writeln('' . $column . ''); } $this->writeln(''); } /** * Prints path link, which links to the source containing the step definition. * * @param DefinitionInterface $definition */ protected function printPathLink(DefinitionInterface $definition) { $url = $this->getParameter('paths_base_url') . $this->relativizePathsInString($definition->getCallbackReflection()->getFileName()); $path = $this->relativizePathsInString($definition->getPath()); $this->writeln('' . $path . ''); } /** * {@inheritdoc} */ protected function printPathComment($path, $indentCount = 0) { $this->writeln('' . $path . ''); } /** * {@inheritdoc} */ protected function printSummary(LoggerDataCollector $logger) { $results = $logger->getScenariosStatuses(); $result = $results['failed'] > 0 ? 'failed' : 'passed'; $this->writeln('
    '); $this->writeln('
    '); parent::printSummary($logger); $this->writeln('
    '); $this->writeln(<<<'HTML' HTML ); $this->writeln('
    '); } /** * {@inheritdoc} */ protected function printScenariosSummary(LoggerDataCollector $logger) { $this->writeln('

    '); parent::printScenariosSummary($logger); $this->writeln('

    '); } /** * {@inheritdoc} */ protected function printStepsSummary(LoggerDataCollector $logger) { $this->writeln('

    '); parent::printStepsSummary($logger); $this->writeln('

    '); } /** * {@inheritdoc} */ protected function printTimeSummary(LoggerDataCollector $logger) { $this->writeln('

    '); parent::printTimeSummary($logger); $this->writeln('

    '); } /** * {@inheritdoc} */ protected function printStatusesSummary(array $statusesStatistics) { $statuses = array(); $statusTpl = '%s'; foreach ($statusesStatistics as $status => $count) { if ($count) { $transStatus = $this->translateChoice( "{$status}_count", $count, array('%1%' => $count) ); $statuses[] = sprintf($statusTpl, $status, $transStatus); } } if (count($statuses)) { $this->writeln(' ('.implode(', ', $statuses).')'); } } /** * Get HTML template. * * @return string */ protected function getHtmlTemplate() { $templatePath = $this->parameters->get('template_path') ?: $this->parameters->get('support_path') . DIRECTORY_SEPARATOR . 'html.tpl'; if (file_exists($templatePath)) { return file_get_contents($templatePath); } return ' Behat Test Suite
    {{content}}
    '; } /** * Get HTML template style. * * @return string */ protected function getHtmlTemplateStyle() { return <<<'HTMLTPL' body { margin:0px; padding:0px; position:relative; padding-top:75px; } #behat { float:left; font-family: Georgia, serif; font-size:18px; line-height:26px; width:100%; } #behat .statistics { float:left; width:100%; margin-bottom:15px; } #behat .statistics p { text-align:right; padding:5px 15px; margin:0px; border-right:10px solid #000; } #behat .statistics.failed p { border-color:#C20000; } #behat .statistics.passed p { border-color:#3D7700; } #behat .feature { margin:15px; } #behat h2, #behat h3, #behat h4 { margin:0px 0px 5px 0px; padding:0px; font-family:Georgia; } #behat h2 .title, #behat h3 .title, #behat h4 .title { font-weight:normal; } #behat .path { font-size:10px; font-weight:normal; font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace !important; color:#999; padding:0px 5px; float:right; } #behat .path a:link, #behat .path a:visited { color:#999; } #behat .path a:hover, #behat .path a:active { background-color:#000; color:#fff; } #behat h3 .path { margin-right:4%; } #behat ul.tags { font-size:14px; font-weight:bold; color:#246AC1; list-style:none; margin:0px; padding:0px; } #behat ul.tags li { display:inline; } #behat ul.tags li:after { content:' '; } #behat ul.tags li:last-child:after { content:''; } #behat .feature > p { margin-top:0px; margin-left:20px; } #behat .scenario { margin-left:20px; margin-bottom:20px; } #behat .scenario > ol, #behat .scenario .examples > ol { margin:0px; list-style:none; padding:0px; } #behat .scenario > ol { margin-left:20px; } #behat .scenario > ol:after, #behat .scenario .examples > ol:after { content:''; display:block; clear:both; } #behat .scenario > ol li, #behat .scenario .examples > ol li { float:left; width:95%; padding-left:5px; border-left:5px solid; margin-bottom:4px; } #behat .scenario > ol li .argument, #behat .scenario .examples > ol li .argument { margin:10px 20px; font-size:16px; overflow:hidden; } #behat .scenario > ol li table.argument, #behat .scenario .examples > ol li table.argument { border:1px solid #d2d2d2; } #behat .scenario > ol li table.argument thead td, #behat .scenario .examples > ol li table.argument thead td { font-weight: bold; } #behat .scenario > ol li table.argument td, #behat .scenario .examples > ol li table.argument td { padding:5px 10px; background:#f3f3f3; } #behat .scenario > ol li .keyword, #behat .scenario .examples > ol li .keyword { font-weight:bold; } #behat .scenario > ol li .path, #behat .scenario .examples > ol li .path { float:right; } #behat .scenario .examples { margin-top:20px; margin-left:40px; } #behat .scenario .examples h4 span { font-weight:normal; background:#f3f3f3; color:#999; padding:0 5px; margin-left:10px; } #behat .scenario .examples table { margin-left:20px; } #behat .scenario .examples table thead td { font-weight:bold; text-align:center; } #behat .scenario .examples table td { padding:2px 10px; font-size:16px; } #behat .scenario .examples table .failed.exception td { border-left:5px solid #000; border-color:#C20000 !important; padding-left:0px; } pre { font-family:monospace; } .snippet { font-size:14px; color:#000; margin-left:20px; } .backtrace { font-size:12px; line-height:18px; color:#000; overflow:hidden; margin-left:20px; padding:15px; border-left:2px solid #C20000; background: #fff; margin-right:15px; } #behat .passed { background:#DBFFB4; border-color:#65C400 !important; color:#3D7700; } #behat .failed { background:#FFFBD3; border-color:#C20000 !important; color:#C20000; } #behat .undefined, #behat .pending { border-color:#FAF834 !important; background:#FCFB98; color:#000; } #behat .skipped { background:lightCyan; border-color:cyan !important; color:#000; } #behat .summary { position: absolute; top: 0px; left: 0px; width:100%; font-family: Arial, sans-serif; font-size: 14px; line-height: 18px; } #behat .summary .counters { padding: 10px; border-top: 0px; border-bottom: 0px; border-right: 0px; border-left: 5px; border-style: solid; height: 52px; overflow: hidden; } #behat .summary .switchers { position: absolute; right: 15px; top: 25px; } #behat .summary .switcher { text-decoration: underline; cursor: pointer; } #behat .summary .switchers a { margin-left: 10px; color: #000; } #behat .summary .switchers a:hover { text-decoration:none; } #behat .summary p { margin:0px; } #behat .jq-toggle > .scenario, #behat .jq-toggle > ol, #behat .jq-toggle > .examples { display:none; } #behat .jq-toggle-opened > .scenario, #behat .jq-toggle-opened > ol, #behat .jq-toggle-opened > .examples { display:block; } #behat .jq-toggle > h2, #behat .jq-toggle > h3 { cursor:pointer; } #behat .jq-toggle > h2:after, #behat .jq-toggle > h3:after { content:' |+'; font-weight:bold; } #behat .jq-toggle-opened > h2:after, #behat .jq-toggle-opened > h3:after { content:' |-'; font-weight:bold; } HTMLTPL; } /** * Get HTML template style. * * @return string */ protected function getHtmlTemplatePrintStyle() { return <<<'HTMLTPL' body { padding:0px; } #behat { font-size:11px; } #behat .jq-toggle > .scenario, #behat .jq-toggle > .scenario .examples, #behat .jq-toggle > ol { display:block; } #behat .summary { position:relative; } #behat .summary .counters { border:none; } #behat .summary .switchers { display:none; } #behat .step .path { display:none; } #behat .jq-toggle > h2:after, #behat .jq-toggle > h3:after { content:''; font-weight:bold; } #behat .jq-toggle-opened > h2:after, #behat .jq-toggle-opened > h3:after { content:''; font-weight:bold; } #behat .scenario > ol li, #behat .scenario .examples > ol li { border-left:none; } HTMLTPL; } /** * Get HTML template script. * * @return string */ protected function getHtmlTemplateScript() { return <<<'HTMLTPL' $(document).ready(function(){ $('#behat .feature h2').click(function(){ $(this).parent().toggleClass('jq-toggle-opened'); }).parent().addClass('jq-toggle'); $('#behat .scenario h3').click(function(){ $(this).parent().toggleClass('jq-toggle-opened'); }).parent().addClass('jq-toggle'); $('#behat_show_all').click(function(){ $('#behat .feature').addClass('jq-toggle-opened'); $('#behat .scenario').addClass('jq-toggle-opened'); }); $('#behat_hide_all').click(function(){ $('#behat .feature').removeClass('jq-toggle-opened'); $('#behat .scenario').removeClass('jq-toggle-opened'); }); $('#behat .summary .counters .scenarios .passed') .addClass('switcher') .click(function(){ var $scenario = $('.feature .scenario:not(:has(.failed, .pending))'); var $feature = $scenario.parent(); $('#behat_hide_all').click(); $scenario.addClass('jq-toggle-opened'); $feature.addClass('jq-toggle-opened'); }); $('#behat .summary .counters .steps .passed') .addClass('switcher') .click(function(){ var $scenario = $('.feature .scenario:has(.passed)'); var $feature = $scenario.parent(); $('#behat_hide_all').click(); $scenario.addClass('jq-toggle-opened'); $feature.addClass('jq-toggle-opened'); }); $('#behat .summary .counters .failed') .addClass('switcher') .click(function(){ var $scenario = $('.feature .scenario:has(.failed)'); var $feature = $scenario.parent(); $('#behat_hide_all').click(); $scenario.addClass('jq-toggle-opened'); $feature.addClass('jq-toggle-opened'); }); $('#behat .summary .counters .skipped') .addClass('switcher') .click(function(){ var $scenario = $('.feature .scenario:has(.skipped)'); var $feature = $scenario.parent(); $('#behat_hide_all').click(); $scenario.addClass('jq-toggle-opened'); $feature.addClass('jq-toggle-opened'); }); $('#behat .summary .counters .pending') .addClass('switcher') .click(function(){ var $scenario = $('.feature .scenario:has(.pending)'); var $feature = $scenario.parent(); $('#behat_hide_all').click(); $scenario.addClass('jq-toggle-opened'); $feature.addClass('jq-toggle-opened'); }); }); HTMLTPL; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Progress formatter. * * @author Konstantin Kudryashov */ class JUnitFormatter extends ConsoleFormatter { /** * Current XML filename. * * @var string */ protected $filename; /** * Test cases. * * @var array */ protected $testcases = array(); /** * Total steps count. * * @var integer */ protected $stepsCount = 0; /** * Total exceptions count. * * @var integer */ protected $exceptionsCount = 0; /** * Step exceptions. * * @var array */ protected $exceptions = array(); /** * Feature start time. * * @var float */ protected $featureStartTime; /** * Scenario start time. * * @var float */ protected $scenarioStartTime; /** * {@inheritdoc} */ protected function getDefaultParameters() { return array(); } /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to */ public static function getSubscribedEvents() { $events = array( 'beforeFeature', 'afterFeature', 'beforeScenario', 'afterScenario', 'beforeOutlineExample', 'afterOutlineExample', 'afterStep' ); return array_combine($events, $events); } /** * Listens to "feature.before" event. * * @param FeatureEvent $event * * @uses printTestSuiteHeader() */ public function beforeFeature(FeatureEvent $event) { $feature = $event->getFeature(); $this->filename = 'TEST-' . basename($feature->getFile(), '.feature') . '.xml'; $this->printTestSuiteHeader($feature); $this->stepsCount = 0; $this->testcases = array(); $this->exceptionsCount = 0; $this->featureStartTime = microtime(true); } /** * Listens to "feature.after" event. * * @param FeatureEvent $event * * @uses printTestSuiteFooter() * @uses flushOutputConsole() */ public function afterFeature(FeatureEvent $event) { $this->printTestSuiteFooter($event->getFeature(), microtime(true) - $this->featureStartTime); $this->flushOutputConsole(); } /** * Listens to "scenario.before" event. * * @param ScenarioEvent $event */ public function beforeScenario(ScenarioEvent $event) { $this->scenarioStartTime = microtime(true); } /** * Listens to "scenario.after" event. * * @param ScenarioEvent $event * * @uses printTestCase() */ public function afterScenario(ScenarioEvent $event) { $this->printTestCase($event->getScenario(), microtime(true) - $this->scenarioStartTime, $event); } /** * Listens to "outline.example.before" event. * * @param OutlineExampleEvent $event */ public function beforeOutlineExample(OutlineExampleEvent $event) { $this->scenarioStartTime = microtime(true); } /** * Listens to "outline.example.after" event. * * @param OutlineExampleEvent $event * * @uses printTestCase() */ public function afterOutlineExample(OutlineExampleEvent $event) { $this->printTestCase($event->getOutline(), microtime(true) - $this->scenarioStartTime, $event); } /** * Listens to "step.after" event. * * @param StepEvent $event */ public function afterStep(StepEvent $event) { if ($event->hasException()) { $this->exceptions[] = $event->getException(); $this->exceptionsCount++; } ++$this->stepsCount; } /** * Prints testsuite header. * * @param FeatureNode $feature */ protected function printTestSuiteHeader(FeatureNode $feature) { $this->writeln(''); } /** * Prints testsuite footer. * * @param FeatureNode $feature * @param float $time */ protected function printTestSuiteFooter(FeatureNode $feature, $time) { $suiteStats = sprintf('classname="behat.features" errors="0" failures="%d" name="%s" file="%s" tests="%d" time="%F"', $this->exceptionsCount, htmlspecialchars($feature->getTitle()), htmlspecialchars($feature->getFile()), $this->stepsCount, $time ); $this->writeln(""); $this->writeln(implode("\n", $this->testcases)); $this->writeln(''); } /** * Prints testcase. * * @param ScenarioNode $scenario * @param float $time * @param EventInterface $event */ protected function printTestCase(ScenarioNode $scenario, $time, EventInterface $event) { $className = $scenario->getFeature()->getTitle(); $name = $scenario->getTitle(); $name .= $event instanceof OutlineExampleEvent ? ', Ex #' . ($event->getIteration() + 1) : ''; $caseStats = sprintf('classname="%s" name="%s" time="%F"', htmlspecialchars($className), htmlspecialchars($name), $time ); $xml = " \n"; foreach ($this->exceptions as $exception) { $xml .= sprintf( ' ', htmlspecialchars($exception->getMessage()), $this->getResultColorCode($event->getResult()) ); $exception = str_replace(array(''), '', (string) $exception); $xml .= "\n"; } $this->exceptions = array(); $xml .= " "; $this->testcases[] = $xml; } /** * {@inheritdoc} */ protected function createOutputStream() { $outputPath = $this->parameters->get('output_path'); if (null === $outputPath) { throw new FormatterException(sprintf( 'You should specify "output_path" parameter for %s', get_class($this) )); } elseif (is_file($outputPath)) { throw new FormatterException(sprintf( 'Directory path expected as "output_path" parameter of %s, but got: %s', get_class($this), $outputPath )); } if (!is_dir($outputPath)) { mkdir($outputPath, 0777, true); } return fopen($outputPath . DIRECTORY_SEPARATOR . $this->filename, 'w'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Pretty formatter. * * @author Konstantin Kudryashov */ class PrettyFormatter extends ProgressFormatter { /** * Maximum line length. * * @var integer */ protected $maxLineLength = 0; /** * Are we in background. * * @var Boolean */ protected $inBackground = false; /** * Is background printed. * * @var Boolean */ protected $isBackgroundPrinted = false; /** * Are we in outline steps. * * @var Boolean */ protected $inOutlineSteps = false; /** * Are we in outline example. * * @var Boolean */ protected $inOutlineExample = false; /** * Is outline headline printed. * * @var Boolean */ protected $isOutlineHeaderPrinted = false; /** * Delayed scenario event. * * @var EventInterface */ protected $delayedScenarioEvent; /** * Delayed step events. * * @var array */ protected $delayedStepEvents = array(); /** * Current step indentation. * * @var integer */ protected $stepIndent = ' '; /** * {@inheritdoc} */ protected function getDefaultParameters() { return array(); } /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to */ public static function getSubscribedEvents() { $events = array( 'beforeSuite', 'afterSuite', 'beforeFeature', 'afterFeature', 'beforeScenario', 'afterScenario', 'beforeBackground', 'afterBackground', 'beforeOutline', 'afterOutline', 'beforeOutlineExample', 'afterOutlineExample', 'afterStep' ); return array_combine($events, $events); } /** * Listens to "suite.before" event. * * @param SuiteEvent $event * * @uses printSuiteHeader() */ public function beforeSuite(SuiteEvent $event) { $this->printSuiteHeader($event->getLogger()); } /** * Listens to "suite.after" event. * * @param SuiteEvent $event * * @uses printSuiteFooter() */ public function afterSuite(SuiteEvent $event) { $this->printSuiteFooter($event->getLogger()); } /** * Listens to "feature.before" event. * * @param FeatureEvent $event * * @uses printFeatureHeader() */ public function beforeFeature(FeatureEvent $event) { $this->isBackgroundPrinted = false; $this->printFeatureHeader($event->getFeature()); } /** * Listens to "feature.after" event. * * @param FeatureEvent $event * * @uses printFeatureFooter() */ public function afterFeature(FeatureEvent $event) { $this->printFeatureFooter($event->getFeature()); } /** * Listens to "background.before" event. * * @param BackgroundEvent $event * * @uses printBackgroundHeader() */ public function beforeBackground(BackgroundEvent $event) { $this->inBackground = true; if ($this->isBackgroundPrinted) { return; } $this->printBackgroundHeader($event->getBackground()); } /** * Listens to "background.after" event. * * @param BackgroundEvent $event * * @uses printBackgroundFooter() */ public function afterBackground(BackgroundEvent $event) { $this->inBackground = false; if ($this->isBackgroundPrinted) { return; } $this->isBackgroundPrinted = true; $this->printBackgroundFooter($event->getBackground()); if (null !== $this->delayedScenarioEvent) { $method = $this->delayedScenarioEvent[0]; $event = $this->delayedScenarioEvent[1]; $this->$method($event); } } /** * Listens to "outline.before" event. * * @param OutlineEvent $event * * @uses printOutlineHeader() */ public function beforeOutline(OutlineEvent $event) { $outline = $event->getOutline(); if (!$this->isBackgroundPrinted && $outline->getFeature()->hasBackground()) { $this->delayedScenarioEvent = array(__FUNCTION__, $event); return; } $this->isOutlineHeaderPrinted = false; $this->printOutlineHeader($outline); } /** * Listens to "outline.example.before" event. * * @param OutlineExampleEvent $event * * @uses printOutlineExampleHeader() */ public function beforeOutlineExample(OutlineExampleEvent $event) { $this->inOutlineExample = true; $this->delayedStepEvents = array(); $this->printOutlineExampleHeader($event->getOutline(), $event->getIteration()); } /** * Listens to "outline.example.after" event. * * @param OutlineExampleEvent $event * * @uses printOutlineExampleFooter() */ public function afterOutlineExample(OutlineExampleEvent $event) { $this->inOutlineExample = false; $this->printOutlineExampleFooter( $event->getOutline(), $event->getIteration(), $event->getResult(), $event->isSkipped() ); } /** * Listens to "outline.after" event. * * @param OutlineEvent $event * * @uses printOutlineFooter() */ public function afterOutline(OutlineEvent $event) { $this->printOutlineFooter($event->getOutline()); } /** * Listens to "scenario.before" event. * * @param ScenarioEvent $event * * @uses printScenarioHeader() */ public function beforeScenario(ScenarioEvent $event) { $scenario = $event->getScenario(); if (!$this->isBackgroundPrinted && $scenario->getFeature()->hasBackground()) { $this->delayedScenarioEvent = array(__FUNCTION__, $event); return; } $this->printScenarioHeader($scenario); } /** * Listens to "scenario.after" event. * * @param ScenarioEvent $event * * @uses printScenarioFooter() */ public function afterScenario(ScenarioEvent $event) { $this->printScenarioFooter($event->getScenario()); } /** * Listens to "step.after" event. * * @param StepEvent $event * * @uses printStep() */ public function afterStep(StepEvent $event) { if ($this->inBackground && $this->isBackgroundPrinted) { return; } if (!$this->inBackground && $this->inOutlineExample) { $this->delayedStepEvents[] = $event; return; } $this->printStep( $event->getStep(), $event->getResult(), $event->getDefinition(), $event->getSnippet(), $event->getException() ); } /** * Prints feature header. * * @param FeatureNode $feature * * @uses printFeatureOrScenarioTags() * @uses printFeatureName() * @uses printFeatureDescription() */ protected function printFeatureHeader(FeatureNode $feature) { $this->printFeatureOrScenarioTags($feature); $this->printFeatureName($feature); if (null !== $feature->getDescription()) { $this->printFeatureDescription($feature); } $this->writeln(); } /** * Prints node tags. * * @param AbstractNode $node */ protected function printFeatureOrScenarioTags(AbstractNode $node) { if (count($tags = $node->getOwnTags())) { $tags = implode(' ', array_map(function($tag){ return '@' . $tag; }, $tags)); if ($node instanceof FeatureNode) { $indent = ''; } else { $indent = ' '; } $this->writeln("$indent{+tag}$tags{-tag}"); } } /** * Prints feature keyword and name. * * @param FeatureNode $feature * * @uses getFeatureOrScenarioName() */ protected function printFeatureName(FeatureNode $feature) { $this->writeln($this->getFeatureOrScenarioName($feature)); } /** * Prints feature description. * * @param FeatureNode $feature */ protected function printFeatureDescription(FeatureNode $feature) { $lines = explode("\n", $feature->getDescription()); foreach ($lines as $line) { $this->writeln(" $line"); } } /** * Prints feature footer. * * @param FeatureNode $feature */ protected function printFeatureFooter(FeatureNode $feature) { } /** * Prints scenario keyword and name. * * @param AbstractScenarioNode $scenario * * @uses getFeatureOrScenarioName() * @uses printScenarioPath() */ protected function printScenarioName(AbstractScenarioNode $scenario) { $title = explode("\n", $this->getFeatureOrScenarioName($scenario)); $this->write(array_shift($title)); $this->printScenarioPath($scenario); if (count($title)) { $this->writeln(implode("\n", $title)); } } /** * Prints scenario definition path. * * @param AbstractScenarioNode $scenario * * @uses getFeatureOrScenarioName() * @uses printPathComment() */ protected function printScenarioPath(AbstractScenarioNode $scenario) { if ($this->getParameter('paths')) { $lines = explode("\n", $this->getFeatureOrScenarioName($scenario)); $nameLength = mb_strlen(current($lines)); $indentCount = $nameLength > $this->maxLineLength ? 0 : $this->maxLineLength - $nameLength; $this->printPathComment( $this->relativizePathsInString($scenario->getFile()).':'.$scenario->getLine(), $indentCount ); } else { $this->writeln(); } } /** * Prints background header. * * @param BackgroundNode $background * * @uses printScenarioName() * @uses printScenarioPath() */ protected function printBackgroundHeader(BackgroundNode $background) { $this->maxLineLength = $this->getMaxLineLength($this->maxLineLength, $background); $this->printScenarioName($background); } /** * Prints background footer. * * @param BackgroundNode $background */ protected function printBackgroundFooter(BackgroundNode $background) { $this->writeln(); } /** * Prints outline header. * * @param OutlineNode $outline * * @uses printFeatureOrScenarioTags() * @uses printScenarioName() */ protected function printOutlineHeader(OutlineNode $outline) { $this->maxLineLength = $this->getMaxLineLength($this->maxLineLength, $outline); $this->printFeatureOrScenarioTags($outline); $this->printScenarioName($outline); } /** * Prints outline footer. * * @param OutlineNode $outline */ protected function printOutlineFooter(OutlineNode $outline) { $this->writeln(); } /** * Prints outline example header. * * @param OutlineNode $outline * @param integer $iteration */ protected function printOutlineExampleHeader(OutlineNode $outline, $iteration) { } /** * Prints outline example result. * * @param OutlineNode $outline outline instance * @param integer $iteration example row number * @param integer $result result code * @param Boolean $skipped is outline example skipped * * @uses printOutlineSteps() * @uses printOutlineExamplesSectionHeader() * @uses printOutlineExampleResult() */ protected function printOutlineExampleFooter(OutlineNode $outline, $iteration, $result, $skipped) { if (!$this->isOutlineHeaderPrinted) { $this->printOutlineSteps($outline); $this->printOutlineExamplesSectionHeader($outline->getExamples()); $this->isOutlineHeaderPrinted = true; } $this->printOutlineExampleResult($outline->getExamples(), $iteration, $result, $skipped); } /** * Prints outline steps. * * @param OutlineNode $outline */ protected function printOutlineSteps(OutlineNode $outline) { $this->inOutlineSteps = true; foreach ($this->delayedStepEvents as $event) { $this->printStep($event->getStep(), StepEvent::SKIPPED, $event->getDefinition()); } $this->inOutlineSteps = false; } /** * Prints outline examples header. * * @param TableNode $examples * * @uses printColorizedTableRow() */ protected function printOutlineExamplesSectionHeader(TableNode $examples) { $this->writeln(); $keyword = $examples->getKeyword(); if (!$this->getParameter('expand')) { $this->writeln(" $keyword:"); $this->printColorizedTableRow($examples->getRowAsString(0), 'skipped'); } } /** * Prints outline example result. * * @param TableNode $examples examples table * @param integer $iteration example row * @param integer $result result code * @param boolean $isSkipped is outline example skipped * * @uses printColorizedTableRow() * @uses printOutlineExampleResultExceptions() */ protected function printOutlineExampleResult(TableNode $examples, $iteration, $result, $isSkipped) { if (!$this->getParameter('expand')) { $color = $this->getResultColorCode($result); $this->printColorizedTableRow($examples->getRowAsString($iteration + 1), $color); $this->printOutlineExampleResultExceptions($examples, $this->delayedStepEvents); } else { $this->write(' ' . $examples->getKeyword() . ': '); $this->writeln('| ' . implode(' | ', $examples->getRow($iteration + 1)) . ' |'); $this->stepIndent = ' '; foreach ($this->delayedStepEvents as $event) { $this->printStep( $event->getStep(), $event->getResult(), $event->getDefinition(), $event->getSnippet(), $event->getException() ); } $this->stepIndent = ' '; if ($iteration < count($examples->getRows()) - 2) { $this->writeln(); } } } /** * Prints outline example exceptions. * * @param TableNode $examples examples table * @param array $events failed steps events */ protected function printOutlineExampleResultExceptions(TableNode $examples, array $events) { foreach ($events as $event) { $exception = $event->getException(); if ($exception && !$exception instanceof UndefinedException) { $color = $this->getResultColorCode($event->getResult()); if ($this->parameters->get('verbose')) { $error = (string) $exception; } else { $error = $exception->getMessage(); } $error = $this->relativizePathsInString($error); $this->writeln( " {+$color}" . strtr($error, array("\n" => "\n ")) . "{-$color}" ); } } } /** * Prints scenario header. * * @param ScenarioNode $scenario * * @uses printFeatureOrScenarioTags() * @uses printScenarioName() */ protected function printScenarioHeader(ScenarioNode $scenario) { $this->maxLineLength = $this->getMaxLineLength($this->maxLineLength, $scenario); $this->printFeatureOrScenarioTags($scenario); $this->printScenarioName($scenario); } /** * Prints scenario footer. * * @param ScenarioNode $scenario */ protected function printScenarioFooter(ScenarioNode $scenario) { $this->writeln(); } /** * Prints step. * * @param StepNode $step step node * @param integer $result result code * @param DefinitionInterface $definition definition (if found one) * @param string $snippet snippet (if step is undefined) * @param \Exception $exception exception (if step is failed) * * @uses printStepBlock() * @uses printStepArguments() * @uses printStepException() * @uses printStepSnippet() */ protected function printStep(StepNode $step, $result, DefinitionInterface $definition = null, $snippet = null, \Exception $exception = null) { $color = $this->getResultColorCode($result); $this->printStepBlock($step, $definition, $color); if ($this->parameters->get('multiline_arguments')) { $this->printStepArguments($step->getArguments(), $color); } if (null !== $exception && (!$exception instanceof UndefinedException || null === $snippet)) { $this->printStepException($exception, $color); } if (null !== $snippet && $this->getParameter('snippets')) { $this->printStepSnippet($snippet); } } /** * Prints step block (name & definition path). * * @param StepNode $step step node * @param DefinitionInterface $definition definition (if found one) * @param string $color color code * * @uses printStepName() * @uses printStepDefinitionPath() */ protected function printStepBlock(StepNode $step, DefinitionInterface $definition = null, $color) { $this->printStepName($step, $definition, $color); if (null !== $definition) { $this->printStepDefinitionPath($step, $definition); } else { $this->writeln(); } } /** * Prints step name. * * @param StepNode $step step node * @param DefinitionInterface $definition definition (if found one) * @param string $color color code * * @uses colorizeDefinitionArguments() */ protected function printStepName(StepNode $step, DefinitionInterface $definition = null, $color) { $type = $step->getType(); $text = $this->inOutlineSteps ? $step->getCleanText() : $step->getText(); $indent = $this->stepIndent; if (null !== $definition) { $text = $this->colorizeDefinitionArguments($text, $definition, $color); } $this->write("$indent{+$color}$type $text{-$color}"); } /** * Prints step definition path. * * @param StepNode $step step node * @param DefinitionInterface $definition definition (if found one) * * @uses printPathComment() */ protected function printStepDefinitionPath(StepNode $step, DefinitionInterface $definition) { if ($this->getParameter('paths')) { $type = $step->getType(); $text = $this->inOutlineSteps ? $step->getCleanText() : $step->getText(); $indent = $this->stepIndent; $nameLength = mb_strlen("$indent$type $text"); $indentCount = $nameLength > $this->maxLineLength ? 0 : $this->maxLineLength - $nameLength; $this->printPathComment( $this->relativizePathsInString($definition->getPath()), $indentCount ); if ($this->getParameter('expand')) { $this->maxLineLength = max($this->maxLineLength, $nameLength); } } else { $this->writeln(); } } /** * Prints step arguments. * * @param array $arguments step arguments * @param string $color color name * * @uses printStepPyStringArgument() * @uses printStepTableArgument() */ protected function printStepArguments(array $arguments, $color) { foreach ($arguments as $argument) { if ($argument instanceof PyStringNode) { $this->printStepPyStringArgument($argument, $color); } elseif ($argument instanceof TableNode) { $this->printStepTableArgument($argument, $color); } } } /** * Prints step exception. * * @param \Exception $exception * @param string $color */ protected function printStepException(\Exception $exception, $color) { $indent = $this->stepIndent; if ($this->parameters->get('verbose')) { $error = (string) $exception; } else { $error = $exception->getMessage(); } $error = $this->relativizePathsInString($error); $this->writeln( "$indent {+$color}" . strtr($error, array("\n" => "\n$indent ")) . "{-$color}" ); } /** * Prints step snippet * * @param DefinitionSnippet $snippet */ protected function printStepSnippet(DefinitionSnippet $snippet) { } /** * Prints PyString argument. * * @param PyStringNode $pystring pystring node * @param string $color color name */ protected function printStepPyStringArgument(PyStringNode $pystring, $color = null) { $indent = $this->stepIndent; $string = strtr( sprintf("$indent \"\"\"\n%s\n\"\"\"", (string) $pystring), array("\n" => "\n$indent ") ); if (null !== $color) { $this->writeln("{+$color}$string{-$color}"); } else { $this->writeln($string); } } /** * Prints table argument. * * @param TableNode $table * @param string $color */ protected function printStepTableArgument(TableNode $table, $color = null) { $indent = $this->stepIndent; $string = strtr("$indent " . (string) $table, array("\n" => "\n$indent ")); if (null !== $color) { $this->writeln("{+$color}$string{-$color}"); } else { $this->writeln($string); } } /** * Prints table row in color. * * @param array $row * @param string $color */ protected function printColorizedTableRow($row, $color) { $string = preg_replace( '/|([^|]*)|/', "{+$color}\$1{-$color}", ' ' . $row ); $this->writeln($string); } /** * Prints suite header. * * @param LoggerDataCollector $logger suite logger */ protected function printSuiteHeader(LoggerDataCollector $logger) { } /** * Prints suite footer information. * * @param LoggerDataCollector $logger suite logger * * @uses printSummary() * @uses printUndefinedStepsSnippets() */ protected function printSuiteFooter(LoggerDataCollector $logger) { $this->printSummary($logger); $this->printUndefinedStepsSnippets($logger); } /** * Returns feature or scenario name. * * @param AbstractNode $node * @param Boolean $haveBaseIndent * * @return string */ protected function getFeatureOrScenarioName(AbstractNode $node, $haveBaseIndent = true) { $keyword = $node->getKeyword(); $baseIndent = ($node instanceof FeatureNode) || !$haveBaseIndent ? '' : ' '; $lines = explode("\n", $node->getTitle()); $title = array_shift($lines); if (count($lines)) { foreach ($lines as $line) { $title .= "\n" . $baseIndent.' '.$line; } } return "$baseIndent$keyword:" . ($title ? ' ' . $title : ''); } /** * Returns step text with colorized arguments. * * @param string $text * @param DefinitionInterface $definition * @param string $color * * @return string */ protected function colorizeDefinitionArguments($text, DefinitionInterface $definition, $color) { $regex = $definition->getRegex(); $paramColor = $color . '_param'; // If it's just a string - skip if ('/' !== substr($regex, 0, 1)) { return $text; } // Find arguments with offsets $matches = array(); preg_match($regex, $text, $matches, PREG_OFFSET_CAPTURE); array_shift($matches); // Replace arguments with colorized ones $shift = 0; $lastReplacementPosition = 0; foreach ($matches as $key => $match) { if (!is_numeric($key) || -1 === $match[1] || false !== strpos($match[0], '<')) { continue; } $offset = $match[1] + $shift; $value = $match[0]; // Skip inner matches if ($lastReplacementPosition > $offset) { continue; } $lastReplacementPosition = $offset + strlen($value); $begin = substr($text, 0, $offset); $end = substr($text, $lastReplacementPosition); $format = "{-$color}{+$paramColor}%s{-$paramColor}{+$color}"; $text = sprintf("%s{$format}%s", $begin, $value, $end); // Keep track of how many extra characters are added $shift += strlen($format) - 2; $lastReplacementPosition += strlen($format) - 2; } // Replace "<", ">" with colorized ones $text = preg_replace('/(<[^>]+>)/', "{-$color}{+$paramColor}\$1{-$paramColor}{+$color}", $text ); return $text; } /** * Returns max lines size for section elements. * * @param integer $max previous max value * @param AbstractScenarioNode $scenario element for calculations * * @return integer */ protected function getMaxLineLength($max, AbstractScenarioNode $scenario) { $lines = explode("\n", $this->getFeatureOrScenarioName($scenario, false)); $max = max($max, mb_strlen(current($lines)) + 2); foreach ($scenario->getSteps() as $step) { $text = $step instanceof ExampleStepNode ? $step->getCleanText() : $step->getText(); $stepDescription = $step->getType() . ' ' . $text; $max = max($max, mb_strlen($stepDescription) + 4); } return $max; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Progress formatter. * * @author Konstantin Kudryashov */ class ProgressFormatter extends ConsoleFormatter { /** * Holds amount of printed items in current line; */ private $stepsPrinted = 0; /** * Maximum line length. * * @var integer */ protected $maxLineLength = 0; /** * {@inheritdoc} */ protected function getDefaultParameters() { return array(); } /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to */ public static function getSubscribedEvents() { $events = array('afterSuite', 'afterStep'); return array_combine($events, $events); } /** * Listens to "suite.after" event. * * @param SuiteEvent $event * * @uses printFailedSteps() * @uses printPendingSteps() * @uses printSummary() * @uses printUndefinedStepsSnippets() */ public function afterSuite(SuiteEvent $event) { $logger = $event->getLogger(); $this->writeln("\n"); $this->printFailedSteps($logger); $this->printPendingSteps($logger); $this->printSummary($logger); $this->printUndefinedStepsSnippets($logger); } /** * Listens to "step.after" event. * * @param StepEvent $event * * @uses printStep() */ public function afterStep(StepEvent $event) { $this->printStep( $event->getStep(), $event->getResult(), $event->getDefinition(), $event->getSnippet(), $event->getException() ); } /** * Prints step. * * @param StepNode $step step node * @param integer $result step result code * @param DefinitionInterface $definition definition instance (if step defined) * @param string $snippet snippet (if step is undefined) * @param \Exception $exception exception (if step is failed) * * @uses StepEvent */ protected function printStep(StepNode $step, $result, DefinitionInterface $definition = null, $snippet = null, \Exception $exception = null) { switch ($result) { case StepEvent::PASSED: $this->write('{+passed}.{-passed}'); break; case StepEvent::SKIPPED: $this->write('{+skipped}-{-skipped}'); break; case StepEvent::PENDING: $this->write('{+pending}P{-pending}'); break; case StepEvent::UNDEFINED: $this->write('{+undefined}U{-undefined}'); break; case StepEvent::FAILED: $this->write('{+failed}F{-failed}'); break; } if (++$this->stepsPrinted % 70 == 0) { $this->writeln(' '.$this->stepsPrinted); } } /** * Prints all failed steps info. * * @param LoggerDataCollector $logger suite logger */ protected function printFailedSteps(LoggerDataCollector $logger) { if (count($logger->getFailedStepsEvents())) { $header = $this->translate('failed_steps_title'); $this->writeln("{+failed}(::) $header (::){-failed}\n"); $this->printExceptionEvents($logger->getFailedStepsEvents()); } } /** * Prints all pending steps information. * * @param LoggerDataCollector $logger suite logger */ protected function printPendingSteps(LoggerDataCollector $logger) { if (count($logger->getPendingStepsEvents())) { $header = $this->translate('pending_steps_title'); $this->writeln("{+pending}(::) $header (::){-pending}\n"); $this->printExceptionEvents($logger->getPendingStepsEvents()); } } /** * Prints exceptions information. * * @param array $events failed step events */ protected function printExceptionEvents(array $events) { foreach ($events as $number => $event) { $exception = $event->getException(); if (null !== $exception) { $color = $exception instanceof PendingException ? 'pending' : 'failed'; if ($this->parameters->get('verbose') && 'pending' !== $color) { $error = (string) $exception; } else { $error = $exception->getMessage(); } $error = sprintf("%s. %s", str_pad((string) ($number + 1), 2, '0', STR_PAD_LEFT), strtr($error, array("\n" => "\n ")) ); $error = $this->relativizePathsInString($error); $this->writeln("{+$color}$error{-$color}"); } $this->printStepPath($event->getStep(), $event->getDefinition(), $exception); } } /** * Prints path to step. * * @param StepNode $step step node * @param DefinitionInterface $definition definition (if step defined) * @param \Exception $exception exception (if step failed) */ protected function printStepPath(StepNode $step, DefinitionInterface $definition = null, \Exception $exception = null) { $color = $exception instanceof PendingException ? 'pending' : 'failed'; $type = $step->getType(); $text = $step->getText(); $stepPath = "In step `$type $text'."; $stepPathLn = mb_strlen($stepPath); $node = $step->getParent(); if ($node instanceof BackgroundNode) { $scenarioPath = "From scenario background."; } else { $title = $node->getTitle(); $title = $title ? "`$title'" : '***'; $scenarioPath = "From scenario $title."; } $scenarioPathLn = mb_strlen($scenarioPath); $feature = $node->getFeature(); $title = $feature->getTitle(); $title = $title ? "`$title'" : '***'; $featurePath = "Of feature $title."; $featurePathLn = mb_strlen($featurePath); $this->maxLineLength = max($this->maxLineLength, $stepPathLn); $this->maxLineLength = max($this->maxLineLength, $scenarioPathLn); $this->maxLineLength = max($this->maxLineLength, $featurePathLn); $this->write(" {+$color}$stepPath{-$color}"); if (null !== $definition) { $indentCount = $this->maxLineLength - $stepPathLn; $this->printPathComment( $this->relativizePathsInString($definition->getPath()), $indentCount ); } else { $this->writeln(); } $this->write(" {+$color}$scenarioPath{-$color}"); $indentCount = $this->maxLineLength - $scenarioPathLn; $this->printPathComment( $this->relativizePathsInString($node->getFile()) . ':' . $node->getLine(), $indentCount ); $this->write(" {+$color}$featurePath{-$color}"); $indentCount = $this->maxLineLength - $featurePathLn; $this->printPathComment( $this->relativizePathsInString($feature->getFile()), $indentCount ); $this->writeln(); } /** * Prints summary suite run information. * * @param LoggerDataCollector $logger suite logger */ protected function printSummary(LoggerDataCollector $logger) { $this->printScenariosSummary($logger); $this->printStepsSummary($logger); if ($this->parameters->get('time')) { $this->printTimeSummary($logger); } } /** * Prints scenarios summary information. * * @param LoggerDataCollector $logger suite logger */ protected function printScenariosSummary(LoggerDataCollector $logger) { $count = $logger->getScenariosCount(); $header = $this->translateChoice('scenarios_count', $count, array('%1%' => $count)); $this->write($header); $this->printStatusesSummary($logger->getScenariosStatuses()); } /** * Prints steps summary information. * * @param LoggerDataCollector $logger suite logger */ protected function printStepsSummary(LoggerDataCollector $logger) { $count = $logger->getStepsCount(); $header = $this->translateChoice('steps_count', $count, array('%1%' => $count)); $this->write($header); $this->printStatusesSummary($logger->getStepsStatuses()); } /** * Prints statuses summary. * * @param array $statusesStatistics statuses statistic hash (status => count) */ protected function printStatusesSummary(array $statusesStatistics) { $statuses = array(); foreach ($statusesStatistics as $status => $count) { if ($count) { $transStatus = $this->translateChoice( "{$status}_count", $count, array('%1%' => $count) ); $statuses[] = "{+$status}$transStatus{-$status}"; } } $this->writeln(count($statuses) ? ' ' . sprintf('(%s)', implode(', ', $statuses)) : ''); } /** * Prints suite run time inforamtion. * * @param LoggerDataCollector $logger suite logger */ protected function printTimeSummary(LoggerDataCollector $logger) { $time = $logger->getTotalTime(); $minutes = floor($time / 60); $seconds = round($time - ($minutes * 60), 3); $this->writeln($minutes . 'm' . $seconds . 's'); } /** * Prints undefined steps snippets. * * @param LoggerDataCollector $logger suite logger */ protected function printUndefinedStepsSnippets(LoggerDataCollector $logger) { if ($this->getParameter('snippets') && count($logger->getDefinitionsSnippets())) { $header = $this->translate('proposal_title'); $this->writeln("\n{+undefined}$header{-undefined}\n"); $this->printSnippets($logger); } } /** * Prints steps snippets. * * @param LoggerDataCollector $logger suite logger */ protected function printSnippets(LoggerDataCollector $logger) { foreach ($logger->getDefinitionsSnippets() as $snippet) { $snippetText = $snippet->getSnippet(); if ($this->getParameter('snippets_paths')) { $indent = str_pad( '', mb_strlen($snippetText) - mb_strlen(ltrim($snippetText)), ' ' ); $this->writeln("{+undefined}$indent/**{-undefined}"); foreach ($snippet->getSteps() as $step) { $this->writeln(sprintf( '{+undefined}%s * %s %s # %s:%d{-undefined}', $indent, $step->getType(), $step->getText(), $this->relativizePathsInString($step->getFile()), $step->getLine() )); } if (false !== mb_strpos($snippetText, '/**')) { $snippetText = str_replace('/**', ' *', $snippetText); } else { $this->writeln("{+undefined}$indent */{-undefined}"); } } $this->writeln("{+undefined}$snippetText{-undefined}\n"); } } /** * Prints path comment. * * @param string $path item path * @param integer $indentCount indenation number */ protected function printPathComment($path, $indentCount = 0) { $indent = str_repeat(' ', $indentCount); $this->writeln("$indent {+comment}# $path{-comment}"); } /** * Returns string with relativized paths. * * @param string $string * * @return string */ protected function relativizePathsInString($string) { if ($basePath = $this->parameters->get('base_path')) { $basePath = realpath($basePath) . DIRECTORY_SEPARATOR; $string = str_replace($basePath, '', $string); } return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Snippets formatter. * * @author Konstantin Kudryashov */ class SnippetsFormatter extends ProgressFormatter { /** * {@inheritdoc} */ protected function getDefaultParameters() { return array(); } /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to */ public static function getSubscribedEvents() { return array('afterSuite' => 'afterSuite'); } /** * Listens to "suite.after" event. * * @param SuiteEvent $event * * @uses printUndefinedStepsSnippets() */ public function afterSuite(SuiteEvent $event) { $logger = $event->getLogger(); $this->writeln(); $this->printSnippets($logger); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Behat\Gherkin\Loader\AbstractFileLoader, Behat\Gherkin\Gherkin; use Symfony\Component\Finder\Finder; /** * Gherkin loader with features/ path support. * * @author Konstantin Kudryashov */ class FeatureSuiteLoader extends AbstractFileLoader { private $featuresPath; private $gherkin; /** * Initializes loader. * * @param string $featuresPath * @param Gherkin $gherkin */ public function __construct($featuresPath, Gherkin $gherkin) { $this->featuresPath = $featuresPath; $this->gherkin = $gherkin; } /** * Checks if current loader supports provided resource. * * @param mixed $resource Resource to load * * @return Boolean */ public function supports($resource) { return '' === $resource && is_dir($this->featuresPath); } /** * Loads features from provided resource. * * @param mixed $resource Resource to load * * @return array */ public function load($resource) { if (!$this->featuresPath || !file_exists($this->featuresPath)) { return array(); } $iterator = Finder::create() ->depth(0) ->followLinks() ->sortByName() ->in($this->featuresPath) ; $features = array(); foreach ($iterator as $path) { $resource = (string) $path; $loader = $this->gherkin->resolveLoader($resource); if (null !== $loader) { $features = array_merge($features, $loader->load($resource)); } } return $features; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Definitions printer. * * @author Konstantin Kudryashov */ class DefinitionsPrinter { private $dispatcher; /** * Initializes definition dispatcher. * * @param DefinitionDispatcher $dispatcher */ public function __construct(DefinitionDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } /** * Prints step definitions into console. * * @param OutputInterface $output * @param string $search * @param string $language * @param Boolean $shortNotation */ public function printDefinitions(OutputInterface $output, $search = null, $language = 'en', $shortNotation = true) { $output->getFormatter()->setStyle( 'capture', new OutputFormatterStyle('yellow', null, array('bold')) ); $output->getFormatter()->setStyle( 'path', new OutputFormatterStyle('black') ); $output->writeln($this->getDefinitionsForPrint($search, $language, $shortNotation)); } /** * Returns available definitions in string. * * @param string $search search string * @param string $language default definitions language * @param Boolean $shortNotation show short notation instead of full one * * @return string */ private function getDefinitionsForPrint($search = null, $language = 'en', $shortNotation = true) { if ($shortNotation) { $template = '{type} {regex}'; } else { $template = <<{type} {regex} {description}# {path} TPL; } $definitions = array(); foreach ($this->dispatcher->getDefinitions() as $regex => $definition) { $regex = $this->dispatcher->translateDefinitionRegex($regex, $language); if ($search && !preg_match('/'.str_replace(' ', '.*', preg_quote($search, '/').'/'), $regex)) { continue; } $regex = preg_replace_callback('/\((?!\?:)[^\)]*\)/', function($capture) { return "{$capture[0]}"; }, $regex); $definitions[] = strtr($template, array( '{regex}' => $regex, '{type}' => str_pad($definition->getType(), 5, ' ', STR_PAD_LEFT), '{description}' => $definition->getDescription() ? '- '.$definition->getDescription()."\n " : '', '{path}' => $definition->getPath() )); } return implode("\n", $definitions); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Story syntax printer. * * @author Konstantin Kudryashov */ class StorySyntaxPrinter { private $dumper; /** * Initializes definition dispatcher. * * @param KeywordsDumper $dumper */ public function __construct(KeywordsDumper $dumper) { $dumper->setKeywordsDumperFunction(array($this, 'dumpKeywords')); $this->dumper = $dumper; } /** * Prints example story syntax into console. * * @param OutputInterface $output * @param string $language */ public function printSyntax(OutputInterface $output, $language = 'en') { $output->getFormatter()->setStyle('comment', new OutputFormatterStyle('yellow')); $output->getFormatter()->setStyle( 'keyword', new OutputFormatterStyle('green', null, array('bold')) ); $story = $this->dumper->dump($language); $story = preg_replace('/^\#.*/', '$0', $story); $output->writeln($story); } /** * Keywords dumper. * * @param array $keywords keywords list * * @return string */ public function dumpKeywords(array $keywords) { $dump = ''.implode('|', $keywords).''; if (1 < count($keywords)) { return '['.$dump.']'; } return $dump; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * AfterFeature hook class. * * @author Konstantin Kudryashov */ class AfterFeature extends FeatureHook { /** * {@inheritdoc} */ public function getEventName() { return 'afterFeature'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * AfterScenario hook class. * * @author Konstantin Kudryashov */ class AfterScenario extends ScenarioHook { /** * {@inheritdoc} */ public function getEventName() { return 'afterScenario'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * AfterStep hook class. * * @author Konstantin Kudryashov */ class AfterStep extends StepHook { /** * {@inheritdoc} */ public function getEventName() { return 'afterStep'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * AfterSuite hook class. * * @author Konstantin Kudryashov */ class AfterSuite extends SuiteHook { /** * {@inheritdoc} */ public function getEventName() { return 'afterSuite'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * BeforeFeature hook class. * * @author Konstantin Kudryashov */ class BeforeFeature extends FeatureHook { /** * {@inheritdoc} */ public function getEventName() { return 'beforeFeature'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * BeforeScenario hook class. * * @author Konstantin Kudryashov */ class BeforeScenario extends ScenarioHook { /** * {@inheritdoc} */ public function getEventName() { return 'beforeScenario'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * BeforeStep hook class. * * @author Konstantin Kudryashov */ class BeforeStep extends StepHook { /** * {@inheritdoc} */ public function getEventName() { return 'beforeStep'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * BeforeSuite hook class. * * @author Konstantin Kudryashov */ class BeforeSuite extends SuiteHook { /** * {@inheritdoc} */ public function getEventName() { return 'beforeSuite'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * FeatureHook hook class. * * @author Konstantin Kudryashov */ abstract class FeatureHook extends FilterableHook { /** * {@inheritdoc} */ public function __construct($callback, $filterString = null) { parent::__construct($callback, $filterString); if (!$this->isClosure()) { $methodRefl = new \ReflectionMethod($callback[0], $callback[1]); if (!$methodRefl->isStatic()) { throw new \InvalidArgumentException(sprintf( '"%s" hook callback: %s::%s() must be a static method', basename(str_replace('\\', '/', get_class($this))), $callback[0], $callback[1] )); } } } /** * {@inheritdoc} */ public function filterMatches(EventInterface $event) { if (null === ($filterString = $this->getFilter())) { return true; } $feature = $event->getFeature(); if (false !== strpos($filterString, '@')) { $filter = new TagFilter($filterString); if ($filter->isFeatureMatch($feature)) { return true; } } elseif (!empty($filterString)) { $filter = new NameFilter($filterString); if ($filter->isFeatureMatch($feature)) { return true; } } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base filterable hook class. * * @author Konstantin Kudryashov */ abstract class FilterableHook extends Hook { private $filter; /** * Initializes hook. * * @param callback $callback callback * @param string $filterString hook filter */ public function __construct($callback, $filterString = null) { parent::__construct($callback); $this->filter = $filterString; } /** * Returns filter string. * * @return stirng */ public function getFilter() { return $this->filter; } /** * Checks that current hook matches provided event object. * * @param EventInterface $event * * @return Boolean */ abstract public function filterMatches(EventInterface $event); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base hook class. * * @author Konstantin Kudryashov */ abstract class Hook extends Annotation implements HookInterface { /** * Constructs annotation. * * @param callback $callback * * @throws \InvalidArgumentException */ public function __construct($callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException(sprintf( '"%s" hook callback should be a valid callable, but %s given', basename(str_replace('\\', '/', get_class($this))), gettype($callback) )); } parent::__construct($callback); } /** * Runs hook callback. * * @param EventInterface $event */ public function run(EventInterface $event) { call_user_func($this->getCallback(), $event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * ScenarioHook hook class. * * @author Konstantin Kudryashov */ abstract class ScenarioHook extends FilterableHook { /** * {@inheritdoc} */ public function filterMatches(EventInterface $event) { if (null === ($filterString = $this->getFilter())) { return true; } if ($event instanceof ScenarioEvent) { $scenario = $event->getScenario(); } else { $scenario = $event->getOutline(); } if (false !== strpos($filterString, '@')) { $filter = new TagFilter($filterString); if ($filter->isScenarioMatch($scenario)) { return true; } } elseif (!empty($filterString)) { $filter = new NameFilter($filterString); if ($filter->isScenarioMatch($scenario)) { return true; } } return false; } /** * Runs hook callback. * * @param EventInterface $event */ public function run(EventInterface $event) { call_user_func($this->getCallbackForContext($event->getContext()), $event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * StepHook hook class. * * @author Konstantin Kudryashov */ abstract class StepHook extends FilterableHook { /** * {@inheritdoc} */ public function filterMatches(EventInterface $event) { if (null === ($filterString = $this->getFilter())) { return true; } $scenario = $event->getLogicalParent(); if (false !== strpos($filterString, '@')) { $filter = new TagFilter($filterString); if ($filter->isScenarioMatch($scenario)) { return true; } } elseif (!empty($filterString)) { $filter = new NameFilter($filterString); if ($filter->isScenarioMatch($scenario)) { return true; } } return false; } /** * Runs hook callback. * * @param EventInterface $event */ public function run(EventInterface $event) { call_user_func($this->getCallbackForContext($event->getContext()), $event); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * SuiteHook hook class. * * @author Konstantin Kudryashov */ abstract class SuiteHook extends Hook { /** * {@inheritdoc} */ public function __construct($callback) { parent::__construct($callback); if (!$this->isClosure()) { $methodRefl = new \ReflectionMethod($callback[0], $callback[1]); if (!$methodRefl->isStatic()) { throw new \InvalidArgumentException(sprintf( '"%s" hook callback: %s::%s() must be a static method', basename(str_replace('\\', '/', get_class($this))), $callback[0], $callback[1] )); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Hook dispatcher. * * @author Konstantin Kudryashov */ class HookDispatcher implements EventSubscriberInterface { private $hooks = array(); private $dryRun = false; /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array The event names to listen to */ public static function getSubscribedEvents() { $events = array( 'beforeSuite', 'afterSuite', 'beforeFeature', 'afterFeature', 'beforeScenario', 'afterScenario', 'beforeOutlineExample', 'afterOutlineExample', 'beforeStep', 'afterStep' ); return array_combine($events, $events); } /** * Sets hook dispatcher to dry-run mode. * * @param Boolean $dryRun */ public function setDryRun($dryRun = true) { $this->dryRun = (bool) $dryRun; } /** * Adds hook into dispatcher. * * @param HookInterface $hook */ public function addHook(HookInterface $hook) { if (!isset($this->hooks[$hook->getEventName()])) { $this->hooks[$hook->getEventName()] = array(); } $this->hooks[$hook->getEventName()][] = $hook; } /** * Returns all available hooks. * * @return array */ public function getHooks() { return $this->hooks; } /** * Cleans dispatcher. */ public function clean() { $this->hooks = array(); } /** * Listens to "suite.before" event. * * @param SuiteEvent $event * * @uses fireSuiteHooks() */ public function beforeSuite(SuiteEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Listens to "suite.after" event. * * @param SuiteEvent $event * * @uses fireSuiteHooks() */ public function afterSuite(SuiteEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Listens to "feature.before" event. * * @param FeatureEvent $event * * @uses fireFeatureHooks() */ public function beforeFeature(FeatureEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Listens to "feature.after" event. * * @param FeatureEvent $event * * @uses fireFeatureHooks() */ public function afterFeature(FeatureEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Listens to "scenario.before" event. * * @param ScenarioEvent $event * * @uses fireScenarioHooks() */ public function beforeScenario(ScenarioEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Listens to "scenario.after" event. * * @param ScenarioEvent $event * * @uses fireScenarioHooks() */ public function afterScenario(ScenarioEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Listens to "outline.example.before" event. * * @param OutlineExampleEvent $event * * @uses fireScenarioHooks() */ public function beforeOutlineExample(OutlineExampleEvent $event) { $this->fireHooks('beforeScenario', $event); } /** * Listens to "outline.example.after" event. * * @param OutlineExampleEvent $event * * @uses fireScenarioHooks() */ public function afterOutlineExample(OutlineExampleEvent $event) { $this->fireHooks('afterScenario', $event); } /** * Listens to "step.before" event. * * @param StepEvent $event * * @uses fireStepHooks() */ public function beforeStep(StepEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Listens to "step.after" event. * * @param StepEvent $event * * @uses fireStepHooks() */ public function afterStep(StepEvent $event) { $this->fireHooks(__FUNCTION__, $event); } /** * Custom error handler. * * This method used as custom error handler when step is running. * * @see set_error_handler() * * @param $code * @param $message * @param $file * @param $line * * @throws \Behat\Behat\Exception\ErrorException */ public function errorHandler($code, $message, $file, $line) { if (0 === error_reporting()) { return; // error reporting turned off or more likely suppresed with @ } throw new ErrorException($code, $message, $file, $line); } /** * Runs hooks with specified name. * * @param string $name hooks name * @param EventInterface $event event to which hooks glued * * @throws \Exception */ protected function fireHooks($name, EventInterface $event) { if ($this->dryRun) { return; } $hooks = isset($this->hooks[$name]) ? $this->hooks[$name] : array(); foreach ($hooks as $hook) { $runable = $hook instanceof FilterableHook ? $hook->filterMatches($event) : true; if ($runable) { if (defined('BEHAT_ERROR_REPORTING')) { $errorLevel = BEHAT_ERROR_REPORTING; } else { $errorLevel = E_ALL ^ E_WARNING; } set_error_handler(array($this, 'errorHandler'), $errorLevel); try { $hook->run($event); } catch (\Exception $e) { restore_error_handler(); $this->addHookInformationToException($hook, $e); throw $e; } restore_error_handler(); } } } /** * Adds hook information to exception thrown from it. * * @param HookInterface $hook hook instance * @param \Exception $exception exception */ private function addHookInformationToException(HookInterface $hook, \Exception $exception) { $refl = new \ReflectionObject($exception); $message = $refl->getProperty('message'); $message->setAccessible(true); $message->setValue($exception, sprintf( 'Exception has been thrown in "%s" hook, defined in %s'."\n\n%s", $hook->getEventName(), $hook->getPath(), $exception->getMessage() )); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Hook interface. * * @author Konstantin Kudryashov */ interface HookInterface { /** * Returns hooked event type. * * @return string */ public function getEventName(); /** * Runs hook callback. * * @param EventInterface $event */ public function run(EventInterface $event); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Closured hook definitions loader. * * @author Konstantin Kudryashov */ class ClosuredHookLoader implements HookLoaderInterface { private $dispatcher; /** * Initializes loader. * * @param HookDispatcher $dispatcher */ public function __construct(HookDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } /** * Loads definitions from provided resource. * * @param mixed $resource */ public function load($resource) { $hooks = $this; require_once($resource); } /** * Hooks into "suite.before". * * @param callback $callback hook callback */ public function beforeSuite($callback) { $this->dispatcher->addHook(new BeforeSuite($callback)); } /** * Hooks into "suite.after". * * @param callback $callback hook callback */ public function afterSuite($callback) { $this->dispatcher->addHook(new AfterSuite($callback)); } /** * Hooks into "feature.before". * * @param string $filter filter string (tags or name) * @param callback $callback hook callback */ public function beforeFeature($filter, $callback) { $this->dispatcher->addHook(new BeforeFeature($callback, '' !== $filter ? $filter : null)); } /** * Hooks into "feature.after". * * @param string $filter filter string (tags or name) * @param callback $callback hook callback */ public function afterFeature($filter, $callback) { $this->dispatcher->addHook(new AfterFeature($callback, '' !== $filter ? $filter : null)); } /** * Hooks into "scenario.before" OR "outline.example.before". * * @param string $filter filter string (tags or name) * @param callback $callback hook callback */ public function beforeScenario($filter, $callback) { $this->dispatcher->addHook(new BeforeScenario($callback, '' !== $filter ? $filter : null)); } /** * Hooks into "scenario.after" OR "outline.example.after". * * @param string $filter filter string (tags or name) * @param callback $callback hook callback */ public function afterScenario($filter, $callback) { $this->dispatcher->addHook(new AfterScenario($callback, '' !== $filter ? $filter : null)); } /** * Hooks into "step.before". * * @param string $filter filter string (tags or name) * @param callback $callback hook callback */ public function beforeStep($filter, $callback) { $this->dispatcher->addHook(new BeforeStep($callback, '' !== $filter ? $filter : null)); } /** * Hooks into "step.after". * * @param string $filter filter string (tags or name) * @param callback $callback hook callback */ public function afterStep($filter, $callback) { $this->dispatcher->addHook(new AfterStep($callback, '' !== $filter ? $filter : null)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Hook loader interface. * * @author Konstantin Kudryashov */ interface HookLoaderInterface { /** * Loads definitions from provided resource. * * @param mixed $resource */ public function load($resource); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Background tester. * * @author Konstantin Kudryashov */ class BackgroundTester implements NodeVisitorInterface { private $logicalParent; private $container; private $dispatcher; private $context; private $skip = false; /** * Initializes tester. * * @param ContainerInterface $container */ public function __construct(ContainerInterface $container) { $this->container = $container; $this->dispatcher = $container->get('behat.event_dispatcher'); } /** * Sets logical parent of the step, which is always a ScenarioNode. * * @param ScenarioNode $parent */ public function setLogicalParent(ScenarioNode $parent) { $this->logicalParent = $parent; } /** * Sets run context. * * @param ContextInterface $context */ public function setContext(ContextInterface $context) { $this->context = $context; } /** * Sets tester to dry-run mode. * * @param Boolean $skip */ public function setSkip($skip = true) { $this->skip = (bool) $skip; } /** * Visits & tests BackgroundNode. * * @param AbstractNode $background * * @return integer */ public function visit(AbstractNode $background) { $this->dispatcher->dispatch('beforeBackground', new BackgroundEvent($background)); $result = 0; $skip = false; // Visit & test steps foreach ($background->getSteps() as $step) { $tester = $this->container->get('behat.tester.step'); $tester->setLogicalParent($this->logicalParent); $tester->setContext($this->context); $tester->skip($skip || $this->skip); $stResult = $step->accept($tester); if (0 !== $stResult) { $skip = true; } $result = max($result, $stResult); } $this->dispatcher->dispatch('afterBackground', new BackgroundEvent($background, $result, $skip)); return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Feature tester. * * @author Konstantin Kudryashov */ class FeatureTester implements NodeVisitorInterface { private $container; private $dispatcher; private $parameters; private $skip = false; /** * Initializes tester. * * @param ContainerInterface $container */ public function __construct(ContainerInterface $container) { $this->container = $container; $this->dispatcher = $container->get('behat.event_dispatcher'); $this->parameters = $container->get('behat.context.dispatcher')->getContextParameters(); } /** * Sets tester to dry-run mode. * * @param Boolean $skip */ public function setSkip($skip = true) { $this->skip = (bool) $skip; } /** * Visits & tests FeatureNode. * * @param AbstractNode $feature * * @return integer * * @throws BehaviorException if unknown scenario type (neither Outline or Scenario) found in feature */ public function visit(AbstractNode $feature) { $this->dispatcher->dispatch( 'beforeFeature', new FeatureEvent($feature, $this->parameters) ); $result = 0; $skip = false; // Visit & test scenarios foreach ($feature->getScenarios() as $scenario) { if ($scenario instanceof OutlineNode) { $tester = $this->container->get('behat.tester.outline'); } elseif ($scenario instanceof ScenarioNode) { $tester = $this->container->get('behat.tester.scenario'); } else { throw new BehaviorException( 'Unknown scenario type found: ' . get_class($scenario) ); } $tester->setSkip($skip || $this->skip); $result = max($result, $scenario->accept($tester)); } $this->dispatcher->dispatch( 'afterFeature', new FeatureEvent($feature, $this->parameters, $result) ); return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Outline tester. * * @author Konstantin Kudryashov */ class OutlineTester extends ScenarioTester { /** * Initializes tester. * * @param ContainerInterface $container */ public function __construct(ContainerInterface $container) { $this->container = $container; $this->dispatcher = $container->get('behat.event_dispatcher'); } /** * Visits & tests OutlineNode. * * @param AbstractNode $outline * * @return integer */ public function visit(AbstractNode $outline) { $this->dispatcher->dispatch('beforeOutline', new OutlineEvent($outline)); $result = 0; // Run examples of outline foreach ($outline->getExamples()->getHash() as $iteration => $tokens) { $itResult = $this->visitOutlineExample($outline, $iteration, $tokens); $result = max($result, $itResult); } $this->dispatcher->dispatch('afterOutline', new OutlineEvent($outline, $result)); return $result; } /** * Visits & tests OutlineNode example. * * @param OutlineNode $outline outline instance * @param integer $row row number * @param array $tokens step replacements for tokens * * @return integer */ private function visitOutlineExample(OutlineNode $outline, $row, array $tokens = array()) { $context = $this->container->get('behat.context.dispatcher')->createContext(); $itResult = 0; $skip = false; $this->dispatcher->dispatch('beforeOutlineExample', new OutlineExampleEvent( $outline, $row, $context )); // Visit & test background if has one if ($outline->getFeature()->hasBackground()) { $bgResult = $this->visitBackground( $outline->getFeature()->getBackground(), $outline, $context ); if (0 !== $bgResult) { $skip = true; } $itResult = max($itResult, $bgResult); } // Visit & test steps foreach ($outline->getSteps() as $step) { $stResult = $this->visitStep($step, $outline, $context, $tokens, $skip); if (0 !== $stResult) { $skip = true; } $itResult = max($itResult, $stResult); } $this->dispatcher->dispatch('afterOutlineExample', new OutlineExampleEvent( $outline, $row, $context, $itResult, $skip )); return $itResult; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Scenario Tester. * * @author Konstantin Kudryashov */ class ScenarioTester implements NodeVisitorInterface { protected $container; protected $dispatcher; private $context; private $skip = false; /** * Initializes tester. * * @param ContainerInterface $container */ public function __construct(ContainerInterface $container) { $this->container = $container; $this->dispatcher = $container->get('behat.event_dispatcher'); $this->context = $container->get('behat.context.dispatcher')->createContext(); } /** * Sets tester to dry-run mode. * * @param Boolean $skip */ public function setSkip($skip = true) { $this->skip = (bool) $skip; } /** * Visits & tests ScenarioNode. * * @param AbstractNode $scenario * * @return integer */ public function visit(AbstractNode $scenario) { $this->dispatcher->dispatch('beforeScenario', new ScenarioEvent($scenario, $this->context)); $result = 0; $skip = false; // Visit & test background if has one if ($scenario->getFeature()->hasBackground()) { $bgResult = $this->visitBackground( $scenario->getFeature()->getBackground(), $scenario, $this->context ); if (0 !== $bgResult) { $skip = true; } $result = max($result, $bgResult); } // Visit & test steps foreach ($scenario->getSteps() as $step) { $stResult = $this->visitStep($step, $scenario, $this->context, array(), $skip); if (0 !== $stResult) { $skip = true; } $result = max($result, $stResult); } $this->dispatcher->dispatch('afterScenario', new ScenarioEvent( $scenario, $this->context, $result, $skip )); return $result; } /** * Visits & tests BackgroundNode. * * @param BackgroundNode $background * @param ScenarioNode $logicalParent * @param ContextInterface $context * * @see BackgroundTester::visit() * * @return integer */ protected function visitBackground(BackgroundNode $background, ScenarioNode $logicalParent, ContextInterface $context) { $tester = $this->container->get('behat.tester.background'); $tester->setLogicalParent($logicalParent); $tester->setContext($context); $tester->setSkip($this->skip); return $background->accept($tester); } /** * Visits & tests StepNode. * * @param StepNode $step step instance * @param ScenarioNode $logicalParent logical parent of the step * @param ContextInterface $context context instance * @param array $tokens step replacements for tokens * @param boolean $skip mark step as skipped? * * @see StepTester::visit() * * @return integer */ protected function visitStep(StepNode $step, ScenarioNode $logicalParent, ContextInterface $context, array $tokens = array(), $skip = false) { if ($logicalParent instanceof OutlineNode) { $step = $step->createExampleRowStep($tokens); } $tester = $this->container->get('behat.tester.step'); $tester->setLogicalParent($logicalParent); $tester->setContext($context); $tester->skip($skip || $this->skip); return $step->accept($tester); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Step Tester. * * @author Konstantin Kudryashov */ class StepTester implements NodeVisitorInterface { private $logicalParent; private $dispatcher; private $context; private $definitions; private $skip = false; /** * Initializes tester. * * @param ContainerInterface $container */ public function __construct(ContainerInterface $container) { $this->dispatcher = $container->get('behat.event_dispatcher'); $this->definitions = $container->get('behat.definition.dispatcher'); } /** * Sets logical parent of the step, which is always a ScenarioNode. * * @param ScenarioNode $parent */ public function setLogicalParent(ScenarioNode $parent) { $this->logicalParent = $parent; } /** * Sets run context. * * @param ContextInterface $context */ public function setContext(ContextInterface $context) { $this->context = $context; } /** * Marks test as skipped. * * @param Boolean $skip skip test? */ public function skip($skip = true) { $this->skip = $skip; } /** * Visits & tests StepNode. * * @param AbstractNode $step * * @return integer */ public function visit(AbstractNode $step) { $this->dispatcher->dispatch('beforeStep', new StepEvent( $step, $this->logicalParent, $this->context )); $afterEvent = $this->executeStep($step); $this->dispatcher->dispatch('afterStep', $afterEvent); return $afterEvent->getResult(); } /** * Searches and runs provided step with DefinitionDispatcher. * * @param StepNode $step step node * * @return StepEvent */ protected function executeStep(StepNode $step) { $context = $this->context; $result = null; $definition = null; $exception = null; $snippet = null; try { $definition = $this->definitions->findDefinition($this->context, $step); if ($this->skip) { return new StepEvent( $step, $this->logicalParent, $context, StepEvent::SKIPPED, $definition ); } try { $this->executeStepDefinition($step, $definition); $result = StepEvent::PASSED; } catch (PendingException $e) { $result = StepEvent::PENDING; $exception = $e; } catch (\Exception $e) { $result = StepEvent::FAILED; $exception = $e; } } catch (UndefinedException $e) { $result = StepEvent::UNDEFINED; $snippet = $this->definitions->proposeDefinition($this->context, $step); $exception = $e; } catch (\Exception $e) { $result = StepEvent::FAILED; $exception = $e; } return new StepEvent( $step, $this->logicalParent, $context, $result, $definition, $exception, $snippet ); } /** * Executes provided step definition. * * @param StepNode $step step node * @param DefinitionInterface $definition step definition */ protected function executeStepDefinition(StepNode $step, DefinitionInterface $definition) { $this->executeStepsChain($step, $definition->run($this->context)); } /** * Executes steps chain (if there's one). * * @param StepNode $step step node * @param mixed $chain chain * * @throws \Exception */ private function executeStepsChain(StepNode $step, $chain = null) { if (null === $chain) { return; } $chain = is_array($chain) ? $chain : array($chain); foreach ($chain as $chainItem) { if ($chainItem instanceof SubstepInterface) { $substepNode = $chainItem->getStepNode(); $substepNode->setParent($step->getParent()); $substepEvent = $this->executeStep($substepNode); if (StepEvent::PASSED !== $substepEvent->getResult()) { throw $substepEvent->getException(); } } elseif (is_callable($chainItem)) { $this->executeStepsChain($step, call_user_func($chainItem)); } } } } ','?','@','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',']','\\',']','^','_','`','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','{','|','}','~','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','',' ','!','C/','PS','$?','Y=','|','SS','"','(c)','a','<<','!','','(r)','-','deg','+-','2','3',"'",'u','P','*',',','1','o','>>','1/4','1/2','3/4','?','A','A','A','A','A','A','AE','C','E','E','E','E','I','I','I','I','D','N','O','O','O','O','O','x','O','U','U','U','U','U','Th','ss','a','a','a','a','a','a','ae','c','e','e','e','e','i','i','i','i','d','n','o','o','o','o','o','/','o','u','u','u','u','y','th','y', ); ','^','V','^','V',"'",'-','/','\\',',','_','\\','/',':','.','`',"'",'^','V','+','-','V','.','@',',','~','"','R','X','G','l','s','x','?','','','','','','','','V','=','"','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]', ); ','[?]','[?]','[?]','f','v','u','yr','y','w','th','th','a','o','ac','ae','o','o','o','oe','on','r','k','c','k','g','ng','g','g','w','h','h','h','h','n','n','n','i','e','j','g','ae','a','eo','p','z','s','s','s','c','z','t','t','d','b','b','p','p','e','m','m','m','l','l','ng','ng','d','o','ear','ior','qu','qu','qu','s','yr','yr','yr','q','x','.',':','+','17','18','19','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]', ); ','.','..','...','.',' ',' ','','','','','',' ','%0','%00',"'","''","'''",'`','``','```','^','<','>','*','!!','!?','-','_','-','^','***','--','/','-[',']-','[?]','?!','!?','7','PP','(]','[)','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','','','','','','','0','','','','4','5','6','7','8','9','+','-','=','(',')','n','0','1','2','3','4','5','6','7','8','9','+','-','=','(',')','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','ECU','CL','Cr','FF','L','mil','N','Pts','Rs','W','NS','D','EU','K','T','Dr','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','','','','','','','','','','','','','','','','','','','','','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]', ); ','>','>','>','>','>','V','V','V','V','<','<','<','<','<','<','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','#','#','#','#','#','^','^','^','O','#','#','#','#','#','#','#','#','[?]','[?]','[?]','[?]','[?]','[?]','[?]', ); ','n','t','q',',','*','5','<','-','u','8','v','.','%','[','$','+','x','!','&',';',':','4','\\','0','z','7','(','_','?','w',']','#','y',')','=','[d7]','[d17]','[d27]','[d127]','[d37]','[d137]','[d237]','[d1237]','[d47]','[d147]','[d247]','[d1247]','[d347]','[d1347]','[d2347]','[d12347]','[d57]','[d157]','[d257]','[d1257]','[d357]','[d1357]','[d2357]','[d12357]','[d457]','[d1457]','[d2457]','[d12457]','[d3457]','[d13457]','[d23457]','[d123457]','[d67]','[d167]','[d267]','[d1267]','[d367]','[d1367]','[d2367]','[d12367]','[d467]','[d1467]','[d2467]','[d12467]','[d3467]','[d13467]','[d23467]','[d123467]','[d567]','[d1567]','[d2567]','[d12567]','[d3567]','[d13567]','[d23567]','[d123567]','[d4567]','[d14567]','[d24567]','[d124567]','[d34567]','[d134567]','[d234567]','[d1234567]','[d8]','[d18]','[d28]','[d128]','[d38]','[d138]','[d238]','[d1238]','[d48]','[d148]','[d248]','[d1248]','[d348]','[d1348]','[d2348]','[d12348]','[d58]','[d158]','[d258]','[d1258]','[d358]','[d1358]','[d2358]','[d12358]','[d458]','[d1458]','[d2458]','[d12458]','[d3458]','[d13458]','[d23458]','[d123458]','[d68]','[d168]','[d268]','[d1268]','[d368]','[d1368]','[d2368]','[d12368]','[d468]','[d1468]','[d2468]','[d12468]','[d3468]','[d13468]','[d23468]','[d123468]','[d568]','[d1568]','[d2568]','[d12568]','[d3568]','[d13568]','[d23568]','[d123568]','[d4568]','[d14568]','[d24568]','[d124568]','[d34568]','[d134568]','[d234568]','[d1234568]','[d78]','[d178]','[d278]','[d1278]','[d378]','[d1378]','[d2378]','[d12378]','[d478]','[d1478]','[d2478]','[d12478]','[d3478]','[d13478]','[d23478]','[d123478]','[d578]','[d1578]','[d2578]','[d12578]','[d3578]','[d13578]','[d23578]','[d123578]','[d4578]','[d14578]','[d24578]','[d124578]','[d34578]','[d134578]','[d234578]','[d1234578]','[d678]','[d1678]','[d2678]','[d12678]','[d3678]','[d13678]','[d23678]','[d123678]','[d4678]','[d14678]','[d24678]','[d124678]','[d34678]','[d134678]','[d234678]','[d1234678]','[d5678]','[d15678]','[d25678]','[d125678]','[d35678]','[d135678]','[d235678]','[d1235678]','[d45678]','[d145678]','[d245678]','[d1245678]','[d345678]','[d1345678]','[d2345678]','[d12345678]', ); ','<<','>> ','[','] ','{','} ','[(',')] ','@','X ','[','] ','[[',']] ','((',')) ','[[',']] ','~ ','``',"''",',,','@','1','2','3','4','5','6','7','8','9','','','','','','','~','+','+','+','+','','@',' // ','+10+','+20+','+30+','[?]','[?]','[?]','','','[?]','a','a','i','i','u','u','e','e','o','o','ka','ga','ki','gi','ku','gu','ke','ge','ko','go','sa','za','si','zi','su','zu','se','ze','so','zo','ta','da','ti','di','tu','tu','du','te','de','to','do','na','ni','nu','ne','no','ha','ba','pa','hi','bi','pi','hu','bu','pu','he','be','pe','ho','bo','po','ma','mi','mu','me','mo','ya','ya','yu','yu','yo','yo','ra','ri','ru','re','ro','wa','wa','wi','we','wo','n','vu','[?]','[?]','[?]','[?]','','','','','"','"','[?]','[?]','a','a','i','i','u','u','e','e','o','o','ka','ga','ki','gi','ku','gu','ke','ge','ko','go','sa','za','si','zi','su','zu','se','ze','so','zo','ta','da','ti','di','tu','tu','du','te','de','to','do','na','ni','nu','ne','no','ha','ba','pa','hi','bi','pi','hu','bu','pu','he','be','pe','ho','bo','po','ma','mi','mu','me','mo','ya','ya','yu','yu','yo','yo','ra','ri','ru','re','ro','wa','wa','wi','we','wo','n','vu','ka','ke','va','vi','ve','vo','','','"','"', ); >','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','(g)','(n)','(d)','(r)','(m)','(b)','(s)','()','(j)','(c)','(k)','(t)','(p)','(h)','(ga)','(na)','(da)','(ra)','(ma)','(ba)','(sa)','(a)','(ja)','(ca)','(ka)','(ta)','(pa)','(ha)','[?]','[?]','[?]','KIS ','(1) ','(2) ','(3) ','(4) ','(5) ','(6) ','(7) ','(8) ','(9) ','(10) ','(Yue) ','(Huo) ','(Shui) ','(Mu) ','(Jin) ','(Tu) ','(Ri) ','(Zhu) ','(You) ','(She) ','(Ming) ','(Te) ','(Cai) ','(Zhu) ','(Lao) ','(Mi) ','(Nan) ','(Nu) ','(Shi) ','(You) ','(Yin) ','(Zhu) ','(Xiang) ','(Xiu) ','(Xie) ','(Zheng) ','(Shang) ','(Zhong) ','(Xia) ','(Zuo) ','(You) ','(Yi) ','(Zong) ','(Xue) ','(Jian) ','(Qi) ','(Zi) ','(Xie) ','(Ye) ','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','[?]','1M','2M','3M','4M','5M','6M','7M','8M','9M','10M','11M','12M','[?]','[?]','[?]','[?]','a','i','u','u','o','ka','ki','ku','ke','ko','sa','si','su','se','so','ta','ti','tu','te','to','na','ni','nu','ne','no','ha','hi','hu','he','ho','ma','mi','mu','me','mo','ya','yu','yo','ra','ri','ru','re','ro','wa','wi','we','wo', ); * @author Jonathan H. Wage * @author */ class Transliterator { /** * Check if a string has utf7 characters in it * * By bmorel at ssi dot fr * * @param string $string * @return boolean $bool */ public static function seemsUtf8($string) { for ($i = 0; $i < strlen($string); $i++) { if (ord($string[$i]) < 0x80) continue; # 0bbbbbbb elseif ((ord($string[$i]) & 0xE0) == 0xC0) $n=1; # 110bbbbb elseif ((ord($string[$i]) & 0xF0) == 0xE0) $n=2; # 1110bbbb elseif ((ord($string[$i]) & 0xF8) == 0xF0) $n=3; # 11110bbb elseif ((ord($string[$i]) & 0xFC) == 0xF8) $n=4; # 111110bb elseif ((ord($string[$i]) & 0xFE) == 0xFC) $n=5; # 1111110b else return false; # Does not match any model for ($j=0; $j<$n; $j++) { # n bytes matching 10bbbbbb follow ? if ((++$i == strlen($string)) || ((ord($string[$i]) & 0xC0) != 0x80)) return false; } } return true; } /** * Remove any illegal characters, accents, etc. * * @param string $string String to unaccent * @return string $string Unaccented string */ public static function unaccent($string) { if (!preg_match('/[\x80-\xff]/', $string)) { return $string; } if (self::seemsUtf8($string)) { $chars = array( // Decompositions for Latin-1 Supplement chr(195).chr(128) => 'A', chr(195).chr(129) => 'A', chr(195).chr(130) => 'A', chr(195).chr(131) => 'A', chr(195).chr(132) => 'A', chr(195).chr(133) => 'A', chr(195).chr(135) => 'C', chr(195).chr(136) => 'E', chr(195).chr(137) => 'E', chr(195).chr(138) => 'E', chr(195).chr(139) => 'E', chr(195).chr(140) => 'I', chr(195).chr(141) => 'I', chr(195).chr(142) => 'I', chr(195).chr(143) => 'I', chr(195).chr(145) => 'N', chr(195).chr(146) => 'O', chr(195).chr(147) => 'O', chr(195).chr(148) => 'O', chr(195).chr(149) => 'O', chr(195).chr(150) => 'O', chr(195).chr(153) => 'U', chr(195).chr(154) => 'U', chr(195).chr(155) => 'U', chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y', chr(195).chr(159) => 's', chr(195).chr(160) => 'a', chr(195).chr(161) => 'a', chr(195).chr(162) => 'a', chr(195).chr(163) => 'a', chr(195).chr(164) => 'a', chr(195).chr(165) => 'a', chr(195).chr(167) => 'c', chr(195).chr(168) => 'e', chr(195).chr(169) => 'e', chr(195).chr(170) => 'e', chr(195).chr(171) => 'e', chr(195).chr(172) => 'i', chr(195).chr(173) => 'i', chr(195).chr(174) => 'i', chr(195).chr(175) => 'i', chr(195).chr(177) => 'n', chr(195).chr(178) => 'o', chr(195).chr(179) => 'o', chr(195).chr(180) => 'o', chr(195).chr(181) => 'o', chr(195).chr(182) => 'o', chr(195).chr(182) => 'o', chr(195).chr(185) => 'u', chr(195).chr(186) => 'u', chr(195).chr(187) => 'u', chr(195).chr(188) => 'u', chr(195).chr(189) => 'y', chr(195).chr(191) => 'y', // Decompositions for Latin Extended-A chr(196).chr(128) => 'A', chr(196).chr(129) => 'a', chr(196).chr(130) => 'A', chr(196).chr(131) => 'a', chr(196).chr(132) => 'A', chr(196).chr(133) => 'a', chr(196).chr(134) => 'C', chr(196).chr(135) => 'c', chr(196).chr(136) => 'C', chr(196).chr(137) => 'c', chr(196).chr(138) => 'C', chr(196).chr(139) => 'c', chr(196).chr(140) => 'C', chr(196).chr(141) => 'c', chr(196).chr(142) => 'D', chr(196).chr(143) => 'd', chr(196).chr(144) => 'D', chr(196).chr(145) => 'd', chr(196).chr(146) => 'E', chr(196).chr(147) => 'e', chr(196).chr(148) => 'E', chr(196).chr(149) => 'e', chr(196).chr(150) => 'E', chr(196).chr(151) => 'e', chr(196).chr(152) => 'E', chr(196).chr(153) => 'e', chr(196).chr(154) => 'E', chr(196).chr(155) => 'e', chr(196).chr(156) => 'G', chr(196).chr(157) => 'g', chr(196).chr(158) => 'G', chr(196).chr(159) => 'g', chr(196).chr(160) => 'G', chr(196).chr(161) => 'g', chr(196).chr(162) => 'G', chr(196).chr(163) => 'g', chr(196).chr(164) => 'H', chr(196).chr(165) => 'h', chr(196).chr(166) => 'H', chr(196).chr(167) => 'h', chr(196).chr(168) => 'I', chr(196).chr(169) => 'i', chr(196).chr(170) => 'I', chr(196).chr(171) => 'i', chr(196).chr(172) => 'I', chr(196).chr(173) => 'i', chr(196).chr(174) => 'I', chr(196).chr(175) => 'i', chr(196).chr(176) => 'I', chr(196).chr(177) => 'i', chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij', chr(196).chr(180) => 'J', chr(196).chr(181) => 'j', chr(196).chr(182) => 'K', chr(196).chr(183) => 'k', chr(196).chr(184) => 'k', chr(196).chr(185) => 'L', chr(196).chr(186) => 'l', chr(196).chr(187) => 'L', chr(196).chr(188) => 'l', chr(196).chr(189) => 'L', chr(196).chr(190) => 'l', chr(196).chr(191) => 'L', chr(197).chr(128) => 'l', chr(197).chr(129) => 'L', chr(197).chr(130) => 'l', chr(197).chr(131) => 'N', chr(197).chr(132) => 'n', chr(197).chr(133) => 'N', chr(197).chr(134) => 'n', chr(197).chr(135) => 'N', chr(197).chr(136) => 'n', chr(197).chr(137) => 'N', chr(197).chr(138) => 'n', chr(197).chr(139) => 'N', chr(197).chr(140) => 'O', chr(197).chr(141) => 'o', chr(197).chr(142) => 'O', chr(197).chr(143) => 'o', chr(197).chr(144) => 'O', chr(197).chr(145) => 'o', chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe', chr(197).chr(148) => 'R', chr(197).chr(149) => 'r', chr(197).chr(150) => 'R', chr(197).chr(151) => 'r', chr(197).chr(152) => 'R', chr(197).chr(153) => 'r', chr(197).chr(154) => 'S', chr(197).chr(155) => 's', chr(197).chr(156) => 'S', chr(197).chr(157) => 's', chr(197).chr(158) => 'S', chr(197).chr(159) => 's', chr(197).chr(160) => 'S', chr(197).chr(161) => 's', chr(197).chr(162) => 'T', chr(197).chr(163) => 't', chr(197).chr(164) => 'T', chr(197).chr(165) => 't', chr(197).chr(166) => 'T', chr(197).chr(167) => 't', chr(197).chr(168) => 'U', chr(197).chr(169) => 'u', chr(197).chr(170) => 'U', chr(197).chr(171) => 'u', chr(197).chr(172) => 'U', chr(197).chr(173) => 'u', chr(197).chr(174) => 'U', chr(197).chr(175) => 'u', chr(197).chr(176) => 'U', chr(197).chr(177) => 'u', chr(197).chr(178) => 'U', chr(197).chr(179) => 'u', chr(197).chr(180) => 'W', chr(197).chr(181) => 'w', chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y', chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z', chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z', chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z', chr(197).chr(190) => 'z', chr(197).chr(191) => 's', // Euro Sign chr(226).chr(130).chr(172) => 'E', // GBP (Pound) Sign chr(194).chr(163) => '', 'Ä' => 'Ae', 'ä' => 'ae', 'Ü' => 'Ue', 'ü' => 'ue', 'Ö' => 'Oe', 'ö' => 'oe', 'ß' => 'ss', // Norwegian characters 'Å'=>'Aa','Æ'=>'Ae','Ø'=>'O','æ'=>'a','ø'=>'o','å'=>'aa' ); $string = strtr($string, $chars); } else { // Assume ISO-8859-1 if not UTF-8 $chars['in'] = chr(128).chr(131).chr(138).chr(142).chr(154).chr(158) .chr(159).chr(162).chr(165).chr(181).chr(192).chr(193).chr(194) .chr(195).chr(196).chr(197).chr(199).chr(200).chr(201).chr(202) .chr(203).chr(204).chr(205).chr(206).chr(207).chr(209).chr(210) .chr(211).chr(212).chr(213).chr(214).chr(216).chr(217).chr(218) .chr(219).chr(220).chr(221).chr(224).chr(225).chr(226).chr(227) .chr(228).chr(229).chr(231).chr(232).chr(233).chr(234).chr(235) .chr(236).chr(237).chr(238).chr(239).chr(241).chr(242).chr(243) .chr(244).chr(245).chr(246).chr(248).chr(249).chr(250).chr(251) .chr(252).chr(253).chr(255); $chars['out'] = "EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy"; $string = strtr($string, $chars['in'], $chars['out']); $doubleChars['in'] = array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)); $doubleChars['out'] = array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th'); $string = str_replace($doubleChars['in'], $doubleChars['out'], $string); } return $string; } /** * US-ASCII transliterations of Unicode text * Ported Sean M. Burke's Text::Unidecode Perl module (He did all the hard work!) * Warning: you should only pass this well formed UTF-8! * Be aware it works by making a copy of the input string which it appends transliterated * characters to - it uses a PHP output buffer to do this - it means, memory use will increase, * requiring up to the same amount again as the input string * * @see http://search.cpan.org/~sburke/Text-Unidecode-0.04/lib/Text/Unidecode.pm * @param string UTF-8 string to convert * @author * @param string (default = ?) Character use if character unknown * @return string US-ASCII string */ public static function utf8ToAscii($str, $unknown = '?') { static $UTF8_TO_ASCII; if (strlen($str) == 0) { return; } preg_match_all('/.{1}|[^\x00]{1,1}$/us', $str, $ar); $chars = $ar[0]; foreach ($chars as $i => $c) { $ud = 0; if (ord($c{0})>=0 && ord($c{0})<=127) { continue; } // ASCII - next please if (ord($c{0})>=192 && ord($c{0})<=223) { $ord = (ord($c{0})-192)*64 + (ord($c{1})-128); } if (ord($c{0})>=224 && ord($c{0})<=239) { $ord = (ord($c{0})-224)*4096 + (ord($c{1})-128)*64 + (ord($c{2})-128); } if (ord($c{0})>=240 && ord($c{0})<=247) { $ord = (ord($c{0})-240)*262144 + (ord($c{1})-128)*4096 + (ord($c{2})-128)*64 + (ord($c{3})-128); } if (ord($c{0})>=248 && ord($c{0})<=251) { $ord = (ord($c{0})-248)*16777216 + (ord($c{1})-128)*262144 + (ord($c{2})-128)*4096 + (ord($c{3})-128)*64 + (ord($c{4})-128); } if (ord($c{0})>=252 && ord($c{0})<=253) { $ord = (ord($c{0})-252)*1073741824 + (ord($c{1})-128)*16777216 + (ord($c{2})-128)*262144 + (ord($c{3})-128)*4096 + (ord($c{4})-128)*64 + (ord($c{5})-128); } if (ord($c{0})>=254 && ord($c{0})<=255) { $chars{$i} = $unknown; continue; } //error $bank = $ord >> 8; if (!array_key_exists($bank, (array) $UTF8_TO_ASCII)) { $bankfile = __DIR__. '/data/'. sprintf("x%02x",$bank).'.php'; if (file_exists($bankfile)) { include $bankfile; } else { $UTF8_TO_ASCII[$bank] = array(); } } $newchar = $ord & 255; if (array_key_exists($newchar, $UTF8_TO_ASCII[$bank])) { $chars{$i} = $UTF8_TO_ASCII[$bank][$newchar]; } else { $chars{$i} = $unknown; } } return implode('', $chars); } /** * Does not transliterate correctly eastern languages * * @param string $text * @param string $separator * @return string */ public static function urlize($text, $separator = '-') { $text = self::unaccent($text); return self::postProcessText($text, $separator); } /** * Uses transliteration tables to convert any kind of utf8 character * * @param string $text * @param string $separator * @return string $text */ public static function transliterate($text, $separator = '-') { if (preg_match('/[\x80-\xff]/', $text) && self::validUtf8($text)) { $text = self::utf8ToAscii($text); } return self::postProcessText($text, $separator); } /** * Tests a string as to whether it's valid UTF-8 and supported by the * Unicode standard * Note: this function has been modified to simple return true or false * @author * @param string UTF-8 encoded string * @return boolean true if valid * @see http://hsivonen.iki.fi/php-utf8/ */ public static function validUtf8($str) { $mState = 0; // cached expected number of octets after the current octet // until the beginning of the next UTF8 character sequence $mUcs4 = 0; // cached Unicode character $mBytes = 1; // cached expected number of octets in the current sequence $len = strlen($str); for ($i = 0; $i < $len; $i++) { $in = ord($str{$i}); if ($mState == 0) { // When mState is zero we expect either a US-ASCII character or a // multi-octet sequence. if (0 == (0x80 & ($in))) { // US-ASCII, pass straight through. $mBytes = 1; } elseif (0xC0 == (0xE0 & ($in))) { // First octet of 2 octet sequence $mUcs4 = ($in); $mUcs4 = ($mUcs4 & 0x1F) << 6; $mState = 1; $mBytes = 2; } elseif (0xE0 == (0xF0 & ($in))) { // First octet of 3 octet sequence $mUcs4 = ($in); $mUcs4 = ($mUcs4 & 0x0F) << 12; $mState = 2; $mBytes = 3; } elseif (0xF0 == (0xF8 & ($in))) { // First octet of 4 octet sequence $mUcs4 = ($in); $mUcs4 = ($mUcs4 & 0x07) << 18; $mState = 3; $mBytes = 4; } elseif (0xF8 == (0xFC & ($in))) { /* First octet of 5 octet sequence. * * This is illegal because the encoded codepoint must be either * (a) not the shortest form or * (b) outside the Unicode range of 0-0x10FFFF. * Rather than trying to resynchronize, we will carry on until the end * of the sequence and let the later error handling code catch it. */ $mUcs4 = ($in); $mUcs4 = ($mUcs4 & 0x03) << 24; $mState = 4; $mBytes = 5; } elseif (0xFC == (0xFE & ($in))) { // First octet of 6 octet sequence, see comments for 5 octet sequence. $mUcs4 = ($in); $mUcs4 = ($mUcs4 & 1) << 30; $mState = 5; $mBytes = 6; } else { /* Current octet is neither in the US-ASCII range nor a legal first * octet of a multi-octet sequence. */ return false; } } else { // When mState is non-zero, we expect a continuation of the multi-octet // sequence if (0x80 == (0xC0 & ($in))) { // Legal continuation. $shift = ($mState - 1) * 6; $tmp = $in; $tmp = ($tmp & 0x0000003F) << $shift; $mUcs4 |= $tmp; /** * End of the multi-octet sequence. mUcs4 now contains the final * Unicode codepoint to be output */ if (0 == --$mState) { /* * Check for illegal sequences and codepoints. */ // From Unicode 3.1, non-shortest form is illegal if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || ((3 == $mBytes) && ($mUcs4 < 0x0800)) || ((4 == $mBytes) && ($mUcs4 < 0x10000)) || (4 < $mBytes) || // From Unicode 3.2, surrogate characters are illegal (($mUcs4 & 0xFFFFF800) == 0xD800) || // Codepoints outside the Unicode range are illegal ($mUcs4 > 0x10FFFF) ) { return false; } //initialize UTF8 cache $mState = 0; $mUcs4 = 0; $mBytes = 1; } } else { /** *((0xC0 & (*in) != 0x80) && (mState != 0)) * Incomplete multi-octet sequence. */ return false; } } } return true; } /** * Cleans up the text and adds separator * * @param string $text * @param string $separator * @return string */ private static function postProcessText($text, $separator) { if (function_exists('mb_strtolower')) { $text = mb_strtolower($text); } else { $text = strtolower($text); } // Remove all none word characters $text = preg_replace('/\W/', ' ', $text); // More stripping. Replace spaces with dashes $text = strtolower(preg_replace('/[^A-Z^a-z^0-9^\/]+/', $separator, preg_replace('/([a-z\d])([A-Z])/', '\1_\2', preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', preg_replace('/::/', '/', $text))))); return trim($text, $separator); } } array ( 'name' => 'English', 'native' => 'English', 'feature' => 'Feature|Business Need|Ability', 'background' => 'Background', 'scenario' => 'Scenario', 'scenario_outline' => 'Scenario Outline|Scenario Template', 'examples' => 'Examples|Scenarios', 'given' => 'Given', 'when' => 'When', 'then' => 'Then', 'and' => 'And', 'but' => 'But', ), 'ar' => array ( 'name' => 'Arabic', 'native' => 'العربية', 'feature' => 'خاصية', 'background' => 'الخلفية', 'scenario' => 'سيناريو', 'scenario_outline' => 'سيناريو مخطط', 'examples' => 'امثلة', 'given' => 'بفرض', 'when' => 'عندما|متى', 'then' => 'اذاً|ثم', 'and' => 'و', 'but' => 'لكن', ), 'bm' => array ( 'name' => 'Malay', 'native' => 'Bahasa Melayu', 'feature' => 'Fungsi', 'background' => 'Latar Belakang', 'scenario' => 'Senario', 'scenario_outline' => 'Menggariskan Senario ', 'examples' => 'Contoh ', 'given' => 'Bagi', 'when' => 'Apabila', 'then' => 'Kemudian', 'and' => 'Dan', 'but' => 'Tetapi', ), 'bg' => array ( 'name' => 'Bulgarian', 'native' => 'български', 'feature' => 'Функционалност', 'background' => 'Предистория', 'scenario' => 'Сценарий', 'scenario_outline' => 'Рамка на сценарий', 'examples' => 'Примери', 'given' => 'Дадено', 'when' => 'Когато', 'then' => 'То', 'and' => 'И', 'but' => 'Но', ), 'ca' => array ( 'name' => 'Catalan', 'native' => 'català', 'background' => 'Rerefons|Antecedents', 'feature' => 'Característica|Funcionalitat', 'scenario' => 'Escenari', 'scenario_outline' => 'Esquema de l\'escenari', 'examples' => 'Exemples', 'given' => 'Donada|Atesa|Donat|Atès', 'when' => 'Quan', 'then' => 'Aleshores|Cal', 'and' => 'I', 'but' => 'Però', ), 'cy-GB' => array ( 'name' => 'Welsh', 'native' => 'Cymraeg', 'background' => 'Cefndir', 'feature' => 'Arwedd', 'scenario' => 'Scenario', 'scenario_outline' => 'Scenario Amlinellol', 'examples' => 'Enghreifftiau', 'given' => 'Anrhegedig a', 'when' => 'Pryd', 'then' => 'Yna', 'and' => 'A', 'but' => 'Ond', ), 'cs' => array ( 'name' => 'Czech', 'native' => 'Česky', 'feature' => 'Požadavek', 'background' => 'Pozadí|Kontext', 'scenario' => 'Scénář', 'scenario_outline' => 'Náčrt Scénáře|Osnova scénáře', 'examples' => 'Příklady', 'given' => 'Pokud|Za předpokladu', 'when' => 'Když', 'then' => 'Pak', 'and' => 'A také|A', 'but' => 'Ale', ), 'da' => array ( 'name' => 'Danish', 'native' => 'dansk', 'feature' => 'Egenskab', 'background' => 'Baggrund', 'scenario' => 'Scenarie', 'scenario_outline' => 'Abstrakt Scenario', 'examples' => 'Eksempler', 'given' => 'Givet', 'when' => 'Når', 'then' => 'Så', 'and' => 'Og', 'but' => 'Men', ), 'de' => array ( 'name' => 'German', 'native' => 'Deutsch', 'feature' => 'Funktionalität', 'background' => 'Grundlage', 'scenario' => 'Szenario', 'scenario_outline' => 'Szenariogrundriss', 'examples' => 'Beispiele', 'given' => 'Gegeben sei|Angenommen', 'when' => 'Wenn', 'then' => 'Dann', 'and' => 'Und', 'but' => 'Aber', ), 'en-au' => array ( 'name' => 'Australian', 'native' => 'Australian', 'feature' => 'Pretty much', 'background' => 'First off', 'scenario' => 'Awww, look mate', 'scenario_outline' => 'Reckon it\'s like', 'examples' => 'You\'ll wanna', 'given' => 'Y\'know', 'when' => 'It\'s just unbelievable', 'then' => 'But at the end of the day I reckon', 'and' => 'Too right', 'but' => 'Yeah nah', ), 'en-lol' => array ( 'name' => 'LOLCAT', 'native' => 'LOLCAT', 'feature' => 'OH HAI', 'background' => 'B4', 'scenario' => 'MISHUN', 'scenario_outline' => 'MISHUN SRSLY', 'examples' => 'EXAMPLZ', 'given' => 'I CAN HAZ', 'when' => 'WEN', 'then' => 'DEN', 'and' => 'AN', 'but' => 'BUT', ), 'en-pirate' => array ( 'name' => 'Pirate', 'native' => 'Pirate', 'feature' => 'Ahoy matey!', 'background' => 'Yo-ho-ho', 'scenario' => 'Heave to', 'scenario_outline' => 'Shiver me timbers', 'examples' => 'Dead men tell no tales', 'given' => 'Gangway!', 'when' => 'Blimey!', 'then' => 'Let go and haul', 'and' => 'Aye', 'but' => 'Avast!', ), 'en-Scouse' => array ( 'name' => 'Scouse', 'native' => 'Scouse', 'feature' => 'Feature', 'background' => 'Dis is what went down', 'scenario' => 'The thing of it is', 'scenario_outline' => 'Wharrimean is', 'examples' => 'Examples', 'given' => 'Givun|Youse know when youse got', 'when' => 'Wun|Youse know like when', 'then' => 'Dun|Den youse gotta', 'and' => 'An', 'but' => 'Buh', ), 'en-tx' => array ( 'name' => 'Texan', 'native' => 'Texan', 'feature' => 'Feature', 'background' => 'Background', 'scenario' => 'Scenario', 'scenario_outline' => 'All y\'all', 'examples' => 'Examples', 'given' => 'Given y\'all', 'when' => 'When y\'all', 'then' => 'Then y\'all', 'and' => 'And y\'all', 'but' => 'But y\'all', ), 'eo' => array ( 'name' => 'Esperanto', 'native' => 'Esperanto', 'feature' => 'Trajto', 'background' => 'Fono', 'scenario' => 'Scenaro', 'scenario_outline' => 'Konturo de la scenaro', 'examples' => 'Ekzemploj', 'given' => 'Donitaĵo', 'when' => 'Se', 'then' => 'Do', 'and' => 'Kaj', 'but' => 'Sed', ), 'es' => array ( 'name' => 'Spanish', 'native' => 'español', 'background' => 'Antecedentes', 'feature' => 'Característica', 'scenario' => 'Escenario', 'scenario_outline' => 'Esquema del escenario', 'examples' => 'Ejemplos', 'given' => 'Dados|Dadas|Dada|Dado', 'when' => 'Cuando', 'then' => 'Entonces', 'and' => 'Y', 'but' => 'Pero', ), 'et' => array ( 'name' => 'Estonian', 'native' => 'eesti keel', 'feature' => 'Omadus', 'background' => 'Taust', 'scenario' => 'Stsenaarium', 'scenario_outline' => 'Raamstsenaarium', 'examples' => 'Juhtumid', 'given' => 'Eeldades', 'when' => 'Kui', 'then' => 'Siis', 'and' => 'Ja', 'but' => 'Kuid', ), 'fa' => array ( 'name' => 'Persian', 'native' => 'فارسی', 'feature' => 'وِیژگی', 'background' => 'زمینه', 'scenario' => 'سناریو', 'scenario_outline' => 'الگوی سناریو', 'examples' => 'نمونه ها', 'given' => 'با فرض', 'when' => 'هنگامی', 'then' => 'آنگاه', 'and' => 'و', 'but' => 'اما', ), 'fi' => array ( 'name' => 'Finnish', 'native' => 'suomi', 'feature' => 'Ominaisuus', 'background' => 'Tausta', 'scenario' => 'Tapaus', 'scenario_outline' => 'Tapausaihio', 'examples' => 'Tapaukset', 'given' => 'Oletetaan', 'when' => 'Kun', 'then' => 'Niin', 'and' => 'Ja', 'but' => 'Mutta', ), 'fr' => array ( 'name' => 'French', 'native' => 'français', 'feature' => 'Fonctionnalité', 'background' => 'Contexte', 'scenario' => 'Scénario', 'scenario_outline' => 'Plan du scénario|Plan du Scénario', 'examples' => 'Exemples', 'given' => 'Soit|Etant données|Étant données|Étant donnés|Etant donnée|Etant donnés|Étant donnée|Etant donné|Étant donné', 'when' => 'Lorsqu\'<|Lorsque|Quand', 'then' => 'Alors', 'and' => 'Et', 'but' => 'Mais', ), 'he' => array ( 'name' => 'Hebrew', 'native' => 'עברית', 'feature' => 'תכונה', 'background' => 'רקע', 'scenario' => 'תרחיש', 'scenario_outline' => 'תבנית תרחיש', 'examples' => 'דוגמאות', 'given' => 'בהינתן', 'when' => 'כאשר', 'then' => 'אזי|אז', 'and' => 'וגם', 'but' => 'אבל', ), 'hi' => array ( 'name' => 'Hindi', 'native' => 'हिंदी', 'feature' => 'रूप लेख', 'background' => 'पृष्ठभूमि', 'scenario' => 'परिदृश्य', 'scenario_outline' => 'परिदृश्य रूपरेखा', 'examples' => 'उदाहरण', 'given' => 'चूंकि|यदि|अगर', 'when' => 'जब', 'then' => 'तब', 'and' => 'तथा|और', 'but' => 'पर', ), 'hr' => array ( 'name' => 'Croatian', 'native' => 'hrvatski', 'feature' => 'Osobina|Mogućnost|Mogucnost', 'background' => 'Pozadina', 'scenario' => 'Scenarij', 'scenario_outline' => 'Skica|Koncept', 'examples' => 'Primjeri|Scenariji', 'given' => 'Zadani|Zadano|Zadan', 'when' => 'Kada|Kad', 'then' => 'Onda', 'and' => 'I', 'but' => 'Ali', ), 'hu' => array ( 'name' => 'Hungarian', 'native' => 'magyar', 'feature' => 'Jellemző', 'background' => 'Háttér', 'scenario' => 'Forgatókönyv', 'scenario_outline' => 'Forgatókönyv vázlat', 'examples' => 'Példák', 'given' => 'Adott|Amennyiben', 'when' => 'Amikor|Majd|Ha', 'then' => 'Akkor', 'and' => 'És', 'but' => 'De', ), 'id' => array ( 'name' => 'Indonesian', 'native' => 'Bahasa Indonesia', 'feature' => 'Fitur', 'background' => 'Dasar', 'scenario' => 'Skenario', 'scenario_outline' => 'Skenario konsep', 'examples' => 'Contoh', 'given' => 'Dengan', 'when' => 'Ketika', 'then' => 'Maka', 'and' => 'Dan', 'but' => 'Tapi', ), 'is' => array ( 'name' => 'Icelandic', 'native' => 'Íslenska', 'feature' => 'Eiginleiki', 'background' => 'Bakgrunnur', 'scenario' => 'Atburðarás', 'scenario_outline' => 'Lýsing Atburðarásar|Lýsing Dæma', 'examples' => 'Dæmi|Atburðarásir', 'given' => 'Ef', 'when' => 'Þegar', 'then' => 'Þá', 'and' => 'Og', 'but' => 'En', ), 'it' => array ( 'name' => 'Italian', 'native' => 'italiano', 'feature' => 'Funzionalità', 'background' => 'Contesto', 'scenario' => 'Scenario', 'scenario_outline' => 'Schema dello scenario', 'examples' => 'Esempi', 'given' => 'Dato|Data|Dati|Date', 'when' => 'Quando', 'then' => 'Allora', 'and' => 'E', 'but' => 'Ma', ), 'ja' => array ( 'name' => 'Japanese', 'native' => '日本語', 'feature' => 'フィーチャ|機能', 'background' => '背景', 'scenario' => 'シナリオ', 'scenario_outline' => 'シナリオアウトライン|シナリオテンプレート|テンプレ|シナリオテンプレ', 'examples' => '例|サンプル', 'given' => '前提<', 'when' => 'もし<', 'then' => 'ならば<', 'and' => 'かつ<', 'but' => 'しかし<|ただし<|但し<', ), 'ko' => array ( 'name' => 'Korean', 'native' => '한국어', 'background' => '배경', 'feature' => '기능', 'scenario' => '시나리오', 'scenario_outline' => '시나리오 개요', 'examples' => '예', 'given' => '조건<|먼저<', 'when' => '만일<|만약<', 'then' => '그러면<', 'and' => '그리고<', 'but' => '하지만<|단<', ), 'lt' => array ( 'name' => 'Lithuanian', 'native' => 'lietuvių kalba', 'feature' => 'Savybė', 'background' => 'Kontekstas', 'scenario' => 'Scenarijus', 'scenario_outline' => 'Scenarijaus šablonas', 'examples' => 'Pavyzdžiai|Scenarijai|Variantai', 'given' => 'Duota', 'when' => 'Kai', 'then' => 'Tada', 'and' => 'Ir', 'but' => 'Bet', ), 'lu' => array ( 'name' => 'Luxemburgish', 'native' => 'Lëtzebuergesch', 'feature' => 'Funktionalitéit', 'background' => 'Hannergrond', 'scenario' => 'Szenario', 'scenario_outline' => 'Plang vum Szenario', 'examples' => 'Beispiller', 'given' => 'ugeholl', 'when' => 'wann', 'then' => 'dann', 'and' => 'an|a', 'but' => 'awer|mä', ), 'lv' => array ( 'name' => 'Latvian', 'native' => 'latviešu', 'feature' => 'Funkcionalitāte|Fīča', 'background' => 'Konteksts|Situācija', 'scenario' => 'Scenārijs', 'scenario_outline' => 'Scenārijs pēc parauga', 'examples' => 'Piemēri|Paraugs', 'given' => 'Kad', 'when' => 'Ja', 'then' => 'Tad', 'and' => 'Un', 'but' => 'Bet', ), 'nl' => array ( 'name' => 'Dutch', 'native' => 'Nederlands', 'feature' => 'Functionaliteit', 'background' => 'Achtergrond', 'scenario' => 'Scenario', 'scenario_outline' => 'Abstract Scenario', 'examples' => 'Voorbeelden', 'given' => 'Gegeven|Stel', 'when' => 'Als', 'then' => 'Dan', 'and' => 'En', 'but' => 'Maar', ), 'no' => array ( 'name' => 'Norwegian', 'native' => 'norsk', 'feature' => 'Egenskap', 'background' => 'Bakgrunn', 'scenario' => 'Scenario', 'scenario_outline' => 'Scenariomal|Abstrakt Scenario', 'examples' => 'Eksempler', 'given' => 'Gitt', 'when' => 'Når', 'then' => 'Så', 'and' => 'Og', 'but' => 'Men', ), 'pl' => array ( 'name' => 'Polish', 'native' => 'polski', 'feature' => 'Właściwość|Funkcja|Aspekt|Potrzeba biznesowa', 'background' => 'Założenia', 'scenario' => 'Scenariusz', 'scenario_outline' => 'Szablon scenariusza', 'examples' => 'Przykłady', 'given' => 'Mając|Zakładając', 'when' => 'Jeżeli|Jeśli|Kiedy|Gdy', 'then' => 'Wtedy', 'and' => 'Oraz|I', 'but' => 'Ale', ), 'pt' => array ( 'name' => 'Portuguese', 'native' => 'português', 'background' => 'Contexto|Cenário de Fundo|Cenario de Fundo|Fundo', 'feature' => 'Funcionalidade|Característica|Caracteristica', 'scenario' => 'Cenário|Cenario', 'scenario_outline' => 'Esquema do Cenário|Esquema do Cenario|Delineação do Cenário|Delineacao do Cenario', 'examples' => 'Exemplos|Cenários|Cenarios', 'given' => 'Dados|Dadas|Dada|Dado', 'when' => 'Quando', 'then' => 'Então|Entao', 'and' => 'E', 'but' => 'Mas', ), 'ro' => array ( 'name' => 'Romanian', 'native' => 'română', 'background' => 'Context', 'feature' => 'Functionalitate|Funcționalitate|Funcţionalitate', 'scenario' => 'Scenariu', 'scenario_outline' => 'Structura scenariu|Structură scenariu', 'examples' => 'Exemple', 'given' => 'Dat fiind|Date fiind|Dati fiind|Dați fiind|Daţi fiind', 'when' => 'Cand|Când', 'then' => 'Atunci', 'and' => 'Si|Și|Şi', 'but' => 'Dar', ), 'ru' => array ( 'name' => 'Russian', 'native' => 'русский', 'feature' => 'Функция|Функционал|Свойство', 'background' => 'Предыстория|Контекст', 'scenario' => 'Сценарий', 'scenario_outline' => 'Структура сценария', 'examples' => 'Примеры', 'given' => 'Допустим|Пусть|Дано', 'when' => 'Когда|Если', 'then' => 'Тогда|То', 'and' => 'К тому же|Также|И', 'but' => 'Но|А', ), 'sv' => array ( 'name' => 'Swedish', 'native' => 'Svenska', 'feature' => 'Egenskap', 'background' => 'Bakgrund', 'scenario' => 'Scenario', 'scenario_outline' => 'Abstrakt Scenario|Scenariomall', 'examples' => 'Exempel', 'given' => 'Givet', 'when' => 'När', 'then' => 'Så', 'and' => 'Och', 'but' => 'Men', ), 'sk' => array ( 'name' => 'Slovak', 'native' => 'Slovensky', 'feature' => 'Požiadavka|Funkcia|Vlastnosť', 'background' => 'Pozadie', 'scenario' => 'Scenár', 'scenario_outline' => 'Náčrt Scenáru|Náčrt Scenára|Osnova Scenára', 'examples' => 'Príklady', 'given' => 'Pokiaľ|Za predpokladu', 'when' => 'Keď|Ak', 'then' => 'Potom|Tak', 'and' => 'A taktiež|A zároveň|A tiež|A', 'but' => 'Ale', ), 'sr-Latn' => array ( 'name' => 'Serbian (Latin)', 'native' => 'Srpski (Latinica)', 'feature' => 'Funkcionalnost|Mogućnost|Mogucnost|Osobina', 'background' => 'Kontekst|Osnova|Pozadina', 'scenario' => 'Scenario|Primer', 'scenario_outline' => 'Struktura scenarija|Skica|Koncept', 'examples' => 'Primeri|Scenariji', 'given' => 'Zadato|Zadate|Zatati', 'when' => 'Kada|Kad', 'then' => 'Onda', 'and' => 'I', 'but' => 'Ali', ), 'sr-Cyrl' => array ( 'name' => 'Serbian', 'native' => 'Српски', 'feature' => 'Функционалност|Могућност|Особина', 'background' => 'Контекст|Основа|Позадина', 'scenario' => 'Сценарио|Пример', 'scenario_outline' => 'Структура сценарија|Скица|Концепт', 'examples' => 'Примери|Сценарији', 'given' => 'Задато|Задате|Задати', 'when' => 'Када|Кад', 'then' => 'Онда', 'and' => 'И', 'but' => 'Али', ), 'tl' => array ( 'name' => 'Telugu', 'native' => 'తెలుగు', 'feature' => 'గుణము', 'background' => 'నేపథ్యం', 'scenario' => 'సన్నివేశం', 'scenario_outline' => 'కథనం', 'examples' => 'ఉదాహరణలు', 'given' => 'చెప్పబడినది', 'when' => 'ఈ పరిస్థితిలో', 'then' => 'అప్పుడు', 'and' => 'మరియు', 'but' => 'కాని', ), 'tr' => array ( 'name' => 'Turkish', 'native' => 'Türkçe', 'feature' => 'Özellik', 'background' => 'Geçmiş', 'scenario' => 'Senaryo', 'scenario_outline' => 'Senaryo taslağı', 'examples' => 'Örnekler', 'given' => 'Diyelim ki', 'when' => 'Eğer ki', 'then' => 'O zaman', 'and' => 'Ve', 'but' => 'Fakat|Ama', ), 'tt' => array ( 'name' => 'Tatar', 'native' => 'Татарча', 'feature' => 'Мөмкинлек|Үзенчәлеклелек', 'background' => 'Кереш', 'scenario' => 'Сценарий', 'scenario_outline' => 'Сценарийның төзелеше', 'examples' => 'Үрнәкләр|Мисаллар', 'given' => 'Әйтик', 'when' => 'Әгәр', 'then' => 'Нәтиҗәдә', 'and' => 'Һәм|Вә', 'but' => 'Ләкин|Әмма', ), 'uk' => array ( 'name' => 'Ukrainian', 'native' => 'Українська', 'feature' => 'Функціонал', 'background' => 'Передумова', 'scenario' => 'Сценарій', 'scenario_outline' => 'Структура сценарію', 'examples' => 'Приклади', 'given' => 'Нехай|Дано|Припустимо, що|Припустимо', 'when' => 'Якщо|Коли', 'then' => 'Тоді|То', 'and' => 'А також|Та|І', 'but' => 'Але', ), 'uz' => array ( 'name' => 'Uzbek', 'native' => 'Узбекча', 'feature' => 'Функционал', 'background' => 'Тарих', 'scenario' => 'Сценарий', 'scenario_outline' => 'Сценарий структураси', 'examples' => 'Мисоллар', 'given' => 'Агар', 'when' => 'Агар', 'then' => 'Унда', 'and' => 'Ва', 'but' => 'Лекин|Бирок|Аммо', ), 'vi' => array ( 'name' => 'Vietnamese', 'native' => 'Tiếng Việt', 'feature' => 'Tính năng', 'background' => 'Bối cảnh', 'scenario' => 'Tình huống|Kịch bản', 'scenario_outline' => 'Khung tình huống|Khung kịch bản', 'examples' => 'Dữ liệu', 'given' => 'Biết|Cho', 'when' => 'Khi', 'then' => 'Thì', 'and' => 'Và', 'but' => 'Nhưng', ), 'zh-CN' => array ( 'name' => 'Chinese simplified', 'native' => '简体中文', 'feature' => '功能', 'background' => '背景', 'scenario' => '场景|剧本', 'scenario_outline' => '场景大纲|剧本大纲', 'examples' => '例子', 'given' => '假如<|假设<|假定<', 'when' => '当<', 'then' => '那么<', 'and' => '而且<|并且<|同时<', 'but' => '但是<', ), 'zh-TW' => array ( 'name' => 'Chinese traditional', 'native' => '繁體中文', 'feature' => '功能', 'background' => '背景', 'scenario' => '場景|劇本', 'scenario_outline' => '場景大綱|劇本大綱', 'examples' => '例子', 'given' => '假如<|假設<|假定<', 'when' => '當<', 'then' => '那麼<', 'and' => '而且<|並且<|同時<', 'but' => '但是<', ), ); Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Parser cache interface. * * @author Konstantin Kudryashov */ interface CacheInterface { /** * Checks that cache for feature exists and is fresh. * * @param string $path Feature path * @param integer $timestamp The last time feature was updated * * @return Boolean */ public function isFresh($path, $timestamp); /** * Reads feature cache from path. * * @param string $path Feature path * * @return FeatureNode */ public function read($path); /** * Caches feature node. * * @param string $path Feature path * @param FeatureNode $feature Feature instance */ public function write($path, FeatureNode $feature); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * File cache. * Caches feature into a file. * * @author Konstantin Kudryashov */ class FileCache implements CacheInterface { private $path; /** * Initializes file cache. * * @param string $path Path to the folder where to store caches. */ public function __construct($path) { if (!is_dir($path)) { mkdir($path, 0777, true); } $this->path = rtrim($path, '/'); } /** * Checks that cache for feature exists and is fresh. * * @param string $path Feature path * @param integer $timestamp The last time feature was updated * * @return Boolean */ public function isFresh($path, $timestamp) { $cachePath = $this->getCachePathFor($path); if (!file_exists($cachePath)) { return false; } return filemtime($cachePath) > $timestamp; } /** * Reads feature cache from path. * * @param string $path Feature path * * @return FeatureNode */ public function read($path) { return unserialize(file_get_contents($this->getCachePathFor($path))); } /** * Caches feature node. * * @param string $path Feature path * @param FeatureNode $feature Feature instance */ public function write($path, FeatureNode $feature) { file_put_contents($this->getCachePathFor($path), serialize($feature)); } /** * Returns feature cache file path from features path. * * @param string $path Feature path * * @return string */ protected function getCachePathFor($path) { return $this->path.'/'.md5($path).'.feature.cache'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Gherkin Dumper. * * @author Jean-François Lépine */ class GherkinDumper { private $keywords; private $indent; /** * Constructs dumper. * * @param KeywordsInterface $keywords Keywords container * @param string $indent Indentation */ public function __construct(KeywordsInterface $keywords, $indent = ' ') { $this->keywords = $keywords; $this->indent = $indent; } /** * Dumps a feature. * * @see dumpFeature() * * @param FeatureNode $feature Feature instance * * @return string */ public function dump(FeatureNode $feature) { return $this->dumpFeature($feature); } /** * Dumps a background. * * @param BackgroundNode $background Background instance * * @return string */ public function dumpBackground(BackgroundNode $background) { $content = $this->dumpKeyword( $this->keywords->getBackgroundKeywords(), $background->getTitle() ); foreach ($background->getSteps() as $step) { $content .= PHP_EOL . $this->dumpIndent(1) . $this->dumpStep($step); } return $content; } /** * Dumps comment. * * @param string $comment Comment string * * @return string */ public function dumpComment($comment) { return $comment ? '# ' . $comment : ''; } /** * Dumps feature. * * @param FeatureNode $feature Feature instance * * @return string */ public function dumpFeature(FeatureNode $feature) { $language = $feature->getLanguage(); $this->keywords->setLanguage($language); $content = '' . $this->dumpLanguage($language) . ($feature->getTags() ? PHP_EOL . $this->dumpTags($feature->getTags(), 0) : '') . PHP_EOL . $this->dumpKeyword($this->keywords->getFeatureKeywords(), $feature->getTitle(), 0) . PHP_EOL . $this->dumpText($feature->getDescription(), 1); if ($feature->getBackground()) { $content .= $this->dumpBackground($feature->getBackground()); } $scenarios = $feature->getScenarios(); foreach ($scenarios as $scenario) { $content .= PHP_EOL . $this->dumpScenario($scenario); } return $content; } /** * Dumps keyword. * * @param string $keyword Keyword string * @param string $text Text * @param integer $indent Indentation * * @return string */ public function dumpKeyword($keyword, $text, $indent = 0) { $keywords = explode('|', $keyword); $keyword = reset($keywords); return $this->dumpIndent($indent) . $keyword . ':' . ((strlen($text) > 0) ? ' ' . ltrim($this->dumpText($text, $indent + 1)) : '') ; } /** * Dumps scenario. * * @param ScenarioNode $scenario Scenario instance * * @return string */ public function dumpScenario(ScenarioNode $scenario) { $keyWordToUse = $scenario instanceof OutlineNode ? $this->keywords->getOutlineKeywords() : $this->keywords->getScenarioKeywords(); $content = '' . (sizeof($scenario->getTags()) > 0 ? PHP_EOL . $this->dumpTags($scenario->getTags(), 1) : '') . PHP_EOL . $this->dumpKeyword($keyWordToUse, $scenario->getTitle(), 1) ; foreach ($scenario->getSteps() as $step) { $content .= PHP_EOL . $this->dumpIndent(2) . $this->dumpStep($step); } if ($scenario instanceof OutlineNode) { $content .= '' . PHP_EOL . PHP_EOL . $this->dumpKeyword($this->keywords->getExamplesKeywords(), '', 1) ; $examples = $scenario->getExamples(); $content .= $this->dumpTableNode($examples, 2); } return $content; } /** * Dumps table node. * * @param TableNode $tableNode Table node * @param integer $indent Indentation * @return string */ public function dumpTableNode(TableNode $tableNode, $indent = 0) { $len = sizeof($tableNode->getRows()); $content = ''; for ($i = 0; $i < $len; $i++) { $content .= PHP_EOL . $this->dumpIndent($indent) . $tableNode->getRowAsString($i); } return $content; } /** * Dumps indentation. * * @param integer $indent Indentation * * @return string */ public function dumpIndent($indent) { return str_repeat($this->indent, $indent); } /** * Dumps a step. * * @param StepNode $step Step node instance * * @return string * * @throws Exception if invalid step type providen */ public function dumpStep(StepNode $step) { switch ($step->getType()) { case 'Given': $kw = $this->keywords->getGivenKeywords(); break; case 'When': $kw = $this->keywords->getWhenKeywords(); break; case 'Then': $kw = $this->keywords->getThenKeywords(); break; case 'But': $kw = $this->keywords->getButKeywords(); break; case 'And': $kw = $this->keywords->getAndKeywords(); break; default: throw new Exception("invalid type given : " . $step->getType()); } return $this->dumpText($kw . ' ' . $step->getText()); } /** * Dumps text. * * @param string $text Text to dump * @param integer $indent Indentation * * @return string */ public function dumpText($text, $indent = 0) { return $this->dumpIndent($indent) . implode( PHP_EOL . $this->dumpIndent($indent), explode(PHP_EOL, $text) ); } /** * Dumps tags. * * @param array $tags Array of tags * @param integer $indent Indentation * * @return string */ public function dumpTags(array $tags, $indent = 0) { if (empty($tags)) { return ''; } return $this->dumpIndent($indent) . '@' . ltrim(implode(' @', $tags)); } /** * Dumps language tag. * * @param string $language Language name * * @return string */ public function dumpLanguage($language) { return $this->dumpComment($this->dumpKeyword('language', $language)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ class Exception extends \Exception {} * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ class LexerException extends Exception {} * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ class ParserException extends Exception {} * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Filter interface. * * @author Konstantin Kudryashov */ interface FilterInterface { /** * Checks if Feature matches specified filter. * * @param FeatureNode $feature Feature instance * * @return Boolean */ public function isFeatureMatch(FeatureNode $feature); /** * Checks if scenario or outline matches specified filter. * * @param ScenarioNode $scenario Scenario or Outline node instance * * @return Boolean */ public function isScenarioMatch(ScenarioNode $scenario); /** * Filters feature according to the filter. * * @param FeatureNode $feature */ public function filterFeature(FeatureNode $feature); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Filters scenarios by definition line number. * * @author Konstantin Kudryashov */ class LineFilter implements FilterInterface { protected $filterLine; /** * Initializes filter. * * @param string $filterLine Line of the scenario to filter on */ public function __construct($filterLine) { $this->filterLine = intval($filterLine); } /** * Checks if Feature matches specified filter. * * @param FeatureNode $feature Feature instance * * @return Boolean */ public function isFeatureMatch(FeatureNode $feature) { return $this->filterLine === $feature->getLine(); } /** * Checks if scenario or outline matches specified filter. * * @param ScenarioNode $scenario Scenario or Outline node instance * * @return Boolean */ public function isScenarioMatch(ScenarioNode $scenario) { if ($this->filterLine === $scenario->getLine()) { return true; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { return $this->filterLine === $scenario->getLine() || in_array($this->filterLine, $scenario->getExamples()->getRowLines()); } return false; } /** * Filters feature according to the filter. * * @param FeatureNode $feature */ public function filterFeature(FeatureNode $feature) { $scenarios = $feature->getScenarios(); foreach ($scenarios as $i => $scenario) { if (!$this->isScenarioMatch($scenario)) { unset($scenarios[$i]); continue; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { $lines = $scenario->getExamples()->getRowLines(); $rows = $scenario->getExamples()->getNumeratedRows(); if (current($lines) <= $this->filterLine && end($lines) >= $this->filterLine) { $scenario->getExamples()->setRows(array()); $scenario->getExamples()->addRow($rows[$lines[0]], $lines[0]); if ($lines[0] !== $this->filterLine) { $scenario->getExamples()->addRow($rows[$this->filterLine], $this->filterLine); } } } } $feature->setScenarios($scenarios); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Filters scenarios by definition line number range. * * @author Fabian Kiss */ class LineRangeFilter implements FilterInterface { protected $filterMinLine; protected $filterMaxLine; /** * Initializes filter. * * @param string $filterMinLine Minimum line of a scenario to filter on * @param string $filterMaxLine Maximum line of a scenario to filter on */ public function __construct($filterMinLine, $filterMaxLine) { $this->filterMinLine = intval($filterMinLine); if ($filterMaxLine == '*') { $this->filterMaxLine = PHP_INT_MAX; } else { $this->filterMaxLine = intval($filterMaxLine); } } /** * Checks if Feature matches specified filter. * * @param FeatureNode $feature Feature instance * * @return Boolean */ public function isFeatureMatch(FeatureNode $feature) { return $this->filterMinLine <= $feature->getLine() && $this->filterMaxLine >= $feature->getLine() ; } /** * Checks if scenario or outline matches specified filter. * * @param ScenarioNode $scenario Scenario or Outline node instance * * @return Boolean */ public function isScenarioMatch(ScenarioNode $scenario) { if ($this->filterMinLine <= $scenario->getLine() && $this->filterMaxLine >= $scenario->getLine()) { return true; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { foreach ($scenario->getExamples()->getRowLines() as $line) { if ($line >= $this->filterMinLine && $line <= $this->filterMaxLine) { return true; } } } return false; } /** * Filters feature according to the filter. * * @param FeatureNode $feature */ public function filterFeature(FeatureNode $feature) { $scenarios = $feature->getScenarios(); foreach ($scenarios as $i => $scenario) { if (!$this->isScenarioMatch($scenario)) { unset($scenarios[$i]); continue; } if ($scenario instanceof OutlineNode && $scenario->hasExamples()) { $lines = $scenario->getExamples()->getRowLines(); $rows = $scenario->getExamples()->getNumeratedRows(); $scenario->getExamples()->setRows(array()); $scenario->getExamples()->addRow($rows[$lines[0]], $lines[0]); unset($rows[$lines[0]]); foreach ($rows as $line => $row) { if ($this->filterMinLine <= $line && $this->filterMaxLine >= $line) { $scenario->getExamples()->addRow($row, $line); } } } } $feature->setScenarios($scenarios); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Filters scenarios by feature/scenario name. * * @author Konstantin Kudryashov */ class NameFilter extends SimpleFilter { protected $filterString; /** * Initializes filter. * * @param string $filterString Name filter string */ public function __construct($filterString) { $this->filterString = trim($filterString); } /** * Checks if Feature matches specified filter. * * @param FeatureNode $feature Feature instance * * @return Boolean */ public function isFeatureMatch(FeatureNode $feature) { if ('/' === $this->filterString[0]) { return (bool) preg_match($this->filterString, $feature->getTitle()); } return false !== mb_strpos($feature->getTitle(), $this->filterString); } /** * Checks if scenario or outline matches specified filter. * * @param ScenarioNode $scenario Scenario or Outline node instance * * @return Boolean */ public function isScenarioMatch(ScenarioNode $scenario) { if ('/' === $this->filterString[0] && 1 === preg_match($this->filterString, $scenario->getTitle())) { return true; } elseif (false !== mb_strpos($scenario->getTitle(), $this->filterString)) { return true; } if (null !== $scenario->getFeature()) { return $this->isFeatureMatch($scenario->getFeature()); } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract filter class. * * @author Konstantin Kudryashov */ abstract class SimpleFilter implements FilterInterface { /** * Filters feature according to the filter. * * @param FeatureNode $feature */ public function filterFeature(FeatureNode $feature) { $scenarios = $feature->getScenarios(); foreach ($scenarios as $i => $scenario) { if (!$this->isScenarioMatch($scenario)) { unset($scenarios[$i]); } } $feature->setScenarios($scenarios); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Filters scenarios by feature/scenario tag. * * @author Konstantin Kudryashov */ class TagFilter extends SimpleFilter { protected $filterString; /** * Initializes filter. * * @param string $filterString Name filter string */ public function __construct($filterString) { $this->filterString = trim($filterString); } /** * Checks if Feature matches specified filter. * * @param FeatureNode $feature Feature instance * * @return Boolean */ public function isFeatureMatch(FeatureNode $feature) { return $this->matchesCondition($feature); } /** * Checks if scenario or outline matches specified filter. * * @param ScenarioNode $scenario Scenario or Outline node instance * * @return Boolean */ public function isScenarioMatch(ScenarioNode $scenario) { return $this->matchesCondition($scenario); } /** * Checks that node matches condition. * * @param AbstractNode $node Node to check * * @return Boolean */ protected function matchesCondition(AbstractNode $node) { $satisfies = true; foreach (explode('&&', $this->filterString) as $andTags) { $satisfiesComma = false; foreach (explode(',', $andTags) as $tag) { $tag = str_replace('@', '', trim($tag)); if ('~' === $tag[0]) { $tag = mb_substr($tag, 1); $satisfiesComma = !$node->hasTag($tag) || $satisfiesComma; } else { $satisfiesComma = $node->hasTag($tag) || $satisfiesComma; } } $satisfies = (false !== $satisfiesComma && $satisfies && $satisfiesComma) || false; } return $satisfies; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Gherkin manager. * * @author Konstantin Kudryashov */ class Gherkin { protected $freeze = true; protected $loaders = array(); protected $filters = array(); /** * Either to freeze features after loading or not. * * @param Boolean $freeze To freeze? */ public function setFreeze($freeze = true) { $this->freeze = (bool) $freeze; } /** * Adds loader to manager. * * @param LoaderInterface $loader Feature loader */ public function addLoader(LoaderInterface $loader) { $this->loaders[] = $loader; } /** * Adds filter to manager. * * @param FilterInterface $filter Feature/Scenario filter */ public function addFilter(FilterInterface $filter) { $this->filters[] = $filter; } /** * Sets base features path. * * @param string $path Loaders base path */ public function setBasePath($path) { foreach ($this->loaders as $loader) { $loader->setBasePath($path); } } /** * Loads & filters resource with added loaders. * * @param mixed $resource Resource to load * @param array $filters Additional filters * * @return array * * @throws \InvalidArgumentException */ public function load($resource, array $filters = array()) { $filters = array_merge($this->filters, $filters); $matches = array(); if (preg_match('/^(.*)\:(\d+)-(\d+|\*)$/', $resource, $matches)) { $resource = $matches[1]; $filters[] = new LineRangeFilter($matches[2], $matches[3]); } elseif (preg_match('/^(.*)\:(\d+)$/', $resource, $matches)) { $resource = $matches[1]; $filters[] = new LineFilter($matches[2]); } $loader = $this->resolveLoader($resource); if (null === $loader) { if ($resource) { $message = sprintf('Can\'t find applicable feature loader for: "%s"', $resource); } else { $message = sprintf('Can\'t find applicable feature loader'); } throw new \InvalidArgumentException( $message."\n". 'Maybe you\'ve forgot to create `features/` folder?' ); } $features = $loader->load($resource); foreach ($features as $i => $feature) { foreach ($filters as $filter) { $filter->filterFeature($feature); if (!$feature->hasScenarios() && !$filter->isFeatureMatch($feature)) { unset($features[$i]); continue; } } if ($this->freeze) { $feature->freeze(); } } return array_values($features); } /** * Resolves loader by resource. * * @param mixed $resoruce Resource to load * * @return LoaderInterface */ public function resolveLoader($resource) { foreach ($this->loaders as $loader) { if ($loader->supports($resource)) { return $loader; } } return null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Array initializable keywords holder. * * $keywords = new Behat\Gherkin\Keywords\ArrayKeywords(array( * 'en' => array( * 'feature' => 'Feature', * 'background' => 'Background', * 'scenario' => 'Scenario', * 'scenario_outline' => 'Scenario Outline|Scenario Template', * 'examples' => 'Examples|Scenarios', * 'given' => 'Given', * 'when' => 'When', * 'then' => 'Then', * 'and' => 'And', * 'but' => 'But' * ), * 'ru' => array( * 'feature' => 'Функционал', * 'background' => 'Предыстория', * 'scenario' => 'Сценарий', * 'scenario_outline' => 'Структура сценария', * 'examples' => 'Значения', * 'given' => 'Допустим', * 'when' => 'Если', * 'then' => 'То', * 'and' => 'И', * 'but' => 'Но' * ) * )); * * @author Konstantin Kudryashov */ class ArrayKeywords implements KeywordsInterface { private $keywords = array(); private $language; /** * Initializes holder with keywords. * * @param array $keywords Keywords array */ public function __construct(array $keywords) { $this->keywords = $keywords; } /** * Sets keywords holder language. * * @param string $language Language name */ public function setLanguage($language) { if (!isset($this->keywords[$language])) { $this->language = 'en'; } else { $this->language = $language; } } /** * Returns Feature keywords (splitted by "|"). * * @return string */ public function getFeatureKeywords() { return $this->keywords[$this->language]['feature']; } /** * Returns Background keywords (splitted by "|"). * * @return string */ public function getBackgroundKeywords() { return $this->keywords[$this->language]['background']; } /** * Returns Scenario keywords (splitted by "|"). * * @return string */ public function getScenarioKeywords() { return $this->keywords[$this->language]['scenario']; } /** * Returns Scenario Outline keywords (splitted by "|"). * * @return string */ public function getOutlineKeywords() { return $this->keywords[$this->language]['scenario_outline']; } /** * Returns Examples keywords (splitted by "|"). * * @return string */ public function getExamplesKeywords() { return $this->keywords[$this->language]['examples']; } /** * Returns Given keywords (splitted by "|"). * * @return string */ public function getGivenKeywords() { return $this->keywords[$this->language]['given']; } /** * Returns When keywords (splitted by "|"). * * @return string */ public function getWhenKeywords() { return $this->keywords[$this->language]['when']; } /** * Returns Then keywords (splitted by "|"). * * @return string */ public function getThenKeywords() { return $this->keywords[$this->language]['then']; } /** * Returns And keywords (splitted by "|"). * * @return string */ public function getAndKeywords() { return $this->keywords[$this->language]['and']; } /** * Returns But keywords (splitted by "|"). * * @return string */ public function getButKeywords() { return $this->keywords[$this->language]['but']; } /** * Returns all step keywords (Given, When, Then, And, But). * * @return string */ public function getStepKeywords() { return implode('|', array( $this->getGivenKeywords(), $this->getWhenKeywords(), $this->getThenKeywords(), $this->getAndKeywords(), $this->getButKeywords() )); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * File initializable keywords holder. * * $keywords = new Behat\Gherkin\Keywords\CachedArrayKeywords($file); * * @author Konstantin Kudryashov */ class CachedArrayKeywords extends ArrayKeywords { /** * Initializes holder with file. * * @param string $file Cached array path */ public function __construct($file) { parent::__construct(include($file)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Cucumber-translations reader. * * $keywords = new Behat\Gherkin\Keywords\CucumberKeywords($i18nYmlPath); * * @author Konstantin Kudryashov */ class CucumberKeywords extends ArrayKeywords { /** * Initializes holder with yaml string OR file. * * @param string $yaml Yaml string */ public function __construct($yaml) { parent::__construct(Yaml::parse($yaml)); } /** * Returns Feature keywords (splitted by "|"). * * @return string */ public function getGivenKeywords() { return $this->prepareStepString(parent::getGivenKeywords()); } /** * Returns When keywords (splitted by "|"). * * @return string */ public function getWhenKeywords() { return $this->prepareStepString(parent::getWhenKeywords()); } /** * Returns Then keywords (splitted by "|"). * * @return string */ public function getThenKeywords() { return $this->prepareStepString(parent::getThenKeywords()); } /** * Returns And keywords (splitted by "|"). * * @return string */ public function getAndKeywords() { return $this->prepareStepString(parent::getAndKeywords()); } /** * Returns But keywords (splitted by "|"). * * @return string */ public function getButKeywords() { return $this->prepareStepString(parent::getButKeywords()); } /** * Trim *| from the begining of the list. * * @param string $keywordsString Keywords string * * @return string */ private function prepareStepString($keywordsString) { if (0 === mb_strpos($keywordsString, '*|')) { $keywordsString = mb_substr($keywordsString, 2); } return $keywordsString; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Gherkin keywords dumper. * * @author Konstantin Kudryashov */ class KeywordsDumper { private $keywords; private $keywordsDumper; /** * Initializes dumper. * * @param KeywordsInterface $keywords Keywords instance */ public function __construct(KeywordsInterface $keywords) { $this->keywords = $keywords; $this->keywordsDumper = array($this, 'dumpKeywords'); } /** * Sets keywords mapper function. * * Callable should accept 2 arguments (array $keywords and Boolean $isShort) * * @param callable $mapper Mapper function */ public function setKeywordsDumperFunction($mapper) { $this->keywordsDumper = $mapper; } /** * Defaults keywords dumper. * * @param array $keywords Keywords list * @param Boolean $isShort Is short version * * @return string */ public function dumpKeywords(array $keywords, $isShort) { if ($isShort) { return 1 < count($keywords) ? '('.implode('|', $keywords).')' : $keywords[0]; } return $keywords[0]; } /** * Dumps keyworded feature into string. * * @param string $language Keywords language * @param Boolean $short Dump short version * * @return string|array String for short version and array of features for extended */ public function dump($language, $short = true) { $this->keywords->setLanguage($language); $languageComment = ''; if ('en' !== $language) { $languageComment = "# language: $language\n"; } $keywords = explode('|', $this->keywords->getFeatureKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); return trim($languageComment.$this->dumpFeature($keywords, $short)); } $features = array(); foreach ($keywords as $keyword) { $keyword = call_user_func($this->keywordsDumper, array($keyword), $short); $features[] = trim($languageComment.$this->dumpFeature($keyword, $short)); } return $features; } /** * Dumps feature example. * * @param string $keyword Item keyword * @param Boolean $short Dump short version? * * @return string */ protected function dumpFeature($keyword, $short = true) { $dump = <<keywords->getBackgroundKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); $dump .= $this->dumpBackground($keywords, $short); } else { $keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short); $dump .= $this->dumpBackground($keyword, $short); } // Scenario $keywords = explode('|', $this->keywords->getScenarioKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); $dump .= $this->dumpScenario($keywords, $short); } else { foreach ($keywords as $keyword) { $keyword = call_user_func($this->keywordsDumper, array($keyword), $short); $dump .= $this->dumpScenario($keyword, $short); } } // Outline $keywords = explode('|', $this->keywords->getOutlineKeywords()); if ($short) { $keywords = call_user_func($this->keywordsDumper, $keywords, $short); $dump .= $this->dumpOutline($keywords, $short); } else { foreach ($keywords as $keyword) { $keyword = call_user_func($this->keywordsDumper, array($keyword), $short); $dump .= $this->dumpOutline($keyword, $short); } } return $dump; } /** * Dumps background example. * * @param string $keyword Item keyword * @param Boolean $short Dump short version? * * @return string */ protected function dumpBackground($keyword, $short = true) { $dump = <<dumpStep( $this->keywords->getGivenKeywords(), 'there is agent A', $short ); // And $dump .= $this->dumpStep( $this->keywords->getAndKeywords(), 'there is agent B', $short ); return $dump."\n"; } /** * Dumps scenario example. * * @param string $keyword Item keyword * @param Boolean $short Dump short version? * * @return string */ protected function dumpScenario($keyword, $short = true) { $dump = <<dumpStep( $this->keywords->getGivenKeywords(), 'there is agent J', $short ); // And $dump .= $this->dumpStep( $this->keywords->getAndKeywords(), 'there is agent K', $short ); // When $dump .= $this->dumpStep( $this->keywords->getWhenKeywords(), 'I erase agent K\'s memory', $short ); // Then $dump .= $this->dumpStep( $this->keywords->getThenKeywords(), 'there should be agent J', $short ); // But $dump .= $this->dumpStep( $this->keywords->getButKeywords(), 'there should not be agent K', $short ); return $dump."\n"; } /** * Dumps outline example. * * @param string $keyword Item keyword * @param Boolean $short Dump short version? * * @return string */ protected function dumpOutline($keyword, $short = true) { $dump = <<dumpStep( $this->keywords->getGivenKeywords(), 'there is agent ', $short ); // And $dump .= $this->dumpStep( $this->keywords->getAndKeywords(), 'there is agent ', $short ); // When $dump .= $this->dumpStep( $this->keywords->getWhenKeywords(), 'I erase agent \'s memory', $short ); // Then $dump .= $this->dumpStep( $this->keywords->getThenKeywords(), 'there should be agent ', $short ); // But $dump .= $this->dumpStep( $this->keywords->getButKeywords(), 'there should not be agent ', $short ); $keywords = explode('|', $this->keywords->getExamplesKeywords()); if ($short) { $keyword = call_user_func($this->keywordsDumper, $keywords, $short); } else { $keyword = call_user_func($this->keywordsDumper, array($keywords[0]), $short); } $dump .= <<keywordsDumper, $keywords, $short); $dump .= <<keywordsDumper, array($keyword), $short); $dump .= << * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Keywords holder interface. * * @author Konstantin Kudryashov */ interface KeywordsInterface { /** * Sets keywords holder language. * * @param string $language Language name */ public function setLanguage($language); /** * Returns Feature keywords (splitted by "|"). * * @return string */ public function getFeatureKeywords(); /** * Returns Background keywords (splitted by "|"). * * @return string */ public function getBackgroundKeywords(); /** * Returns Scenario keywords (splitted by "|"). * * @return string */ public function getScenarioKeywords(); /** * Returns Scenario Outline keywords (splitted by "|"). * * @return string */ public function getOutlineKeywords(); /** * Returns Examples keywords (splitted by "|"). * * @return string */ public function getExamplesKeywords(); /** * Returns Given keywords (splitted by "|"). * * @return string */ public function getGivenKeywords(); /** * Returns When keywords (splitted by "|"). * * @return string */ public function getWhenKeywords(); /** * Returns Then keywords (splitted by "|"). * * @return string */ public function getThenKeywords(); /** * Returns And keywords (splitted by "|"). * * @return string */ public function getAndKeywords(); /** * Returns But keywords (splitted by "|"). * * @return string */ public function getButKeywords(); /** * Returns all step keywords (splitted by "|"). * * @return string */ public function getStepKeywords(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Symfony Translation Component's keywords holder. * * $translator = new Symfony\Component\Translation\Translator('en', new Symfony\Component\Translation\MessageSelector()); * $translator->addLoader(...); * $translator->addResource(...); * ... * $translator->addResource(...); * * $keywords = new Behat\Gherkin\Keywords\SymfonyTranslationKeywords($translator); * * @author Konstantin Kudryashov */ class SymfonyTranslationKeywords implements KeywordsInterface { private $translator; private $locale = 'en'; /** * Initializes keywords holder. * * @param Translator $translator Translator instance */ public function __construct(Translator $translator) { $this->translator = $translator; } /** * Sets keywords holder language. * * @param string $language Language name */ public function setLanguage($language) { $this->locale = $language; } /** * Returns Feature keywords (splitted by "|"). * * @return string */ public function getFeatureKeywords() { return $this->translator->trans('Feature', array(), 'gherkin', $this->locale); } /** * Returns Background keywords (splitted by "|"). * * @return string */ public function getBackgroundKeywords() { return $this->translator->trans('Background', array(), 'gherkin', $this->locale); } /** * Returns Scenario keywords (splitted by "|"). * * @return string */ public function getScenarioKeywords() { return $this->translator->trans('Scenario', array(), 'gherkin', $this->locale); } /** * Returns Scenario Outline keywords (splitted by "|"). * * @return string */ public function getOutlineKeywords() { return $this->translator->trans('Scenario Outline', array(), 'gherkin', $this->locale); } /** * Returns Examples keywords (splitted by "|"). * * @return string */ public function getExamplesKeywords() { return $this->translator->trans('Examples', array(), 'gherkin', $this->locale); } /** * Returns Given keywords (splitted by "|"). * * @return string */ public function getGivenKeywords() { } /** * Returns When keywords (splitted by "|"). * * @return string */ public function getWhenKeywords() { } /** * Returns Then keywords (splitted by "|"). * * @return string */ public function getThenKeywords() { } /** * Returns And keywords (splitted by "|"). * * @return string */ public function getAndKeywords() { } /** * Returns But keywords (splitted by "|"). * * @return string */ public function getButKeywords() { } /** * Returns all step keywords (splitted by "|"). * * @return string */ public function getStepKeywords() { return $this->translator->trans('Given|When|Then|And|But', array(), 'gherkin', $this->locale); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Gherkin lexer. * * @author Konstantin Kudryashov */ class Lexer { private $lines; private $line; private $lineNumber; private $eos; private $keywords; private $keywordsCache = array(); private $deferredObjects = array(); private $stash = array(); private $inPyString = false; private $pyStringSwallow = 0; private $featureStarted = false; private $allowMultilineArguments = false; private $allowSteps = false; /** * Initializes lexer. * * @param KeywordsInterface $keywords Keywords holder */ public function __construct(KeywordsInterface $keywords) { $this->keywords = $keywords; } /** * Sets lexer input. * * @param string $input Input string */ public function setInput($input) { // try to detect unsupported encoding if ('UTF-8' !== mb_detect_encoding($input, 'UTF-8', true)) { throw new LexerException('Feature file is not in UTF8 encoding'); } $input = strtr($input, array("\r\n" => "\n", "\r" => "\n")); $this->lines = explode("\n", $input); $this->line = array_shift($this->lines); $this->lineNumber = 1; $this->eos = false; $this->deferredObjects = array(); $this->stash = array(); $this->inPyString = false; $this->pyStringSwallow = 0; $this->featureStarted = false; $this->allowMultilineArguments = false; $this->allowSteps = false; } /** * Sets keywords language. * * @param string $language Language name */ public function setLanguage($language) { $this->keywords->setLanguage($language); $this->keywordsCache = array(); } /** * Returns next token or previously stashed one. * * @return stdClass */ public function getAdvancedToken() { return $this->getStashedToken() ?: $this->getNextToken(); } /** * Defers token. * * @param stdClass $token Token to defer */ public function deferToken(\stdClass $token) { $token->defered = true; $this->deferredObjects[] = $token; } /** * Predicts for number of tokens. * * @param integer $number Number of tokens to predict * * @return stdClass */ public function predictToken($number = 1) { $fetch = $number - count($this->stash); while ($fetch-- > 0) { $this->stash[] = $this->getNextToken(); } return $this->stash[--$number]; } /** * Constructs token with specified parameters. * * @param string $type Token type * @param string $value Token value * * @return stdClass */ public function takeToken($type, $value = null) { return (Object) array( 'type' => $type, 'line' => $this->lineNumber, 'value' => $value ?: null, 'defered' => false ); } /** * Consumes line from input & increments line counter. */ protected function consumeLine() { ++$this->lineNumber; if (!count($this->lines)) { $this->eos = true; return false; } $this->line = array_shift($this->lines); } /** * Returns stashed token or false if hasn't. * * @return stdClass|Boolean */ protected function getStashedToken() { return count($this->stash) ? array_shift($this->stash) : null; } /** * Returns deferred token or false if hasn't. * * @return stdClass|Boolean */ protected function getDeferredToken() { return count($this->deferredObjects) ? array_shift($this->deferredObjects) : null; } /** * Returns next token from input. * * @return stdClass */ protected function getNextToken() { return $this->getDeferredToken() ?: $this->scanEOS() ?: $this->scanLanguage() ?: $this->scanComment() ?: $this->scanPyStringOperator() ?: $this->scanPyStringContent() ?: $this->scanStep() ?: $this->scanScenario() ?: $this->scanBackground() ?: $this->scanOutline() ?: $this->scanExamples() ?: $this->scanFeature() ?: $this->scanTags() ?: $this->scanTableRow() ?: $this->scanNewline() ?: $this->scanText(); } /** * Scans for token with specified regex. * * @param string $regex Regular expression * @param string $type Expected token type * * @return stdClass|null */ protected function scanInput($regex, $type) { $matches = array(); if (preg_match($regex, $this->line, $matches)) { $token = $this->takeToken($type, $matches[1]); $this->consumeLine(); return $token; } } /** * Scans for token with specified keywords. * * @param string $keywords Keywords (splitted with |) * @param string $type Expected token type * * @return stdClass|null */ protected function scanInputForKeywords($keywords, $type) { $matches = array(); if (preg_match('/^(\s*)('.$keywords.'):\s*(.*)/u', $this->line, $matches)) { $token = $this->takeToken($type, $matches[3]); $token->keyword = $matches[2]; $token->indent = mb_strlen($matches[1]); $this->consumeLine(); // turn off language searching if ('Feature' === $type) { $this->featureStarted = true; } // turn off PyString and Table searching if (in_array($type, array('Feature', 'Scenario', 'Outline'))) { $this->allowMultilineArguments = false; } elseif ('Examples' === $type) { $this->allowMultilineArguments = true; } // turn on steps searching if (in_array($type, array('Scenario', 'Background', 'Outline'))) { $this->allowSteps = true; } return $token; } } /** * Scans EOS from input & returns it if found. * * @return stdClass|null */ protected function scanEOS() { if (!$this->eos) { return; } return $this->takeToken('EOS'); } /** * Returns keywords for provided type. * * @param string $type Keyword type * * @return string */ protected function getKeywords($type) { if (!isset($this->keywordsCache[$type])) { $getter = 'get' . $type . 'Keywords'; $keywords = $this->keywords->$getter(); if ('Step' === $type) { $paded = array(); foreach (explode('|', $keywords) as $keyword) { $paded[] = false !== mb_strpos($keyword, '<') ? mb_substr($keyword, 0, -1).'\s*' : $keyword.'\s+'; } $keywords = implode('|', $paded); } $this->keywordsCache[$type] = $keywords; } return $this->keywordsCache[$type]; } /** * Scans Feature from input & returns it if found. * * @return stdClass|null */ protected function scanFeature() { return $this->scanInputForKeywords($this->getKeywords('Feature'), 'Feature'); } /** * Scans Background from input & returns it if found. * * @return stdClass|null */ protected function scanBackground() { return $this->scanInputForKeywords($this->getKeywords('Background'), 'Background'); } /** * Scans Scenario from input & returns it if found. * * @return stdClass|null */ protected function scanScenario() { return $this->scanInputForKeywords($this->getKeywords('Scenario'), 'Scenario'); } /** * Scans Scenario Outline from input & returns it if found. * * @return stdClass|null */ protected function scanOutline() { return $this->scanInputForKeywords($this->getKeywords('Outline'), 'Outline'); } /** * Scans Scenario Outline Examples from input & returns it if found. * * @return stdClass|null */ protected function scanExamples() { return $this->scanInputForKeywords($this->getKeywords('Examples'), 'Examples'); } /** * Scans Step from input & returns it if found. * * @return stdClass|null */ protected function scanStep() { if (!$this->allowSteps) { return; } $matches = array(); $keywords = $this->getKeywords('Step'); if (preg_match('/^\s*('.$keywords.')([^\s].+)/u', $this->line, $matches)) { $token = $this->takeToken('Step', trim($matches[1])); $token->text = $matches[2]; $this->consumeLine(); $this->allowMultilineArguments = true; return $token; } } /** * Scans PyString from input & returns it if found. * * @return stdClass|null */ protected function scanPyStringOperator() { if (!$this->allowMultilineArguments) { return; } $matches = array(); if (false !== ($pos = mb_strpos($this->line, '"""'))) { $this->inPyString =! $this->inPyString; $token = $this->takeToken('PyStringOperator'); $this->pyStringSwallow = $pos; $this->consumeLine(); return $token; } } /** * Scans PyString content. * * @return stdClass|null */ protected function scanPyStringContent() { if ($this->inPyString) { $token = $this->scanText(); // swallow trailing spaces $token->value = preg_replace('/^\s{0,'.$this->pyStringSwallow.'}/', '', $token->value); return $token; } } /** * Scans Table Row from input & returns it if found. * * @return stdClass|null */ protected function scanTableRow() { if (!$this->allowMultilineArguments) { return; } $line = trim($this->line); if (isset($line[0]) && '|' === $line[0]) { $token = $this->takeToken('TableRow'); $line = mb_substr($line, 1, mb_strlen($line) - 2); $columns = array_map(function($column) { return trim(str_replace('\\|', '|', $column)); }, preg_split('/(?columns = $columns; $this->consumeLine(); return $token; } } /** * Scans Tags from input & returns it if found. * * @return stdClass|null */ protected function scanTags() { $line = trim($this->line); if (isset($line[0]) && '@' === $line[0]) { $token = $this->takeToken('Tag'); $tags = explode('@', mb_substr($line, 1)); $tags = array_map('trim', $tags); $token->tags = $tags; $this->consumeLine(); return $token; } } /** * Scans Language specifier from input & returns it if found. * * @return stdClass|null */ protected function scanLanguage() { if ($this->featureStarted) { return; } if (!$this->inPyString) { if (0 === mb_strpos(ltrim($this->line), '#') && false !== mb_strpos($this->line, 'language')) { return $this->scanInput('/^\s*\#\s*language:\s*([\w_\-]+)\s*$/', 'Language'); } } } /** * Scans Comment from input & returns it if found. * * @return stdClass|null */ protected function scanComment() { if (!$this->inPyString) { if (0 === mb_strpos(ltrim($this->line), '#')) { $token = $this->takeToken('Comment', $this->line); $this->consumeLine(); return $token; } } } /** * Scans Newline from input & returns it if found. * * @return stdClass|null */ protected function scanNewline() { if ('' === trim($this->line)) { $token = $this->takeToken('Newline', mb_strlen($this->line)); $this->consumeLine(); return $token; } } /** * Scans text from input & returns it if found. * * @return stdClass|null */ protected function scanText() { $token = $this->takeToken('Text', $this->line); $this->consumeLine(); return $token; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract filesystem loader. * * @author Konstantin Kudryashov */ abstract class AbstractFileLoader implements FileLoaderInterface { protected $basePath; /** * Sets base features path. * * @param string $path Base loader path */ public function setBasePath($path) { $this->basePath = realpath($path); } /** * Finds relative path for provided absolute (relative to base features path). * * @param string $path Absolute path * * @return string */ protected function findRelativePath($path) { if (null !== $this->basePath) { return strtr($path, array($this->basePath . DIRECTORY_SEPARATOR => '')); } return $path; } /** * Finds absolute path for provided relative (relative to base features path). * * @param string $path Relative path * * @return string */ protected function findAbsolutePath($path) { if (is_file($path) || is_dir($path)) { return realpath($path); } if (null === $this->basePath) { return false; } if (is_file($this->basePath . DIRECTORY_SEPARATOR . $path) || is_dir($this->basePath . DIRECTORY_SEPARATOR . $path)) { return realpath($this->basePath . DIRECTORY_SEPARATOR . $path); } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * From-array loader. * * @author Konstantin Kudryashov */ class ArrayLoader implements LoaderInterface { /** * Checks if current loader supports provided resource. * * @param mixed $resource Resource to load * * @return Boolean */ public function supports($resource) { return is_array($resource) && (isset($resource['features']) || isset($resource['feature'])); } /** * Loads features from provided resource. * * @param mixed $resource Resource to load * * @return array */ public function load($resource) { $features = array(); if (isset($resource['features'])) { foreach ($resource['features'] as $iterator => $hash) { $feature = $this->loadFeatureHash($hash, $iterator); $features[] = $feature; } } elseif (isset($resource['feature'])) { $feature = $this->loadFeatureHash($resource['feature'], 0); $features[] = $feature; } return $features; } /** * Loads feature from provided feature hash. * * @param array $hash Feature hash * @param integer $line Feature definition line * * @return FeatureNode */ protected function loadFeatureHash(array $hash, $line = 0) { $feature = new Node\FeatureNode(null, null, null, isset($hash['line']) ? $hash['line'] : $line); $feature->setKeyword(isset($hash['keyword']) ? $hash['keyword'] : 'Feature'); if (isset($hash['title'])) { $feature->setTitle($hash['title']); } if (isset($hash['description'])) { $feature->setDescription($hash['description']); } if (isset($hash['tags'])) { $feature->setTags($hash['tags']); } if (isset($hash['language'])) { $feature->setLanguage($hash['language']); } if (isset($hash['background'])) { $feature->setBackground($this->loadBackgroundHash($hash['background'])); } if (isset($hash['scenarios'])) { foreach ($hash['scenarios'] as $scenarioIterator => $scenarioHash) { if (isset($scenarioHash['type']) && 'outline' === $scenarioHash['type']) { $feature->addScenario($this->loadOutlineHash($scenarioHash, $scenarioIterator)); } else { $feature->addScenario($this->loadScenarioHash($scenarioHash, $scenarioIterator)); } } } return $feature; } /** * Loads background from provided hash. * * @param array $hash Background hash * * @return BackgroundNode */ protected function loadBackgroundHash(array $hash) { $background = new Node\BackgroundNode(null, isset($hash['line']) ? $hash['line'] : 0); $background->setKeyword(isset($hash['keyword']) ? $hash['keyword'] : 'Background'); if (isset($hash['title'])) { $background->setTitle($hash['title']); } if (isset($hash['steps'])) { foreach ($hash['steps'] as $stepIterator => $stepHash) { $background->addStep($this->loadStepHash($stepHash, $stepIterator)); } } return $background; } /** * Loads scenario from provided scenario hash. * * @param array $hash Scenario hash * @param integer $line Scenario definition line * * @return ScenarioNode */ protected function loadScenarioHash(array $hash, $line = 0) { $scenario = new Node\ScenarioNode(null, isset($hash['line']) ? $hash['line'] : $line); $scenario->setKeyword(isset($hash['keyword']) ? $hash['keyword'] : 'Scenario'); if (isset($hash['title'])) { $scenario->setTitle($hash['title']); } if (isset($hash['tags'])) { $scenario->setTags($hash['tags']); } if (isset($hash['steps'])) { foreach ($hash['steps'] as $stepIterator => $stepHash) { $scenario->addStep($this->loadStepHash($stepHash, $stepIterator)); } } return $scenario; } /** * Loads outline from provided outline hash. * * @param array $hash Outline hash * @param integer $line Outline definition line * * @return OutlineNode */ protected function loadOutlineHash(array $hash, $line = 0) { $outline = new Node\OutlineNode(null, isset($hash['line']) ? $hash['line'] : $line); $outline->setKeyword(isset($hash['keyword']) ? $hash['keyword'] : 'Scenario Outline'); if (isset($hash['title'])) { $outline->setTitle($hash['title']); } if (isset($hash['tags'])) { $outline->setTags($hash['tags']); } if (isset($hash['examples'])) { if (isset($hash['examples']['keyword'])) { $keyword = $hash['examples']['keyword']; unset($hash['examples']['keyword']); } else { $keyword = 'Examples'; } $table = $this->loadTableHash($hash['examples']); $table->setKeyword($keyword); $outline->setExamples($table); } if (isset($hash['steps'])) { foreach ($hash['steps'] as $stepIterator => $stepHash) { $outline->addStep($this->loadStepHash($stepHash, $stepIterator)); } } return $outline; } /** * Loads step from provided hash. * * @param array $hash Step hash * @param integer $line Step definition line * * @return StepNode */ protected function loadStepHash(array $hash, $line = 0) { $step = new Node\StepNode( $hash['type'], isset($hash['text']) ? $hash['text'] : null, isset($hash['line']) ? $hash['line'] : $line ); if (isset($hash['arguments'])) { foreach ($hash['arguments'] as $argumentHash) { if ('table' === $argumentHash['type']) { $step->addArgument($this->loadTableHash($argumentHash['rows'])); } elseif ('pystring' === $argumentHash['type']) { $step->addArgument($this->loadPyStringHash($argumentHash)); } } } return $step; } /** * Loads table from provided hash. * * @param array $hash Table hash * * @return TableNode */ protected function loadTableHash(array $hash) { $table = new Node\TableNode(); foreach ($hash as $line => $row) { $table->addRow($row, $line); } return $table; } /** * Loads PyString from provided hash. * * @param array $hash PyString hash * * @return PyStringNode */ protected function loadPyStringHash(array $hash) { $string = new Node\PyStringNode($hash['text']); return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Directory contents loader. * * @author Konstantin Kudryashov */ class DirectoryLoader extends AbstractFileLoader { protected $gherkin; /** * Initializes loader. * * @param Gherkin $gherkin Gherkin manager */ public function __construct(Gherkin $gherkin) { $this->gherkin = $gherkin; } /** * Checks if current loader supports provided resource. * * @param mixed $resource Resource to load * * @return Boolean */ public function supports($path) { return is_string($path) && is_dir($this->findAbsolutePath($path)); } /** * Loads features from provided resource. * * @param mixed $resource Resource to load * * @return array */ public function load($path) { $path = $this->findAbsolutePath($path); $finder = new Finder(); $iterator = $finder->files()->sortByName()->in($path); $features = array(); foreach ($iterator as $path) { $path = (string) $path; $loader = $this->gherkin->resolveLoader($path); if (null !== $loader) { $features = array_merge($features, $loader->load($path)); } } return $features; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * File Loader interface. * * @author Konstantin Kudryashov */ interface FileLoaderInterface extends LoaderInterface { /** * Sets base features path. * * @param string $path Base loader path */ public function setBasePath($path); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Gherkin *.feature files loader. * * @author Konstantin Kudryashov */ class GherkinFileLoader extends AbstractFileLoader { protected $parser; protected $cache; /** * Initializes loader. * * @param Parser $parser Parser * @param CacheInterface $cache Cache layer */ public function __construct(Parser $parser, CacheInterface $cache = null) { $this->parser = $parser; $this->cache = $cache; } /** * Sets cache layer. * * @param CacheInterface $cache Cache layer */ public function setCache(CacheInterface $cache) { $this->cache = $cache; } /** * Checks if current loader supports provided resource. * * @param mixed $resource Resource to load * * @return Boolean */ public function supports($path) { return is_string($path) && is_file($absolute = $this->findAbsolutePath($path)) && 'feature' === pathinfo($absolute, PATHINFO_EXTENSION); } /** * Loads features from provided resource. * * @param mixed $resource Resource to load * * @return array */ public function load($path) { $path = $this->findAbsolutePath($path); if ($this->cache) { if ($this->cache->isFresh($path, filemtime($path))) { $feature = $this->cache->read($path); } elseif (null !== $feature = $this->parseFeature($path)) { $this->cache->write($path, $feature); } } else { $feature = $this->parseFeature($path); } return null !== $feature ? array($feature) : array(); } /** * Parses feature at provided absolute path. * * @param string $path Feature path * * @return FeatureNode */ protected function parseFeature($path) { $filename = $this->findRelativePath($path); $content = file_get_contents($path); $feature = $this->parser->parse($content, $filename); return $feature; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Loader interface. * * @author Konstantin Kudryashov */ interface LoaderInterface { /** * Checks if current loader supports provided resource. * * @param mixed $resource Resource to load * * @return Boolean */ public function supports($resource); /** * Loads features from provided resource. * * @param mixed $resource Resource to load * * @return array */ public function load($resource); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Yaml files loader. * * @author Konstantin Kudryashov */ class YamlFileLoader extends ArrayLoader implements FileLoaderInterface { protected $basePath; /** * Checks if current loader supports provided resource. * * @param mixed $resource Resource to load * * @return Boolean */ public function supports($path) { return is_string($path) && is_file($absolute = $this->findAbsolutePath($path)) && 'yml' === pathinfo($absolute, PATHINFO_EXTENSION); } /** * Loads features from provided resource. * * @param mixed $resource Resource to load * * @return array */ public function load($path) { $path = $this->findAbsolutePath($path); $hash = Yaml::parse($path); $features = parent::load($hash); $filename = $this->findRelativePath($path); foreach ($features as $feature) { $feature->setFile($filename); } return $features; } /** * Sets base features path. * * @param string $path Base loader path */ public function setBasePath($path) { $this->basePath = realpath($path); } /** * Finds relative path for provided absolute (relative to base features path). * * @param string $path Absolute path * * @return string */ protected function findRelativePath($path) { if (null !== $this->basePath) { return strtr($path, array($this->basePath . DIRECTORY_SEPARATOR => '')); } return $path; } /** * Finds absolute path for provided relative (relative to base features path). * * @param string $path Relative path * * @return string */ protected function findAbsolutePath($path) { if (is_file($path) || is_dir($path)) { return realpath($path); } elseif (is_file($this->basePath . DIRECTORY_SEPARATOR . $path) || is_dir($this->basePath . DIRECTORY_SEPARATOR . $path)) { return realpath($this->basePath . DIRECTORY_SEPARATOR . $path); } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract Gherkin AST node. * * @author Konstantin Kudryashov */ abstract class AbstractNode { private $line; private $keyword; /** * Initializes node. * * @param integer $line Line number */ public function __construct($line = 0) { $this->line = $line; } /** * Accepts specific visitor & visits current node. * * @param NodeVisitorInterface $visitor Node visitor * * @return mixed */ public function accept(NodeVisitorInterface $visitor) { return $visitor->visit($this); } /** * Returns node line number. * * @return integer */ public function getLine() { return $this->line; } /** * Sets current node definition keyword. * * @param string $keyword Keyword * * @throws \LogicException if feature is frozen */ public function setKeyword($keyword) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen node keyword.'); } $this->keyword = $keyword; } /** * Returns current node definition keyword. * * @return string */ public function getKeyword() { return $this->keyword; } /** * Checks whether node has been frozen. * * @return Boolean */ abstract public function isFrozen(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Abstract Gherkin AST node. * * @author Konstantin Kudryashov */ abstract class AbstractScenarioNode extends AbstractNode { protected $title; protected $steps = array(); protected $feature; /** * Initializes scenario. * * @param string $title Scenario title * @param integer $line Definition line */ public function __construct($title = null, $line = 0) { parent::__construct($line); $this->title = $title; } /** * Sets scenario title. * * @param string $title Scenario title * * @throws \LogicException if feature is frozen */ public function setTitle($title) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change scenario/background title in frozen feature.'); } $this->title = $title; } /** * Returns scenario title. * * @return string */ public function getTitle() { return $this->title; } /** * Adds step to the node. * * @param StepNode $step Step * * @throws \LogicException if feature is frozen */ public function addStep(StepNode $step) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change scenario/background steps in frozen feature.'); } $step->setParent($this); $this->steps[] = $step; } /** * Sets scenario steps. * * @param array $steps Array of StepNode * * @throws \LogicException if feature is frozen */ public function setSteps(array $steps) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change scenario/background steps in frozen feature.'); } $this->steps = array(); foreach ($steps as $step) { $this->addStep($step); } } /** * Checks if node has steps. * * @return Boolean */ public function hasSteps() { return count($this->steps) > 0; } /** * Returns scenario steps. * * @return array */ public function getSteps() { return $this->steps; } /** * Sets parent feature of the node. * * @param FeatureNode $feature Feature instance * * @throws \LogicException if feature is frozen */ public function setFeature(FeatureNode $feature) { if ($this->isFrozen()) { throw new \LogicException('Impossible to reassign scenario/background in frozen feature.'); } $this->feature = $feature; } /** * Returns parent feature of the node. * * @return FeatureNode */ public function getFeature() { return $this->feature; } /** * Returns definition file. * * @return string */ public function getFile() { return null !== $this->feature ? $this->feature->getFile() : null; } /** * Returns language of the feature. * * @return string */ public function getLanguage() { return null !== $this->feature ? $this->feature->getLanguage() : null; } /** * Checks whether scenario has been frozen. * * @return Boolean */ public function isFrozen() { return null !== $this->feature ? $this->feature->isFrozen() : false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Background Gherkin AST node. * * @author Konstantin Kudryashov */ class BackgroundNode extends AbstractScenarioNode { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * PyString Argument for outline examples row Gherkin AST node. * * @author Konstantin Kudryashov */ class ExamplePyStringNode extends PyStringNode { private $cleanLines = array(); /** * Initializes PyString. * * @param PyStringNode $simpleString String from which this example string should be created * @param array $tokens Replacement tokens values */ public function __construct(PyStringNode $simpleString, array $tokens) { $this->cleanLines = $lines = $simpleString->getLines(); foreach ($tokens as $key => $value) { foreach (array_keys($lines) as $line) { $lines[$line] = str_replace('<'.$key.'>', $value, $lines[$line]); } } $this->setLines($lines); } /** * Returns not replaced with tokens string lines. * * @return array */ public function getCleanLines() { return $this->cleanLines; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Outline example step Gherkin AST node. * * @author Konstantin Kudryashov */ class ExampleStepNode extends StepNode { private $cleanText; /** * Initizalizes step. * * @param StepNode $simpleStep Initial step * @param array $tokens Example table row tokens */ public function __construct(StepNode $simpleStep, array $tokens) { $text = $this->cleanText = $simpleStep->getText(); foreach ($tokens as $key => $value) { $text = str_replace('<' . $key . '>', $value, $text); } parent::__construct( $simpleStep->getType(), $text, $simpleStep->getLine() ); foreach ($simpleStep->getArguments() as $argument) { if ($argument instanceof TableNode || $argument instanceof PyStringNode) { $this->addArgument($argument->createExampleRowStepArgument($tokens)); } } $this->setParent($simpleStep->getParent()); } /** * Returns untokenized step text. * * @return string */ public function getCleanText() { return $this->cleanText; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Table Argument Gherkin AST node. * * @author Konstantin Kudryashov */ class ExampleTableNode extends TableNode { private $cleanRows = array(); /** * Initializes table. * * @param TableNode $cleanTable * @param array $tokens * * @internal param string $table Initial table string */ public function __construct(TableNode $cleanTable, array $tokens) { $this->cleanRows = $rows = $cleanTable->getRows(); foreach ($tokens as $key => $value) { foreach (array_keys($rows) as $row) { foreach (array_keys($rows[$row]) as $col) { $rows[$row][$col] = str_replace('<'.$key.'>', $value, $rows[$row][$col]); } } } $this->setKeyword($cleanTable->getKeyword()); $this->setRows($rows); } /** * Returns rows without tokens being replaced. * * @return array */ public function getCleanRows() { return $this->cleanRows(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Feature Gherkin AST node. * * @author Konstantin Kudryashov */ class FeatureNode extends AbstractNode { private $title; private $description; private $file; private $background; private $language = 'en'; private $scenarios = array(); private $tags = array(); private $frozen = false; /** * Initializes feature. * * @param string $title Feature title * @param string $description Feature description (3-liner) * @param string $file Feature filename * @param integer $line Definition line */ public function __construct($title = null, $description = null, $file = null, $line = 0) { parent::__construct($line); $this->title = $title; $this->description = $description; $this->file = $file; } /** * Sets feature title. * * @param string $title Feature title * * @throws \LogicException if feature is frozen */ public function setTitle($title) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature title.'); } $this->title = $title; } /** * Returns feature title. * * @return string */ public function getTitle() { return $this->title; } /** * Sets feature description (narrative). * * @param string $description Feature description * * @throws \LogicException if feature is frozen */ public function setDescription($description) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature description.'); } $this->description = $description; } /** * Returns feature description (narrative). * * @return string */ public function getDescription() { return $this->description; } /** * Sets language of the feature. * * @param string $language Langauge name * * @throws \LogicException if feature is frozen */ public function setLanguage($language) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature language.'); } $this->language = $language; } /** * Returns language of the feature. * * @return string */ public function getLanguage() { return $this->language; } /** * Sets feature background. * * @param BackgroundNode $background Background instance * * @throws \LogicException if feature is frozen */ public function setBackground(BackgroundNode $background) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature background.'); } $background->setFeature($this); $this->background = $background; } /** * Checks if feature has background. * * @return Boolean */ public function hasBackground() { return null !== $this->background; } /** * Returns feature background. * * @return BackgroundNode */ public function getBackground() { return $this->background; } /** * Adds scenario or outline to the feature. * * @param ScenarioNode $scenario Scenario instance * * @throws \LogicException if feature is frozen */ public function addScenario(ScenarioNode $scenario) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature scenarios.'); } $scenario->setFeature($this); $this->scenarios[] = $scenario; } /** * Sets scenarios & outlines to the feature. * * @param array $scenarios Array of ScenariosNode's or OutlineNode's * * @throws \LogicException if feature is frozen */ public function setScenarios(array $scenarios) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature scenarios.'); } $this->scenarios = array(); foreach ($scenarios as $scenario) { $this->addScenario($scenario); } } /** * Checks that feature has scenarios. * * @return Boolean */ public function hasScenarios() { return count($this->scenarios) > 0; } /** * Returns feature scenarios & outlines. * * @return array */ public function getScenarios() { return $this->scenarios; } /** * Sets feature tags. * * @param array $tags Array of tags * * @throws \LogicException if feature is frozen */ public function setTags(array $tags) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature tags.'); } $this->tags = $tags; } /** * Adds tag to the feature. * * @param string $tag Tag name * * @throws \LogicException if feature is frozen */ public function addTag($tag) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature tags.'); } $this->tags[] = $tag; } /** * Checks if the feature has tags. * * @return Boolean */ public function hasTags() { return count($this->getTags()) > 0; } /** * Checks if the feature has tag. * * @param string $tag Tag name * * @return Boolean */ public function hasTag($tag) { return in_array($tag, $this->getTags()); } /** * Returns feature tags. * * @return array */ public function getTags() { return $this->tags; } /** * Returns only own tags (without inherited ones). * * @return array */ public function getOwnTags() { return $this->tags; } /** * Sets feature filename. * * @param string $path Sets feature file * * @throws \LogicException if feature is frozen */ public function setFile($path) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change frozen feature.'); } $this->file = $path; } /** * Returns feature filename. * * @return string */ public function getFile() { return $this->file; } /** * Freeze feature to changes. * Prevents feature modification in future */ public function freeze() { $this->frozen = true; } /** * Checks whether feature has been frozen. * * @return Boolean */ public function isFrozen() { return $this->frozen; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Node Visitor Interface. * * @author Konstantin Kudryashov */ interface NodeVisitorInterface { /** * Visits specific node. * * @param AbstractNode $visitee Visitee object * * @return mixed */ public function visit(AbstractNode $visitee); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Scenario Outline Gherkin AST node. * * @author Konstantin Kudryashov */ class OutlineNode extends ScenarioNode { private $examples; /** * Sets outline examples table. * * @param TableNode $examples Examples table * * @throws \LogicException if feature is frozen */ public function setExamples(TableNode $examples) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change outline examples in frozen feature.'); } $this->examples = $examples; } /** * Checks if outline has examples. * * @return Boolean */ public function hasExamples() { return null !== $this->examples; } /** * Returns examples table. * * @return TableNode */ public function getExamples() { return $this->examples; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * PyString Argument Gherkin AST node. * * @author Konstantin Kudryashov */ class PyStringNode implements StepArgumentNodeInterface { private $lines = array(); /** * Initializes PyString. * * @param string $string Initial string */ public function __construct($string = null) { if (null !== $string) { $string = preg_replace("/\r\n|\r/", "\n", $string); $this->lines = explode("\n", $string); } } /** * Returns new PyString node with replaced outline example row tokens. * * @param array $tokens * * @return ExamplePyStringNode */ public function createExampleRowStepArgument(array $tokens) { return new ExamplePyStringNode($this, $tokens); } /** * Adds a line to the PyString. * * @param string $line Line of text */ public function addLine($line) { $this->lines[] = $line; } /** * Sets PyString lines. * * @param array $lines Array of text lines */ public function setLines(array $lines) { $this->lines = $lines; } /** * Returns PyString lines. * * @return array */ public function getLines() { return $this->lines; } /** * Returns raw string. * * @return string */ public function getRaw() { return implode("\n", $this->lines); } /** * Converts PyString into string. * * @return string */ public function __toString() { return $this->getRaw(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Scenario Gherkin AST node. * * @author Konstantin Kudryashov */ class ScenarioNode extends AbstractScenarioNode { private $tags = array(); /** * Sets scenario tags. * * @param array $tags Array of tag names * * @throws \LogicException if feature is frozen */ public function setTags(array $tags) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change scenario tags in frozen feature.'); } $this->tags = $tags; } /** * Adds tag to scenario. * * @param string $tag Tag name * * @throws \LogicException if feature is frozen */ public function addTag($tag) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change scenario tags in frozen feature.'); } $this->tags[] = $tag; } /** * Checks if scenario has tags. * * @return Boolean */ public function hasTags() { return count($this->getTags()) > 0; } /** * Checks if scenario has tag. * * @param string $tag * * @return Boolean */ public function hasTag($tag) { return in_array($tag, $this->getTags()); } /** * Returns scenario tags. * * @return array */ public function getTags() { $tags = $this->tags; if ($feature = $this->getFeature()) { $tags = array_merge($tags, $feature->getTags()); } return $tags; } /** * Returns only own tags (without inherited ones). * * @return array */ public function getOwnTags() { return $this->tags; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Node Visitor Interface. * * @author Konstantin Kudryashov */ interface StepArgumentNodeInterface { /** * Returns new node with replaced outline example row tokens. * * @param array $tokens * * @return ExamplePyStringNode */ public function createExampleRowStepArgument(array $tokens); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Step Gherkin AST node. * * @author Konstantin Kudryashov */ class StepNode extends AbstractNode { private $type; private $text; private $parent; private $arguments = array(); /** * Initizalizes step. * * @param string $type Step type * @param string $text Step text * @param integer $line Definition line */ public function __construct($type, $text = null, $line = 0) { parent::__construct($line); $this->type = $type; $this->text = $text; } /** * Returns new example step, initialized with values from specific row. * * @return ExampleStepNode * * @throws \LogicException if feature is frozen */ public function createExampleRowStep(array $tokens) { if (!$this->isFrozen()) { throw new \LogicException('Impossible to get example step from non-frozen one.'); } return new ExampleStepNode($this, $tokens); } /** * Sets step type. * * @param string $type Step type (Given|When|Then|And etc) * * @throws \LogicException if feature is frozen */ public function setType($type) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change step type in frozen feature.'); } $this->type = $type; } /** * Returns step type. * * @return string */ public function getType() { return $this->type; } /** * Sets step text. * * @param string $text Step text * * @throws \LogicException if feature is frozen */ public function setText($text) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change step text in frozen feature.'); } $this->text = $text; } /** * Returns step text. * * @return string */ public function getText() { return $this->text; } /** * Adds argument to step. * * @param StepArgumentNodeInterface $argument Step argument * * @throws \LogicException if feature is frozen */ public function addArgument(StepArgumentNodeInterface $argument) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change step arguments in frozen feature.'); } $this->arguments[] = $argument; } /** * Sets step arguments. * * @param array $arguments Array of arguments * * @throws \LogicException if feature is frozen */ public function setArguments(array $arguments) { if ($this->isFrozen()) { throw new \LogicException('Impossible to change step arguments in frozen feature.'); } foreach ($arguments as $argument) { $this->addArgument($argument); } } /** * Checks if step has arguments. * * @return Boolean */ public function hasArguments() { return count($this->arguments) > 0; } /** * Returns step arguments. * * @return array */ public function getArguments() { return $this->arguments; } /** * Sets parent node of the step. * * @param AbstractScenarioNode $node Parent scenario * * @throws \LogicException if feature is frozen */ public function setParent(AbstractScenarioNode $node) { if ($this->isFrozen()) { throw new \LogicException('Impossible to reassign step from frozen feature.'); } $this->parent = $node; } /** * Returns parent node of the step. * * @return AbstractScenarioNode */ public function getParent() { return $this->parent; } /** * Returns definition file. * * @return string */ public function getFile() { return null !== $this->parent ? $this->parent->getFile() : null; } /** * Returns language of the feature. * * @return string */ public function getLanguage() { return null !== $this->parent ? $this->parent->getLanguage() : null; } /** * Checks whether step has been frozen. * * @return Boolean */ public function isFrozen() { return null !== $this->parent ? $this->parent->isFrozen() : false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Table Argument Gherkin AST node. * * @author Konstantin Kudryashov */ class TableNode implements StepArgumentNodeInterface { private $rows = array(); private $rowLines = array(); private $keyword; /** * Initializes table. * * @param string $table Initial table string */ public function __construct($table = null) { if (null !== $table) { $table = preg_replace("/\r\n|\r/", "\n", $table); foreach (explode("\n", $table) as $row) { $this->addRow($row); } } } /** * Returns new node with replaced outline example row tokens. * * @returns ExampleTableNode */ public function createExampleRowStepArgument(array $tokens) { return new ExampleTableNode($this, $tokens); } /** * Adds a row to the string. * * @param string|array $row Columns hash (column1 => value, column2 => value) or row string * @param null|integer $line Row line number */ public function addRow($row, $line = null) { if (is_array($row)) { $this->rows[] = $row; } else { $row = preg_replace("/^\s*\||\|\s*$/", '', $row); $this->rows[] = array_map(function($item) { return preg_replace("/^\s*|\s*$/", '', $item); }, explode('|', $row)); } $this->rowLines[count($this->rows) - 1] = $line; } /** * Returns table rows. * * @return array */ public function getRows() { return $this->rows; } /** * Sets table rows. * * @param array $rows */ public function setRows(array $rows) { $this->rows = $rows; $this->rowLines = array(); } /** * Returns specific row in a table. * * @param integer $rowNum Row number * * @return array */ public function getRow($rowNum) { return $this->rows[$rowNum]; } /** * Converts row into delimited string. * * @param integer $rowNum Row number * * @return string */ public function getRowAsString($rowNum) { $values = array(); foreach ($this->getRow($rowNum) as $col => $value) { $values[] = $this->padRight(' '.$value.' ', $this->getMaxLengthForColumn($col) + 2); } return sprintf('|%s|', implode('|', $values)); } /** * Returns table hash, formed by columns (ColumnHash). * * @return array */ public function getHash() { $rows = $this->getRows(); $keys = array_shift($rows); $hash = array(); foreach ($rows as $row) { $hash[] = array_combine($keys, $row); } return $hash; } /** * Returns table hash, formed by rows (RowsHash). * * @return array */ public function getRowsHash() { $hash = array(); foreach ($this->getRows() as $row) { $hash[array_shift($row)] = (1 == count($row)) ? $row[0] : $row; } return $hash; } /** * Sets current node definition keyword. * * @param string $keyword Sets table keyword */ public function setKeyword($keyword) { $this->keyword = $keyword; } /** * Returns current node definition keyword. * * @return string */ public function getKeyword() { return $this->keyword; } /** * Returns numerated table lines. * Line numbers are keys, lines are values. * * @return array */ public function getNumeratedRows() { return array_combine($this->rowLines, $this->rows); } /** * Returns line numbers for rows. * * @return array */ public function getRowLines() { return $this->rowLines; } /** * Returns table start line number. * * @return integer */ public function getLine() { return count($this->rowLines) ? $this->rowLines[0] : 0; } /** * Converts table into string * * @return string */ public function __toString() { $string = ''; for ($i = 0; $i < count($this->getRows()); $i++) { if ('' !== $string) { $string .= "\n"; } $string .= $this->getRowAsString($i); } return $string; } /** * Returns max length of specific column. * * @param integer $columnNum Column number * * @return integer */ protected function getMaxLengthForColumn($columnNum) { $max = 0; foreach ($this->getRows() as $row) { if(isset($row[$columnNum])){ if (($tmp = mb_strlen($row[$columnNum])) > $max) { $max = $tmp; } } } return $max; } /** * Pads string right. * * @param string $text Text to pad * @param integer $length Lenght * * @return string */ protected function padRight($text, $length) { while ($length > mb_strlen($text)) { $text = $text . ' '; } return $text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Gherkin parser. * * $lexer = new Behat\Gherkin\Lexer($keywords); * $parser = new Behat\Gherkin\Parser($lexer); * $featuresArray = $parser->parse('/path/to/feature.feature'); * * @author Konstantin Kudryashov */ class Parser { private $file; private $lexer; /** * Initializes parser. * * @param Lexer $lexer Lexer instance */ public function __construct(Lexer $lexer) { $this->lexer = $lexer; } /** * Parses input & returns features array. * * @param string $input Gherkin string document * * @return array * * @throws ParserException */ public function parse($input, $file = null) { $this->file = $file; try { $this->lexer->setInput($input); } catch (LexerException $e) { throw new ParserException( sprintf('Lexer exception "%s" throwed for file %s', $e->getMessage(), $file) ); } $this->lexer->setLanguage($language = 'en'); $languageSpecifierLine = null; $feature = null; while ('EOS' !== ($predicted = $this->predictTokenType())) { if ('Newline' === $predicted || 'Comment' === $predicted) { $this->lexer->getAdvancedToken(); } elseif ('Language' === $predicted) { $token = $this->expectTokenType('Language'); $language = $token->value; if (null === $languageSpecifierLine) { // Reparse input with new language $languageSpecifierLine = $token->line; $this->lexer->setInput($input); $this->lexer->setLanguage($language); } elseif ($languageSpecifierLine !== $token->line) { // Language already specified throw new ParserException(sprintf( 'Ambigious language specifiers on lines: %d and %d%s', $languageSpecifierLine, $token->line, $this->file ? ' in file: ' . $this->file : '' )); } } elseif (null === $feature && ('Feature' === $predicted || ( 'Tag' === $predicted && 'Feature' === $this->predictTokenType(2)) )) { $feature = $this->parseExpression(); $feature->setLanguage($language); } else { $this->expectTokenType(array('Comment', 'Scenario', 'Outline', 'Step')); } } return $feature; } /** * Returns next token if it's type equals to expected. * * @param string $types Token type * * @return stdClass * * @throws ParserException if token type is differ from expected one */ protected function expectTokenType($type) { $types = (array) $type; if (in_array($this->predictTokenType(), $types)) { return $this->lexer->getAdvancedToken(); } throw new ParserException(sprintf('Expected %s token, but got %s on line: %d%s', implode(' or ', $types), $this->predictTokenType(), $this->lexer->predictToken()->line, $this->file ? ' in file: ' . $this->file : '' )); } /** * Returns next token if it's type equals to expected. * * @param string $type Token type * * @return stdClass */ protected function acceptTokenType($type) { if ($type === $this->predictTokenType()) { return $this->lexer->getAdvancedToken(); } } /** * Returns next token type without real input reading (prediction). * * @param integer $number Number of tokens to predict * * @return string */ protected function predictTokenType($number = 1) { return $this->lexer->predictToken($number)->type; } /** * Parses current expression & returns Node. * * @return string|AbstractNode */ protected function parseExpression() { switch ($this->predictTokenType()) { case 'Feature': return $this->parseFeature(); case 'Background': return $this->parseBackground(); case 'Scenario': return $this->parseScenario(); case 'Outline': return $this->parseOutline(); case 'TableRow': return $this->parseTable(); case 'PyStringOperator': return $this->parsePyString(); case 'Step': return $this->parseStep(); case 'Comment': return $this->parseComment(); case 'Text': return $this->parseText(); case 'Tag': $token = $this->lexer->getAdvancedToken(); $this->skipExtraChars(); $this->lexer->deferToken($this->lexer->getAdvancedToken()); $this->lexer->deferToken($token); return $this->parseExpression(); } } /** * Parses feature token & returns it's node. * * @return FeatureNode */ protected function parseFeature() { $token = $this->expectTokenType('Feature'); $node = new Node\FeatureNode(trim($token->value) ?: null, null, $this->file, $token->line); $node->setKeyword($token->keyword); // Parse tags $this->parseNodeTags($node); // Parse description $this->parseNodeDescription($node, $token->indent+2); // Parse background if ('Background' === $this->predictTokenType()) { $node->setBackground($this->parseExpression()); } // Parse scenarios & outlines while ('Scenario' === ($predicted = $this->predictTokenType()) || ('Tag' === $predicted && 'Scenario' === ($predicted2 = $this->predictTokenType(2))) || 'Outline' === $predicted || ('Tag' === $predicted && 'Outline' === $predicted2)) { $node->addScenario($this->parseExpression()); } return $node; } /** * Parses background token & returns it's node. * * @return BackgroundNode */ protected function parseBackground() { $token = $this->expectTokenType('Background'); $node = new Node\BackgroundNode(trim($token->value) ?: null, $token->line); $node->setKeyword($token->keyword); $this->skipComments(); // Parse title $this->parseNodeDescription($node, $token->indent+2); // Parse steps while ('Step' === $this->predictTokenType()) { $node->addStep($this->parseExpression()); } return $node; } /** * Parses scenario outline token & returns it's node. * * @return OutlineNode */ protected function parseOutline() { $token = $this->expectTokenType('Outline'); $node = new Node\OutlineNode(trim($token->value) ?: null, $token->line); $node->setKeyword($token->keyword); // Parse tags $this->parseNodeTags($node); // Parse title $this->parseNodeDescription($node, $token->indent+2); // Parse steps while ('Step' === $this->predictTokenType()) { $node->addStep($this->parseExpression()); } // Examples block $examplesToken = $this->expectTokenType('Examples'); $this->skipExtraChars(); // Parse examples table $table = $this->parseTable(); $table->setKeyword($examplesToken->keyword); $node->setExamples($table); return $node; } /** * Parses scenario token & returns it's node. * * @return ScenarioNode */ protected function parseScenario() { $token = $this->expectTokenType('Scenario'); $node = new Node\ScenarioNode(trim($token->value) ?: null, $token->line); $node->setKeyword($token->keyword); // Parse tags $this->parseNodeTags($node); // Parse title $this->parseNodeDescription($node, $token->indent+2); // Parse scenario steps while ('Step' === $this->predictTokenType()) { $node->addStep($this->parseExpression()); } return $node; } /** * Parses step token & returns it's node. * * @return StepNode */ protected function parseStep() { $token = $this->expectTokenType('Step'); $node = new Node\StepNode($token->value, trim($token->text) ?: null, $token->line); $this->skipExtraChars(); // Parse PyString argument if ('PyStringOperator' === $this->predictTokenType()) { $node->addArgument($this->parseExpression()); } // Parse Table argument if ('TableRow' === $this->predictTokenType()) { $node->addArgument($this->parseExpression()); } return $node; } /** * Parses table token & returns it's node. * * @return TableNode */ protected function parseTable() { $token = $this->expectTokenType('TableRow'); $node = new Node\TableNode(); $node->addRow($token->columns, $token->line); $this->skipExtraChars(); while ('TableRow' === $this->predictTokenType()) { $token = $this->expectTokenType('TableRow'); $node->addRow($token->columns, $token->line); $this->skipExtraChars(); } return $node; } /** * Parses PyString token & returns it's node. * * @return PyStringNode */ protected function parsePyString() { $token = $this->expectTokenType('PyStringOperator'); $node = new Node\PyStringNode(); while ('PyStringOperator' !== ($predicted = $this->predictTokenType()) && 'Text' === $predicted) { $node->addLine($this->parseText(false)); } $this->expectTokenType('PyStringOperator'); $this->skipExtraChars(); return $node; } /** * Parses next text token & returns it's string content. * * @param Boolean $skipExtraChars Do we need to skip newlines & spaces * * @return string */ protected function parseText($skipExtraChars = true) { $token = $this->expectTokenType('Text'); if ($skipExtraChars) { $this->skipExtraChars(); } return $token->value; } /** * Parses next comment token & returns it's string content. * * @return string */ protected function parseComment() { $token = $this->expectTokenType('Comment'); return $token->value; } /** * Parse tags for the feature/scenario/outline node. * * @param AbstractNode $node Node with tags */ private function parseNodeTags(Node\AbstractNode $node) { $this->skipComments(); while ('Tag' === $this->predictTokenType()) { $node->setTags($this->lexer->getAdvancedToken()->tags); $this->skipComments(); } } /** * Parse description/title for feature/background/scenario/outline node. * * @param AbstractNode $node Node with description * @param integer $indentation Indentation */ private function parseNodeDescription(Node\AbstractNode $node, $indentation) { $setter = 'setTitle'; $getter = 'getTitle'; if ($node instanceof Node\FeatureNode) { $setter = 'setDescription'; $getter = 'getDescription'; } // Parse description/title while (in_array($predicted = $this->predictTokenType(), array('Text', 'Newline'))) { if ('Text' === $predicted) { $text = $this->parseText(false); $text = preg_replace('/^\s{0,'.$indentation.'}|\s*$/', '', $text); } else { $this->acceptTokenType('Newline'); $text = ''; } if ($node instanceof Node\FeatureNode && null === $node->$getter()) { $node->$setter($text); } else { $node->$setter($node->$getter() . "\n" . $text); } $this->skipComments(); } // Trim title/description if (null !== $node->$getter()) { $node->$setter(rtrim($node->$getter()) ?: null); } } /** * Skips newlines & comments in input. * * @param Boolean $skipNL Skip newline? */ private function skipExtraChars() { while ($this->acceptTokenType('Newline') || $this->acceptTokenType('Comment')); } /** * Skips newlines & comments in input. */ private function skipComments() { while ($this->acceptTokenType('Comment')); } } $vendorDir . '/symfony/yaml/', 'Symfony\\Component\\Translation\\' => $vendorDir . '/symfony/translation/', 'Symfony\\Component\\Finder\\' => $vendorDir . '/symfony/finder/', 'Symfony\\Component\\EventDispatcher\\' => $vendorDir . '/symfony/event-dispatcher/', 'Symfony\\Component\\DependencyInjection\\' => $vendorDir . '/symfony/dependency-injection/', 'Symfony\\Component\\Console\\' => $vendorDir . '/symfony/console/', 'Symfony\\Component\\Config\\' => $vendorDir . '/symfony/config/', 'Behat\\Gherkin' => $vendorDir . '/behat/gherkin/src/', 'Behat\\Behat' => $baseDir . '/src/', ); $path) { $loader->add($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } $loader->register(true); return $loader; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0 class loader * * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier * @author Jordi Boggiano */ class ClassLoader { private $prefixes = array(); private $fallbackDirs = array(); private $useIncludePath = false; private $classMap = array(); public function getPrefixes() { return $this->prefixes; } public function getFallbackDirs() { return $this->fallbackDirs; } public function getClassMap() { return $this->classMap; } /** * @param array $classMap Class to filename map */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of classes, merging with any others previously set. * * @param string $prefix The classes prefix * @param array|string $paths The location(s) of the classes * @param bool $prepend Prepend the location(s) */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirs = array_merge( (array) $paths, $this->fallbackDirs ); } else { $this->fallbackDirs = array_merge( $this->fallbackDirs, (array) $paths ); } return; } if (!isset($this->prefixes[$prefix])) { $this->prefixes[$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixes[$prefix] = array_merge( (array) $paths, $this->prefixes[$prefix] ); } else { $this->prefixes[$prefix] = array_merge( $this->prefixes[$prefix], (array) $paths ); } } /** * Registers a set of classes, replacing any others previously set. * * @param string $prefix The classes prefix * @param array|string $paths The location(s) of the classes */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirs = (array) $paths; return; } $this->prefixes[$prefix] = (array) $paths; } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); } /** * Unregisters this instance as an autoloader. */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); } /** * Loads the given class or interface. * * @param string $class The name of the class * @return bool|null True, if loaded */ public function loadClass($class) { if ($file = $this->findFile($class)) { include $file; return true; } } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|null The path, if found */ public function findFile($class) { if ('\\' == $class[0]) { $class = substr($class, 1); } if (isset($this->classMap[$class])) { return $this->classMap[$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); } else { // PEAR-like class name $classPath = null; $className = $class; } $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; foreach ($this->prefixes as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { return $dir . DIRECTORY_SEPARATOR . $classPath; } } } } foreach ($this->fallbackDirs as $dir) { if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { return $dir . DIRECTORY_SEPARATOR . $classPath; } } if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) { return $file; } return $this->classMap[$class] = false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config; use Symfony\Component\Config\Resource\ResourceInterface; /** * ConfigCache manages PHP cache files. * * When debug is enabled, it knows when to flush the cache * thanks to an array of ResourceInterface instances. * * @author Fabien Potencier */ class ConfigCache { private $debug; private $file; /** * Constructor. * * @param string $file The absolute cache path * @param Boolean $debug Whether debugging is enabled or not */ public function __construct($file, $debug) { $this->file = $file; $this->debug = (Boolean) $debug; } /** * Gets the cache file path. * * @return string The cache file path */ public function __toString() { return $this->file; } /** * Checks if the cache is still fresh. * * This method always returns true when debug is off and the * cache file exists. * * @return Boolean true if the cache is fresh, false otherwise */ public function isFresh() { if (!is_file($this->file)) { return false; } if (!$this->debug) { return true; } $metadata = $this->file.'.meta'; if (!is_file($metadata)) { return false; } $time = filemtime($this->file); $meta = unserialize(file_get_contents($metadata)); foreach ($meta as $resource) { if (!$resource->isFresh($time)) { return false; } } return true; } /** * Writes cache. * * @param string $content The content to write in the cache * @param ResourceInterface[] $metadata An array of ResourceInterface instances * * @throws \RuntimeException When cache file can't be wrote */ public function write($content, array $metadata = null) { $dir = dirname($this->file); if (!is_dir($dir)) { if (false === @mkdir($dir, 0777, true)) { throw new \RuntimeException(sprintf('Unable to create the %s directory', $dir)); } } elseif (!is_writable($dir)) { throw new \RuntimeException(sprintf('Unable to write in the %s directory', $dir)); } $tmpFile = tempnam($dir, basename($this->file)); if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $this->file)) { @chmod($this->file, 0666 & ~umask()); } else { throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $this->file)); } if (null !== $metadata && true === $this->debug) { $file = $this->file.'.meta'; $tmpFile = tempnam($dir, basename($file)); if (false !== @file_put_contents($tmpFile, serialize($metadata)) && @rename($tmpFile, $file)) { @chmod($file, 0666 & ~umask()); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Exception\InvalidTypeException; use Symfony\Component\Config\Definition\Exception\UnsetKeyException; /** * Represents an Array node in the config tree. * * @author Johannes M. Schmitt */ class ArrayNode extends BaseNode implements PrototypeNodeInterface { protected $xmlRemappings; protected $children; protected $allowFalse; protected $allowNewKeys; protected $addIfNotSet; protected $performDeepMerging; protected $ignoreExtraKeys; protected $normalizeKeys; /** * Constructor. * * @param string $name The Node's name * @param NodeInterface $parent The node parent */ public function __construct($name, NodeInterface $parent = null) { parent::__construct($name, $parent); $this->children = array(); $this->xmlRemappings = array(); $this->removeKeyAttribute = true; $this->allowFalse = false; $this->addIfNotSet = false; $this->allowNewKeys = true; $this->performDeepMerging = true; $this->normalizeKeys = true; } public function setNormalizeKeys($normalizeKeys) { $this->normalizeKeys = (Boolean) $normalizeKeys; } /** * Normalizes keys between the different configuration formats. * * Namely, you mostly have foo_bar in YAML while you have foo-bar in XML. * After running this method, all keys are normalized to foo_bar. * * If you have a mixed key like foo-bar_moo, it will not be altered. * The key will also not be altered if the target key already exists. * * @param mixed $value * * @return array The value with normalized keys */ protected function preNormalize($value) { if (!$this->normalizeKeys || !is_array($value)) { return $value; } foreach ($value as $k => $v) { if (false !== strpos($k, '-') && false === strpos($k, '_') && !array_key_exists($normalizedKey = str_replace('-', '_', $k), $value)) { $value[$normalizedKey] = $v; unset($value[$k]); } } return $value; } /** * Retrieves the children of this node. * * @return array The children */ public function getChildren() { return $this->children; } /** * Sets the xml remappings that should be performed. * * @param array $remappings an array of the form array(array(string, string)) */ public function setXmlRemappings(array $remappings) { $this->xmlRemappings = $remappings; } /** * Sets whether to add default values for this array if it has not been * defined in any of the configuration files. * * @param Boolean $boolean */ public function setAddIfNotSet($boolean) { $this->addIfNotSet = (Boolean) $boolean; } /** * Sets whether false is allowed as value indicating that the array should be unset. * * @param Boolean $allow */ public function setAllowFalse($allow) { $this->allowFalse = (Boolean) $allow; } /** * Sets whether new keys can be defined in subsequent configurations. * * @param Boolean $allow */ public function setAllowNewKeys($allow) { $this->allowNewKeys = (Boolean) $allow; } /** * Sets if deep merging should occur. * * @param Boolean $boolean */ public function setPerformDeepMerging($boolean) { $this->performDeepMerging = (Boolean) $boolean; } /** * Whether extra keys should just be ignore without an exception. * * @param Boolean $boolean To allow extra keys */ public function setIgnoreExtraKeys($boolean) { $this->ignoreExtraKeys = (Boolean) $boolean; } /** * Sets the node Name. * * @param string $name The node's name */ public function setName($name) { $this->name = $name; } /** * Checks if the node has a default value. * * @return Boolean */ public function hasDefaultValue() { return $this->addIfNotSet; } /** * Retrieves the default value. * * @return array The default value * * @throws \RuntimeException if the node has no default value */ public function getDefaultValue() { if (!$this->hasDefaultValue()) { throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath())); } $defaults = array(); foreach ($this->children as $name => $child) { if ($child->hasDefaultValue()) { $defaults[$name] = $child->getDefaultValue(); } } return $defaults; } /** * Adds a child node. * * @param NodeInterface $node The child node to add * * @throws \InvalidArgumentException when the child node has no name * @throws \InvalidArgumentException when the child node's name is not unique */ public function addChild(NodeInterface $node) { $name = $node->getName(); if (empty($name)) { throw new \InvalidArgumentException('Child nodes must be named.'); } if (isset($this->children[$name])) { throw new \InvalidArgumentException(sprintf('A child node named "%s" already exists.', $name)); } $this->children[$name] = $node; } /** * Finalizes the value of this node. * * @param mixed $value * * @return mixed The finalised value * * @throws UnsetKeyException * @throws InvalidConfigurationException if the node doesn't have enough children */ protected function finalizeValue($value) { if (false === $value) { $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value)); throw new UnsetKeyException($msg); } foreach ($this->children as $name => $child) { if (!array_key_exists($name, $value)) { if ($child->isRequired()) { $msg = sprintf('The child node "%s" at path "%s" must be configured.', $name, $this->getPath()); $ex = new InvalidConfigurationException($msg); $ex->setPath($this->getPath()); throw $ex; } if ($child->hasDefaultValue()) { $value[$name] = $child->getDefaultValue(); } continue; } try { $value[$name] = $child->finalize($value[$name]); } catch (UnsetKeyException $unset) { unset($value[$name]); } } return $value; } /** * Validates the type of the value. * * @param mixed $value * * @throws InvalidTypeException */ protected function validateType($value) { if (!is_array($value) && (!$this->allowFalse || false !== $value)) { $ex = new InvalidTypeException(sprintf( 'Invalid type for path "%s". Expected array, but got %s', $this->getPath(), gettype($value) )); $ex->setPath($this->getPath()); throw $ex; } } /** * Normalizes the value. * * @param mixed $value The value to normalize * * @return mixed The normalized value * * @throws InvalidConfigurationException */ protected function normalizeValue($value) { if (false === $value) { return $value; } $value = $this->remapXml($value); $normalized = array(); foreach ($this->children as $name => $child) { if (array_key_exists($name, $value)) { $normalized[$name] = $child->normalize($value[$name]); unset($value[$name]); } } // if extra fields are present, throw exception if (count($value) && !$this->ignoreExtraKeys) { $msg = sprintf('Unrecognized options "%s" under "%s"', implode(', ', array_keys($value)), $this->getPath()); $ex = new InvalidConfigurationException($msg); $ex->setPath($this->getPath()); throw $ex; } return $normalized; } /** * Remaps multiple singular values to a single plural value. * * @param array $value The source values * * @return array The remapped values */ protected function remapXml($value) { foreach ($this->xmlRemappings as $transformation) { list($singular, $plural) = $transformation; if (!isset($value[$singular])) { continue; } $value[$plural] = Processor::normalizeConfig($value, $singular, $plural); unset($value[$singular]); } return $value; } /** * Merges values together. * * @param mixed $leftSide The left side to merge. * @param mixed $rightSide The right side to merge. * * @return mixed The merged values * * @throws InvalidConfigurationException * @throws \RuntimeException */ protected function mergeValues($leftSide, $rightSide) { if (false === $rightSide) { // if this is still false after the last config has been merged the // finalization pass will take care of removing this key entirely return false; } if (false === $leftSide || !$this->performDeepMerging) { return $rightSide; } foreach ($rightSide as $k => $v) { // no conflict if (!array_key_exists($k, $leftSide)) { if (!$this->allowNewKeys) { $ex = new InvalidConfigurationException(sprintf( 'You are not allowed to define new elements for path "%s". ' .'Please define all elements for this path in one config file. ' .'If you are trying to overwrite an element, make sure you redefine it ' .'with the same name.', $this->getPath() )); $ex->setPath($this->getPath()); throw $ex; } $leftSide[$k] = $v; continue; } if (!isset($this->children[$k])) { throw new \RuntimeException('merge() expects a normalized config array.'); } $leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v); } return $leftSide; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\Exception; use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; /** * The base node class * * @author Johannes M. Schmitt */ abstract class BaseNode implements NodeInterface { protected $name; protected $parent; protected $normalizationClosures; protected $finalValidationClosures; protected $allowOverwrite; protected $required; protected $equivalentValues; protected $attributes = array(); /** * Constructor. * * @param string $name The name of the node * @param NodeInterface $parent The parent of this node * * @throws \InvalidArgumentException if the name contains a period. */ public function __construct($name, NodeInterface $parent = null) { if (false !== strpos($name, '.')) { throw new \InvalidArgumentException('The name must not contain ".".'); } $this->name = $name; $this->parent = $parent; $this->normalizationClosures = array(); $this->finalValidationClosures = array(); $this->allowOverwrite = true; $this->required = false; $this->equivalentValues = array(); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; } public function getAttribute($key, $default = null) { return isset($this->attributes[$key]) ? $this->attributes[$key] : $default; } public function hasAttribute($key) { return isset($this->attributes[$key]); } public function getAttributes() { return $this->attributes; } public function setAttributes(array $attributes) { $this->attributes = $attributes; } public function removeAttribute($key) { unset($this->attributes[$key]); } /** * Sets an info message. * * @param string $info */ public function setInfo($info) { $this->setAttribute('info', $info); } /** * Returns info message. * * @return string The info text */ public function getInfo() { return $this->getAttribute('info'); } /** * Sets the example configuration for this node. * * @param string|array $example */ public function setExample($example) { $this->setAttribute('example', $example); } /** * Retrieves the example configuration for this node. * * @return string|array The example */ public function getExample() { return $this->getAttribute('example'); } /** * Adds an equivalent value. * * @param mixed $originalValue * @param mixed $equivalentValue */ public function addEquivalentValue($originalValue, $equivalentValue) { $this->equivalentValues[] = array($originalValue, $equivalentValue); } /** * Set this node as required. * * @param Boolean $boolean Required node */ public function setRequired($boolean) { $this->required = (Boolean) $boolean; } /** * Sets if this node can be overridden. * * @param Boolean $allow */ public function setAllowOverwrite($allow) { $this->allowOverwrite = (Boolean) $allow; } /** * Sets the closures used for normalization. * * @param \Closure[] $closures An array of Closures used for normalization */ public function setNormalizationClosures(array $closures) { $this->normalizationClosures = $closures; } /** * Sets the closures used for final validation. * * @param \Closure[] $closures An array of Closures used for final validation */ public function setFinalValidationClosures(array $closures) { $this->finalValidationClosures = $closures; } /** * Checks if this node is required. * * @return Boolean */ public function isRequired() { return $this->required; } /** * Returns the name of this node * * @return string The Node's name. */ public function getName() { return $this->name; } /** * Retrieves the path of this node. * * @return string The Node's path */ public function getPath() { $path = $this->name; if (null !== $this->parent) { $path = $this->parent->getPath().'.'.$path; } return $path; } /** * Merges two values together. * * @param mixed $leftSide * @param mixed $rightSide * * @return mixed The merged value * * @throws ForbiddenOverwriteException */ final public function merge($leftSide, $rightSide) { if (!$this->allowOverwrite) { throw new ForbiddenOverwriteException(sprintf( 'Configuration path "%s" cannot be overwritten. You have to ' .'define all options for this path, and any of its sub-paths in ' .'one configuration section.', $this->getPath() )); } $this->validateType($leftSide); $this->validateType($rightSide); return $this->mergeValues($leftSide, $rightSide); } /** * Normalizes a value, applying all normalization closures. * * @param mixed $value Value to normalize. * * @return mixed The normalized value. */ final public function normalize($value) { $value = $this->preNormalize($value); // run custom normalization closures foreach ($this->normalizationClosures as $closure) { $value = $closure($value); } // replace value with their equivalent foreach ($this->equivalentValues as $data) { if ($data[0] === $value) { $value = $data[1]; } } // validate type $this->validateType($value); // normalize value return $this->normalizeValue($value); } /** * Normalizes the value before any other normalization is applied. * * @param $value * * @return $value The normalized array value */ protected function preNormalize($value) { return $value; } /** * Finalizes a value, applying all finalization closures. * * @param mixed $value The value to finalize * * @return mixed The finalized value * * @throws InvalidConfigurationException */ final public function finalize($value) { $this->validateType($value); $value = $this->finalizeValue($value); // Perform validation on the final value if a closure has been set. // The closure is also allowed to return another value. foreach ($this->finalValidationClosures as $closure) { try { $value = $closure($value); } catch (Exception $correctEx) { throw $correctEx; } catch (\Exception $invalid) { throw new InvalidConfigurationException(sprintf( 'Invalid configuration for path "%s": %s', $this->getPath(), $invalid->getMessage() ), $invalid->getCode(), $invalid); } } return $value; } /** * Validates the type of a Node. * * @param mixed $value The value to validate * * @throws InvalidTypeException when the value is invalid */ abstract protected function validateType($value); /** * Normalizes the value. * * @param mixed $value The value to normalize. * * @return mixed The normalized value */ abstract protected function normalizeValue($value); /** * Merges two values together. * * @param mixed $leftSide * @param mixed $rightSide * * @return mixed The merged value */ abstract protected function mergeValues($leftSide, $rightSide); /** * Finalizes a value. * * @param mixed $value The value to finalize * * @return mixed The finalized value */ abstract protected function finalizeValue($value); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\InvalidTypeException; /** * This node represents a Boolean value in the config tree. * * @author Johannes M. Schmitt */ class BooleanNode extends ScalarNode { /** * {@inheritDoc} */ protected function validateType($value) { if (!is_bool($value)) { $ex = new InvalidTypeException(sprintf( 'Invalid type for path "%s". Expected boolean, but got %s.', $this->getPath(), gettype($value) )); $ex->setPath($this->getPath()); throw $ex; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\ArrayNode; use Symfony\Component\Config\Definition\PrototypedArrayNode; use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; /** * This class provides a fluent interface for defining an array node. * * @author Johannes M. Schmitt */ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinitionInterface { protected $performDeepMerging; protected $ignoreExtraKeys; protected $children; protected $prototype; protected $atLeastOne; protected $allowNewKeys; protected $key; protected $removeKeyItem; protected $addDefaults; protected $addDefaultChildren; protected $nodeBuilder; protected $normalizeKeys; /** * {@inheritDoc} */ public function __construct($name, NodeParentInterface $parent = null) { parent::__construct($name, $parent); $this->children = array(); $this->addDefaults = false; $this->addDefaultChildren = false; $this->allowNewKeys = true; $this->atLeastOne = false; $this->allowEmptyValue = true; $this->performDeepMerging = true; $this->nullEquivalent = array(); $this->trueEquivalent = array(); $this->normalizeKeys = true; } /** * Sets a custom children builder. * * @param NodeBuilder $builder A custom NodeBuilder */ public function setBuilder(NodeBuilder $builder) { $this->nodeBuilder = $builder; } /** * Returns a builder to add children nodes. * * @return NodeBuilder */ public function children() { return $this->getNodeBuilder(); } /** * Sets a prototype for child nodes. * * @param string $type the type of node * * @return NodeDefinition */ public function prototype($type) { return $this->prototype = $this->getNodeBuilder()->node(null, $type)->setParent($this); } /** * Adds the default value if the node is not set in the configuration. * * This method is applicable to concrete nodes only (not to prototype nodes). * If this function has been called and the node is not set during the finalization * phase, it's default value will be derived from its children default values. * * @return ArrayNodeDefinition */ public function addDefaultsIfNotSet() { $this->addDefaults = true; return $this; } /** * Adds children with a default value when none are defined. * * @param integer|string|array|null $children The number of children|The child name|The children names to be added * * This method is applicable to prototype nodes only. * * @return ArrayNodeDefinition */ public function addDefaultChildrenIfNoneSet($children = null) { $this->addDefaultChildren = $children; return $this; } /** * Requires the node to have at least one element. * * This method is applicable to prototype nodes only. * * @return ArrayNodeDefinition */ public function requiresAtLeastOneElement() { $this->atLeastOne = true; return $this; } /** * Disallows adding news keys in a subsequent configuration. * * If used all keys have to be defined in the same configuration file. * * @return ArrayNodeDefinition */ public function disallowNewKeysInSubsequentConfigs() { $this->allowNewKeys = false; return $this; } /** * Sets a normalization rule for XML configurations. * * @param string $singular The key to remap * @param string $plural The plural of the key for irregular plurals * * @return ArrayNodeDefinition */ public function fixXmlConfig($singular, $plural = null) { $this->normalization()->remap($singular, $plural); return $this; } /** * Sets the attribute which value is to be used as key. * * This is useful when you have an indexed array that should be an * associative array. You can select an item from within the array * to be the key of the particular item. For example, if "id" is the * "key", then: * * array( * array('id' => 'my_name', 'foo' => 'bar'), * ); * * becomes * * array( * 'my_name' => array('foo' => 'bar'), * ); * * If you'd like "'id' => 'my_name'" to still be present in the resulting * array, then you can set the second argument of this method to false. * * This method is applicable to prototype nodes only. * * @param string $name The name of the key * @param Boolean $removeKeyItem Whether or not the key item should be removed. * * @return ArrayNodeDefinition */ public function useAttributeAsKey($name, $removeKeyItem = true) { $this->key = $name; $this->removeKeyItem = $removeKeyItem; return $this; } /** * Sets whether the node can be unset. * * @param Boolean $allow * * @return ArrayNodeDefinition */ public function canBeUnset($allow = true) { $this->merge()->allowUnset($allow); return $this; } /** * Adds an "enabled" boolean to enable the current section. * * By default, the section is disabled. If any configuration is specified then * the node will be automatically enabled: * * enableableArrayNode: {enabled: true, ...} # The config is enabled & default values get overridden * enableableArrayNode: ~ # The config is enabled & use the default values * enableableArrayNode: true # The config is enabled & use the default values * enableableArrayNode: {other: value, ...} # The config is enabled & default values get overridden * enableableArrayNode: {enabled: false, ...} # The config is disabled * enableableArrayNode: false # The config is disabled * * @return ArrayNodeDefinition */ public function canBeEnabled() { $this ->addDefaultsIfNotSet() ->treatFalseLike(array('enabled' => false)) ->treatTrueLike(array('enabled' => true)) ->treatNullLike(array('enabled' => true)) ->beforeNormalization() ->ifArray() ->then(function($v) { $v['enabled'] = isset($v['enabled']) ? $v['enabled'] : true; return $v; }) ->end() ->children() ->booleanNode('enabled') ->defaultFalse() ; return $this; } /** * Adds an "enabled" boolean to enable the current section. * * By default, the section is enabled. * * @return ArrayNodeDefinition */ public function canBeDisabled() { $this ->addDefaultsIfNotSet() ->treatFalseLike(array('enabled' => false)) ->treatTrueLike(array('enabled' => true)) ->treatNullLike(array('enabled' => true)) ->children() ->booleanNode('enabled') ->defaultTrue() ; return $this; } /** * Disables the deep merging of the node. * * @return ArrayNodeDefinition */ public function performNoDeepMerging() { $this->performDeepMerging = false; return $this; } /** * Allows extra config keys to be specified under an array without * throwing an exception. * * Those config values are simply ignored. This should be used only * in special cases where you want to send an entire configuration * array through a special tree that processes only part of the array. * * @return ArrayNodeDefinition */ public function ignoreExtraKeys() { $this->ignoreExtraKeys = true; return $this; } /** * Sets key normalization. * * @param Boolean $bool Whether to enable key normalization * * @return ArrayNodeDefinition */ public function normalizeKeys($bool) { $this->normalizeKeys = (Boolean) $bool; return $this; } /** * Appends a node definition. * * $node = new ArrayNodeDefinition() * ->children() * ->scalarNode('foo')->end() * ->scalarNode('baz')->end() * ->end() * ->append($this->getBarNodeDefinition()) * ; * * @param NodeDefinition $node A NodeDefinition instance * * @return ArrayNodeDefinition This node */ public function append(NodeDefinition $node) { $this->children[$node->name] = $node->setParent($this); return $this; } /** * Returns a node builder to be used to add children and prototype * * @return NodeBuilder The node builder */ protected function getNodeBuilder() { if (null === $this->nodeBuilder) { $this->nodeBuilder = new NodeBuilder(); } return $this->nodeBuilder->setParent($this); } /** * {@inheritDoc} */ protected function createNode() { if (null === $this->prototype) { $node = new ArrayNode($this->name, $this->parent); $this->validateConcreteNode($node); $node->setAddIfNotSet($this->addDefaults); foreach ($this->children as $child) { $child->parent = $node; $node->addChild($child->getNode()); } } else { $node = new PrototypedArrayNode($this->name, $this->parent); $this->validatePrototypeNode($node); if (null !== $this->key) { $node->setKeyAttribute($this->key, $this->removeKeyItem); } if (true === $this->atLeastOne) { $node->setMinNumberOfElements(1); } if ($this->default) { $node->setDefaultValue($this->defaultValue); } if (false !== $this->addDefaultChildren) { $node->setAddChildrenIfNoneSet($this->addDefaultChildren); if ($this->prototype instanceof static && null === $this->prototype->prototype) { $this->prototype->addDefaultsIfNotSet(); } } $this->prototype->parent = $node; $node->setPrototype($this->prototype->getNode()); } $node->setAllowNewKeys($this->allowNewKeys); $node->addEquivalentValue(null, $this->nullEquivalent); $node->addEquivalentValue(true, $this->trueEquivalent); $node->addEquivalentValue(false, $this->falseEquivalent); $node->setPerformDeepMerging($this->performDeepMerging); $node->setRequired($this->required); $node->setIgnoreExtraKeys($this->ignoreExtraKeys); $node->setNormalizeKeys($this->normalizeKeys); if (null !== $this->normalization) { $node->setNormalizationClosures($this->normalization->before); $node->setXmlRemappings($this->normalization->remappings); } if (null !== $this->merge) { $node->setAllowOverwrite($this->merge->allowOverwrite); $node->setAllowFalse($this->merge->allowFalse); } if (null !== $this->validation) { $node->setFinalValidationClosures($this->validation->rules); } return $node; } /** * Validate the configuration of a concrete node. * * @param ArrayNode $node The related node * * @throws InvalidDefinitionException */ protected function validateConcreteNode(ArrayNode $node) { $path = $node->getPath(); if (null !== $this->key) { throw new InvalidDefinitionException( sprintf('->useAttributeAsKey() is not applicable to concrete nodes at path "%s"', $path) ); } if (true === $this->atLeastOne) { throw new InvalidDefinitionException( sprintf('->requiresAtLeastOneElement() is not applicable to concrete nodes at path "%s"', $path) ); } if ($this->default) { throw new InvalidDefinitionException( sprintf('->defaultValue() is not applicable to concrete nodes at path "%s"', $path) ); } if (false !== $this->addDefaultChildren) { throw new InvalidDefinitionException( sprintf('->addDefaultChildrenIfNoneSet() is not applicable to concrete nodes at path "%s"', $path) ); } } /** * Validate the configuration of a prototype node. * * @param PrototypedArrayNode $node The related node * * @throws InvalidDefinitionException */ protected function validatePrototypeNode(PrototypedArrayNode $node) { $path = $node->getPath(); if ($this->addDefaults) { throw new InvalidDefinitionException( sprintf('->addDefaultsIfNotSet() is not applicable to prototype nodes at path "%s"', $path) ); } if (false !== $this->addDefaultChildren) { if ($this->default) { throw new InvalidDefinitionException( sprintf('A default value and default children might not be used together at path "%s"', $path) ); } if (null !== $this->key && (null === $this->addDefaultChildren || is_integer($this->addDefaultChildren) && $this->addDefaultChildren > 0)) { throw new InvalidDefinitionException( sprintf('->addDefaultChildrenIfNoneSet() should set default children names as ->useAttributeAsKey() is used at path "%s"', $path) ); } if (null === $this->key && (is_string($this->addDefaultChildren) || is_array($this->addDefaultChildren))) { throw new InvalidDefinitionException( sprintf('->addDefaultChildrenIfNoneSet() might not set default children names as ->useAttributeAsKey() is not used at path "%s"', $path) ); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\BooleanNode; /** * This class provides a fluent interface for defining a node. * * @author Johannes M. Schmitt */ class BooleanNodeDefinition extends ScalarNodeDefinition { /** * {@inheritDoc} */ public function __construct($name, NodeParentInterface $parent = null) { parent::__construct($name, $parent); $this->nullEquivalent = true; } /** * Instantiate a Node * * @return BooleanNode The node */ protected function instantiateNode() { return new BooleanNode($this->name, $this->parent); } } */ class EnumNodeDefinition extends ScalarNodeDefinition { private $values; public function values(array $values) { $values = array_unique($values); if (count($values) <= 1) { throw new \InvalidArgumentException('->values() must be called with at least two distinct values.'); } $this->values = $values; return $this; } /** * Instantiate a Node * * @return EnumNode The node * * @throws \RuntimeException */ protected function instantiateNode() { if (null === $this->values) { throw new \RuntimeException('You must call ->values() on enum nodes.'); } return new EnumNode($this->name, $this->parent, $this->values); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\Exception\UnsetKeyException; /** * This class builds an if expression. * * @author Johannes M. Schmitt * @author Christophe Coevoet */ class ExprBuilder { protected $node; public $ifPart; public $thenPart; /** * Constructor * * @param NodeDefinition $node The related node */ public function __construct(NodeDefinition $node) { $this->node = $node; } /** * Marks the expression as being always used. * * @param \Closure $then * * @return ExprBuilder */ public function always(\Closure $then = null) { $this->ifPart = function($v) { return true; }; if (null !== $then) { $this->thenPart = $then; } return $this; } /** * Sets a closure to use as tests. * * The default one tests if the value is true. * * @param \Closure $closure * * @return ExprBuilder */ public function ifTrue(\Closure $closure = null) { if (null === $closure) { $closure = function($v) { return true === $v; }; } $this->ifPart = $closure; return $this; } /** * Tests if the value is a string. * * @return ExprBuilder */ public function ifString() { $this->ifPart = function($v) { return is_string($v); }; return $this; } /** * Tests if the value is null. * * @return ExprBuilder */ public function ifNull() { $this->ifPart = function($v) { return null === $v; }; return $this; } /** * Tests if the value is an array. * * @return ExprBuilder */ public function ifArray() { $this->ifPart = function($v) { return is_array($v); }; return $this; } /** * Tests if the value is in an array. * * @param array $array * * @return ExprBuilder */ public function ifInArray(array $array) { $this->ifPart = function($v) use ($array) { return in_array($v, $array, true); }; return $this; } /** * Tests if the value is not in an array. * * @param array $array * * @return ExprBuilder */ public function ifNotInArray(array $array) { $this->ifPart = function($v) use ($array) { return !in_array($v, $array, true); }; return $this; } /** * Sets the closure to run if the test pass. * * @param \Closure $closure * * @return ExprBuilder */ public function then(\Closure $closure) { $this->thenPart = $closure; return $this; } /** * Sets a closure returning an empty array. * * @return ExprBuilder */ public function thenEmptyArray() { $this->thenPart = function($v) { return array(); }; return $this; } /** * Sets a closure marking the value as invalid at validation time. * * if you want to add the value of the node in your message just use a %s placeholder. * * @param string $message * * @return ExprBuilder * * @throws \InvalidArgumentException */ public function thenInvalid($message) { $this->thenPart = function ($v) use ($message) {throw new \InvalidArgumentException(sprintf($message, json_encode($v))); }; return $this; } /** * Sets a closure unsetting this key of the array at validation time. * * @return ExprBuilder * * @throws UnsetKeyException */ public function thenUnset() { $this->thenPart = function ($v) { throw new UnsetKeyException('Unsetting key'); }; return $this; } /** * Returns the related node * * @return NodeDefinition * * @throws \RuntimeException */ public function end() { if (null === $this->ifPart) { throw new \RuntimeException('You must specify an if part.'); } if (null === $this->thenPart) { throw new \RuntimeException('You must specify a then part.'); } return $this->node; } /** * Builds the expressions. * * @param ExprBuilder[] $expressions An array of ExprBuilder instances to build * * @return array */ public static function buildExpressions(array $expressions) { foreach ($expressions as $k => $expr) { if ($expr instanceof ExprBuilder) { $expressions[$k] = function($v) use ($expr) { return call_user_func($expr->ifPart, $v) ? call_user_func($expr->thenPart, $v) : $v; }; } } return $expressions; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\FloatNode; /** * This class provides a fluent interface for defining a float node. * * @author Jeanmonod David */ class FloatNodeDefinition extends NumericNodeDefinition { /** * Instantiates a Node. * * @return FloatNode The node */ protected function instantiateNode() { return new FloatNode($this->name, $this->parent, $this->min, $this->max); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\IntegerNode; /** * This class provides a fluent interface for defining an integer node. * * @author Jeanmonod David */ class IntegerNodeDefinition extends NumericNodeDefinition { /** * Instantiates a Node. * * @return IntegerNode The node */ protected function instantiateNode() { return new IntegerNode($this->name, $this->parent, $this->min, $this->max); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; /** * This class builds merge conditions. * * @author Johannes M. Schmitt */ class MergeBuilder { protected $node; public $allowFalse; public $allowOverwrite; /** * Constructor * * @param NodeDefinition $node The related node */ public function __construct(NodeDefinition $node) { $this->node = $node; $this->allowFalse = false; $this->allowOverwrite = true; } /** * Sets whether the node can be unset. * * @param Boolean $allow * * @return MergeBuilder */ public function allowUnset($allow = true) { $this->allowFalse = $allow; return $this; } /** * Sets whether the node can be overwritten. * * @param Boolean $deny Whether the overwriting is forbidden or not * * @return MergeBuilder */ public function denyOverwrite($deny = true) { $this->allowOverwrite = !$deny; return $this; } /** * Returns the related node. * * @return NodeDefinition */ public function end() { return $this->node; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; /** * This class provides a fluent interface for building a node. * * @author Johannes M. Schmitt */ class NodeBuilder implements NodeParentInterface { protected $parent; protected $nodeMapping; /** * Constructor * */ public function __construct() { $this->nodeMapping = array( 'variable' => __NAMESPACE__.'\\VariableNodeDefinition', 'scalar' => __NAMESPACE__.'\\ScalarNodeDefinition', 'boolean' => __NAMESPACE__.'\\BooleanNodeDefinition', 'integer' => __NAMESPACE__.'\\IntegerNodeDefinition', 'float' => __NAMESPACE__.'\\FloatNodeDefinition', 'array' => __NAMESPACE__.'\\ArrayNodeDefinition', 'enum' => __NAMESPACE__.'\\EnumNodeDefinition', ); } /** * Set the parent node. * * @param ParentNodeDefinitionInterface $parent The parent node * * @return NodeBuilder This node builder */ public function setParent(ParentNodeDefinitionInterface $parent = null) { $this->parent = $parent; return $this; } /** * Creates a child array node. * * @param string $name The name of the node * * @return ArrayNodeDefinition The child node */ public function arrayNode($name) { return $this->node($name, 'array'); } /** * Creates a child scalar node. * * @param string $name the name of the node * * @return ScalarNodeDefinition The child node */ public function scalarNode($name) { return $this->node($name, 'scalar'); } /** * Creates a child Boolean node. * * @param string $name The name of the node * * @return BooleanNodeDefinition The child node */ public function booleanNode($name) { return $this->node($name, 'boolean'); } /** * Creates a child integer node. * * @param string $name the name of the node * * @return IntegerNodeDefinition The child node */ public function integerNode($name) { return $this->node($name, 'integer'); } /** * Creates a child float node. * * @param string $name the name of the node * * @return FloatNodeDefinition The child node */ public function floatNode($name) { return $this->node($name, 'float'); } /** * Creates a child EnumNode. * * @param string $name * * @return EnumNodeDefinition */ public function enumNode($name) { return $this->node($name, 'enum'); } /** * Creates a child variable node. * * @param string $name The name of the node * * @return VariableNodeDefinition The builder of the child node */ public function variableNode($name) { return $this->node($name, 'variable'); } /** * Returns the parent node. * * @return ParentNodeDefinitionInterface The parent node */ public function end() { return $this->parent; } /** * Creates a child node. * * @param string $name The name of the node * @param string $type The type of the node * * @return NodeDefinition The child node * * @throws \RuntimeException When the node type is not registered * @throws \RuntimeException When the node class is not found */ public function node($name, $type) { $class = $this->getNodeClass($type); $node = new $class($name); $this->append($node); return $node; } /** * Appends a node definition. * * Usage: * * $node = new ArrayNodeDefinition('name') * ->children() * ->scalarNode('foo')->end() * ->scalarNode('baz')->end() * ->append($this->getBarNodeDefinition()) * ->end() * ; * * @param NodeDefinition $node * * @return NodeBuilder This node builder */ public function append(NodeDefinition $node) { if ($node instanceof ParentNodeDefinitionInterface) { $builder = clone $this; $builder->setParent(null); $node->setBuilder($builder); } if (null !== $this->parent) { $this->parent->append($node); // Make this builder the node parent to allow for a fluid interface $node->setParent($this); } return $this; } /** * Adds or overrides a node Type. * * @param string $type The name of the type * @param string $class The fully qualified name the node definition class * * @return NodeBuilder This node builder */ public function setNodeClass($type, $class) { $this->nodeMapping[strtolower($type)] = $class; return $this; } /** * Returns the class name of the node definition. * * @param string $type The node type * * @return string The node definition class name * * @throws \RuntimeException When the node type is not registered * @throws \RuntimeException When the node class is not found */ protected function getNodeClass($type) { $type = strtolower($type); if (!isset($this->nodeMapping[$type])) { throw new \RuntimeException(sprintf('The node type "%s" is not registered.', $type)); } $class = $this->nodeMapping[$type]; if (!class_exists($class)) { throw new \RuntimeException(sprintf('The node class "%s" does not exist.', $class)); } return $class; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\NodeInterface; use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; /** * This class provides a fluent interface for defining a node. * * @author Johannes M. Schmitt */ abstract class NodeDefinition implements NodeParentInterface { protected $name; protected $normalization; protected $validation; protected $defaultValue; protected $default; protected $required; protected $merge; protected $allowEmptyValue; protected $nullEquivalent; protected $trueEquivalent; protected $falseEquivalent; /** * @var NodeParentInterface|NodeInterface */ protected $parent; protected $attributes = array(); /** * Constructor * * @param string $name The name of the node * @param NodeParentInterface $parent The parent */ public function __construct($name, NodeParentInterface $parent = null) { $this->parent = $parent; $this->name = $name; $this->default = false; $this->required = false; $this->trueEquivalent = true; $this->falseEquivalent = false; } /** * Sets the parent node. * * @param NodeParentInterface $parent The parent * * @return NodeDefinition */ public function setParent(NodeParentInterface $parent) { $this->parent = $parent; return $this; } /** * Sets info message. * * @param string $info The info text * * @return NodeDefinition */ public function info($info) { return $this->attribute('info', $info); } /** * Sets example configuration. * * @param string|array $example * * @return NodeDefinition */ public function example($example) { return $this->attribute('example', $example); } /** * Sets an attribute on the node. * * @param string $key * @param mixed $value * * @return NodeDefinition */ public function attribute($key, $value) { $this->attributes[$key] = $value; return $this; } /** * Returns the parent node. * * @return NodeParentInterface The builder of the parent node */ public function end() { return $this->parent; } /** * Creates the node. * * @param Boolean $forceRootNode Whether to force this node as the root node * * @return NodeInterface */ public function getNode($forceRootNode = false) { if ($forceRootNode) { $this->parent = null; } if (null !== $this->normalization) { $this->normalization->before = ExprBuilder::buildExpressions($this->normalization->before); } if (null !== $this->validation) { $this->validation->rules = ExprBuilder::buildExpressions($this->validation->rules); } $node = $this->createNode(); $node->setAttributes($this->attributes); return $node; } /** * Sets the default value. * * @param mixed $value The default value * * @return NodeDefinition */ public function defaultValue($value) { $this->default = true; $this->defaultValue = $value; return $this; } /** * Sets the node as required. * * @return NodeDefinition */ public function isRequired() { $this->required = true; return $this; } /** * Sets the equivalent value used when the node contains null. * * @param mixed $value * * @return NodeDefinition */ public function treatNullLike($value) { $this->nullEquivalent = $value; return $this; } /** * Sets the equivalent value used when the node contains true. * * @param mixed $value * * @return NodeDefinition */ public function treatTrueLike($value) { $this->trueEquivalent = $value; return $this; } /** * Sets the equivalent value used when the node contains false. * * @param mixed $value * * @return NodeDefinition */ public function treatFalseLike($value) { $this->falseEquivalent = $value; return $this; } /** * Sets null as the default value. * * @return NodeDefinition */ public function defaultNull() { return $this->defaultValue(null); } /** * Sets true as the default value. * * @return NodeDefinition */ public function defaultTrue() { return $this->defaultValue(true); } /** * Sets false as the default value. * * @return NodeDefinition */ public function defaultFalse() { return $this->defaultValue(false); } /** * Sets an expression to run before the normalization. * * @return ExprBuilder */ public function beforeNormalization() { return $this->normalization()->before(); } /** * Denies the node value being empty. * * @return NodeDefinition */ public function cannotBeEmpty() { $this->allowEmptyValue = false; return $this; } /** * Sets an expression to run for the validation. * * The expression receives the value of the node and must return it. It can * modify it. * An exception should be thrown when the node is not valid. * * @return ExprBuilder */ public function validate() { return $this->validation()->rule(); } /** * Sets whether the node can be overwritten. * * @param Boolean $deny Whether the overwriting is forbidden or not * * @return NodeDefinition */ public function cannotBeOverwritten($deny = true) { $this->merge()->denyOverwrite($deny); return $this; } /** * Gets the builder for validation rules. * * @return ValidationBuilder */ protected function validation() { if (null === $this->validation) { $this->validation = new ValidationBuilder($this); } return $this->validation; } /** * Gets the builder for merging rules. * * @return MergeBuilder */ protected function merge() { if (null === $this->merge) { $this->merge = new MergeBuilder($this); } return $this->merge; } /** * Gets the builder for normalization rules. * * @return NormalizationBuilder */ protected function normalization() { if (null === $this->normalization) { $this->normalization = new NormalizationBuilder($this); } return $this->normalization; } /** * Instantiate and configure the node according to this definition * * @return NodeInterface $node The node instance * * @throws InvalidDefinitionException When the definition is invalid */ abstract protected function createNode(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; /** * An interface that must be implemented by all node parents * * @author Victor Berchet */ interface NodeParentInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; /** * This class builds normalization conditions. * * @author Johannes M. Schmitt */ class NormalizationBuilder { protected $node; public $before; public $remappings; /** * Constructor * * @param NodeDefinition $node The related node */ public function __construct(NodeDefinition $node) { $this->node = $node; $this->keys = false; $this->remappings = array(); $this->before = array(); } /** * Registers a key to remap to its plural form. * * @param string $key The key to remap * @param string $plural The plural of the key in case of irregular plural * * @return NormalizationBuilder */ public function remap($key, $plural = null) { $this->remappings[] = array($key, null === $plural ? $key.'s' : $plural); return $this; } /** * Registers a closure to run before the normalization or an expression builder to build it if null is provided. * * @param \Closure $closure * * @return ExprBuilder|NormalizationBuilder */ public function before(\Closure $closure = null) { if (null !== $closure) { $this->before[] = $closure; return $this; } return $this->before[] = new ExprBuilder($this->node); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; /** * Abstract class that contain common code of integer and float node definition. * * @author David Jeanmonod */ abstract class NumericNodeDefinition extends ScalarNodeDefinition { protected $min; protected $max; /** * Ensures that the value is smaller than the given reference. * * @param mixed $max * * @return NumericNodeDefinition * * @throws \InvalidArgumentException when the constraint is inconsistent */ public function max($max) { if (isset($this->min) && $this->min > $max) { throw new \InvalidArgumentException(sprintf('You cannot define a max(%s) as you already have a min(%s)', $max, $this->min)); } $this->max = $max; return $this; } /** * Ensures that the value is bigger than the given reference. * * @param mixed $min * * @return NumericNodeDefinition * * @throws \InvalidArgumentException when the constraint is inconsistent */ public function min($min) { if (isset($this->max) && $this->max < $min) { throw new \InvalidArgumentException(sprintf('You cannot define a min(%s) as you already have a max(%s)', $min, $this->max)); } $this->min = $min; return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; /** * An interface that must be implemented by nodes which can have children * * @author Victor Berchet */ interface ParentNodeDefinitionInterface { public function children(); public function append(NodeDefinition $node); public function setBuilder(NodeBuilder $builder); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\ScalarNode; /** * This class provides a fluent interface for defining a node. * * @author Johannes M. Schmitt */ class ScalarNodeDefinition extends VariableNodeDefinition { /** * Instantiate a Node * * @return ScalarNode The node */ protected function instantiateNode() { return new ScalarNode($this->name, $this->parent); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\NodeInterface; /** * This is the entry class for building a config tree. * * @author Johannes M. Schmitt */ class TreeBuilder implements NodeParentInterface { protected $tree; protected $root; protected $builder; /** * Creates the root node. * * @param string $name The name of the root node * @param string $type The type of the root node * @param NodeBuilder $builder A custom node builder instance * * @return ArrayNodeDefinition|NodeDefinition The root node (as an ArrayNodeDefinition when the type is 'array') * * @throws \RuntimeException When the node type is not supported */ public function root($name, $type = 'array', NodeBuilder $builder = null) { $builder = $builder ?: new NodeBuilder(); return $this->root = $builder->node($name, $type)->setParent($this); } /** * Builds the tree. * * @return NodeInterface * * @throws \RuntimeException */ public function buildTree() { if (null === $this->root) { throw new \RuntimeException('The configuration tree has no root node.'); } if (null !== $this->tree) { return $this->tree; } return $this->tree = $this->root->getNode(true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; /** * This class builds validation conditions. * * @author Christophe Coevoet */ class ValidationBuilder { protected $node; public $rules; /** * Constructor * * @param NodeDefinition $node The related node */ public function __construct(NodeDefinition $node) { $this->node = $node; $this->rules = array(); } /** * Registers a closure to run as normalization or an expression builder to build it if null is provided. * * @param \Closure $closure * * @return ExprBuilder|ValidationBuilder */ public function rule(\Closure $closure = null) { if (null !== $closure) { $this->rules[] = $closure; return $this; } return $this->rules[] = new ExprBuilder($this->node); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Builder; use Symfony\Component\Config\Definition\VariableNode; /** * This class provides a fluent interface for defining a node. * * @author Johannes M. Schmitt */ class VariableNodeDefinition extends NodeDefinition { /** * Instantiate a Node * * @return VariableNode The node */ protected function instantiateNode() { return new VariableNode($this->name, $this->parent); } /** * {@inheritDoc} */ protected function createNode() { $node = $this->instantiateNode(); if (null !== $this->normalization) { $node->setNormalizationClosures($this->normalization->before); } if (null !== $this->merge) { $node->setAllowOverwrite($this->merge->allowOverwrite); } if (true === $this->default) { $node->setDefaultValue($this->defaultValue); } if (false === $this->allowEmptyValue) { $node->setAllowEmptyValue($this->allowEmptyValue); } $node->addEquivalentValue(null, $this->nullEquivalent); $node->addEquivalentValue(true, $this->trueEquivalent); $node->addEquivalentValue(false, $this->falseEquivalent); $node->setRequired($this->required); if (null !== $this->validation) { $node->setFinalValidationClosures($this->validation->rules); } return $node; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; /** * Configuration interface * * @author Victor Berchet */ interface ConfigurationInterface { /** * Generates the configuration tree builder. * * @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder */ public function getConfigTreeBuilder(); } */ class EnumNode extends ScalarNode { private $values; public function __construct($name, NodeInterface $parent = null, array $values = array()) { $values = array_unique($values); if (count($values) <= 1) { throw new \InvalidArgumentException('$values must contain at least two distinct elements.'); } parent::__construct($name, $parent); $this->values = $values; } public function getValues() { return $this->values; } protected function finalizeValue($value) { $value = parent::finalizeValue($value); if (!in_array($value, $this->values, true)) { $ex = new InvalidConfigurationException(sprintf( 'The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), implode(', ', array_map('json_encode', $this->values)))); $ex->setPath($this->getPath()); throw $ex; } return $value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Exception; /** * This exception is thrown whenever the key of an array is not unique. This can * only be the case if the configuration is coming from an XML file. * * @author Johannes M. Schmitt */ class DuplicateKeyException extends InvalidConfigurationException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Exception; /** * Base exception for all configuration exceptions * * @author Johannes M. Schmitt */ class Exception extends \RuntimeException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Exception; /** * This exception is thrown when a configuration path is overwritten from a * subsequent configuration file, but the entry node specifically forbids this. * * @author Johannes M. Schmitt */ class ForbiddenOverwriteException extends InvalidConfigurationException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Exception; /** * A very general exception which can be thrown whenever non of the more specific * exceptions is suitable. * * @author Johannes M. Schmitt */ class InvalidConfigurationException extends Exception { private $path; public function setPath($path) { $this->path = $path; } public function getPath() { return $this->path; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Exception; /** * Thrown when an error is detected in a node Definition. * * @author Victor Berchet */ class InvalidDefinitionException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Exception; /** * This exception is thrown if an invalid type is encountered. * * @author Johannes M. Schmitt */ class InvalidTypeException extends InvalidConfigurationException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition\Exception; /** * This exception is usually not encountered by the end-user, but only used * internally to signal the parent scope to unset a key. * * @author Johannes M. Schmitt */ class UnsetKeyException extends Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\InvalidTypeException; /** * This node represents a float value in the config tree. * * @author Jeanmonod David */ class FloatNode extends NumericNode { /** * {@inheritDoc} */ protected function validateType($value) { // Integers are also accepted, we just cast them if (is_int($value)) { $value = (float) $value; } if (!is_float($value)) { $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected float, but got %s.', $this->getPath(), gettype($value))); $ex->setPath($this->getPath()); throw $ex; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\InvalidTypeException; /** * This node represents an integer value in the config tree. * * @author Jeanmonod David */ class IntegerNode extends NumericNode { /** * {@inheritDoc} */ protected function validateType($value) { if (!is_int($value)) { $ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected int, but got %s.', $this->getPath(), gettype($value))); $ex->setPath($this->getPath()); throw $ex; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; /** * Common Interface among all nodes. * * In most cases, it is better to inherit from BaseNode instead of implementing * this interface yourself. * * @author Johannes M. Schmitt */ interface NodeInterface { /** * Returns the name of the node. * * @return string The name of the node */ public function getName(); /** * Returns the path of the node. * * @return string The node path */ public function getPath(); /** * Returns true when the node is required. * * @return Boolean If the node is required */ public function isRequired(); /** * Returns true when the node has a default value. * * @return Boolean If the node has a default value */ public function hasDefaultValue(); /** * Returns the default value of the node. * * @return mixed The default value * @throws \RuntimeException if the node has no default value */ public function getDefaultValue(); /** * Normalizes the supplied value. * * @param mixed $value The value to normalize * * @return mixed The normalized value */ public function normalize($value); /** * Merges two values together. * * @param mixed $leftSide * @param mixed $rightSide * * @return mixed The merged values */ public function merge($leftSide, $rightSide); /** * Finalizes a value. * * @param mixed $value The value to finalize * * @return mixed The finalized value */ public function finalize($value); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; /** * This node represents a numeric value in the config tree * * @author David Jeanmonod */ class NumericNode extends ScalarNode { protected $min; protected $max; public function __construct($name, NodeInterface $parent = null, $min = null, $max = null) { parent::__construct($name, $parent); $this->min = $min; $this->max = $max; } /** * {@inheritDoc} */ protected function finalizeValue($value) { $value = parent::finalizeValue($value); $errorMsg = null; if (isset($this->min) && $value < $this->min) { $errorMsg = sprintf('The value %s is too small for path "%s". Should be greater than: %s', $value, $this->getPath(), $this->min); } if (isset($this->max) && $value > $this->max) { $errorMsg = sprintf('The value %s is too big for path "%s". Should be less than: %s', $value, $this->getPath(), $this->max); } if (isset($errorMsg)) { $ex = new InvalidConfigurationException($errorMsg); $ex->setPath($this->getPath()); throw $ex; } return $value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; /** * This class is the entry point for config normalization/merging/finalization. * * @author Johannes M. Schmitt */ class Processor { /** * Processes an array of configurations. * * @param NodeInterface $configTree The node tree describing the configuration * @param array $configs An array of configuration items to process * * @return array The processed configuration */ public function process(NodeInterface $configTree, array $configs) { $currentConfig = array(); foreach ($configs as $config) { $config = $configTree->normalize($config); $currentConfig = $configTree->merge($currentConfig, $config); } return $configTree->finalize($currentConfig); } /** * Processes an array of configurations. * * @param ConfigurationInterface $configuration The configuration class * @param array $configs An array of configuration items to process * * @return array The processed configuration */ public function processConfiguration(ConfigurationInterface $configuration, array $configs) { return $this->process($configuration->getConfigTreeBuilder()->buildTree(), $configs); } /** * Normalizes a configuration entry. * * This method returns a normalize configuration array for a given key * to remove the differences due to the original format (YAML and XML mainly). * * Here is an example. * * The configuration in XML: * * twig.extension.foo * twig.extension.bar * * And the same configuration in YAML: * * extensions: ['twig.extension.foo', 'twig.extension.bar'] * * @param array $config A config array * @param string $key The key to normalize * @param string $plural The plural form of the key if it is irregular * * @return array */ public static function normalizeConfig($config, $key, $plural = null) { if (null === $plural) { $plural = $key.'s'; } if (isset($config[$plural])) { return $config[$plural]; } if (isset($config[$key])) { if (is_string($config[$key]) || !is_int(key($config[$key]))) { // only one return array($config[$key]); } return $config[$key]; } return array(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Exception\DuplicateKeyException; use Symfony\Component\Config\Definition\Exception\UnsetKeyException; use Symfony\Component\Config\Definition\Exception\Exception; /** * Represents a prototyped Array node in the config tree. * * @author Johannes M. Schmitt */ class PrototypedArrayNode extends ArrayNode { protected $prototype; protected $keyAttribute; protected $removeKeyAttribute; protected $minNumberOfElements; protected $defaultValue; protected $defaultChildren; /** * Constructor. * * @param string $name The Node's name * @param NodeInterface $parent The node parent */ public function __construct($name, NodeInterface $parent = null) { parent::__construct($name, $parent); $this->minNumberOfElements = 0; $this->defaultValue = array(); } /** * Sets the minimum number of elements that a prototype based node must * contain. By default this is zero, meaning no elements. * * @param integer $number */ public function setMinNumberOfElements($number) { $this->minNumberOfElements = $number; } /** * Sets the attribute which value is to be used as key. * * This is useful when you have an indexed array that should be an * associative array. You can select an item from within the array * to be the key of the particular item. For example, if "id" is the * "key", then: * * array( * array('id' => 'my_name', 'foo' => 'bar'), * ); * * becomes * * array( * 'my_name' => array('foo' => 'bar'), * ); * * If you'd like "'id' => 'my_name'" to still be present in the resulting * array, then you can set the second argument of this method to false. * * @param string $attribute The name of the attribute which value is to be used as a key * @param Boolean $remove Whether or not to remove the key */ public function setKeyAttribute($attribute, $remove = true) { $this->keyAttribute = $attribute; $this->removeKeyAttribute = $remove; } /** * Retrieves the name of the attribute which value should be used as key. * * @return string The name of the attribute */ public function getKeyAttribute() { return $this->keyAttribute; } /** * Sets the default value of this node. * * @param string $value * * @throws \InvalidArgumentException if the default value is not an array */ public function setDefaultValue($value) { if (!is_array($value)) { throw new \InvalidArgumentException($this->getPath().': the default value of an array node has to be an array.'); } $this->defaultValue = $value; } /** * Checks if the node has a default value. * * @return Boolean */ public function hasDefaultValue() { return true; } /** * Adds default children when none are set. * * @param integer|string|array|null $children The number of children|The child name|The children names to be added */ public function setAddChildrenIfNoneSet($children = array('defaults')) { if (null === $children) { $this->defaultChildren = array('defaults'); } else { $this->defaultChildren = is_integer($children) && $children > 0 ? range(1, $children) : (array) $children; } } /** * Retrieves the default value. * * The default value could be either explicited or derived from the prototype * default value. * * @return array The default value */ public function getDefaultValue() { if (null !== $this->defaultChildren) { $default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : array(); $defaults = array(); foreach (array_values($this->defaultChildren) as $i => $name) { $defaults[null === $this->keyAttribute ? $i : $name] = $default; } return $defaults; } return $this->defaultValue; } /** * Sets the node prototype. * * @param PrototypeNodeInterface $node */ public function setPrototype(PrototypeNodeInterface $node) { $this->prototype = $node; } /** * Retrieves the prototype * * @return PrototypeNodeInterface The prototype */ public function getPrototype() { return $this->prototype; } /** * Disable adding concrete children for prototyped nodes. * * @param NodeInterface $node The child node to add * * @throws Exception */ public function addChild(NodeInterface $node) { throw new Exception('A prototyped array node can not have concrete children.'); } /** * Finalizes the value of this node. * * @param mixed $value * * @return mixed The finalized value * * @throws UnsetKeyException * @throws InvalidConfigurationException if the node doesn't have enough children */ protected function finalizeValue($value) { if (false === $value) { $msg = sprintf('Unsetting key for path "%s", value: %s', $this->getPath(), json_encode($value)); throw new UnsetKeyException($msg); } foreach ($value as $k => $v) { $this->prototype->setName($k); try { $value[$k] = $this->prototype->finalize($v); } catch (UnsetKeyException $unset) { unset($value[$k]); } } if (count($value) < $this->minNumberOfElements) { $msg = sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements); $ex = new InvalidConfigurationException($msg); $ex->setPath($this->getPath()); throw $ex; } return $value; } /** * Normalizes the value. * * @param mixed $value The value to normalize * * @return mixed The normalized value * * @throws InvalidConfigurationException * @throws DuplicateKeyException */ protected function normalizeValue($value) { if (false === $value) { return $value; } $value = $this->remapXml($value); $isAssoc = array_keys($value) !== range(0, count($value) -1); $normalized = array(); foreach ($value as $k => $v) { if (null !== $this->keyAttribute && is_array($v)) { if (!isset($v[$this->keyAttribute]) && is_int($k) && !$isAssoc) { $msg = sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath()); $ex = new InvalidConfigurationException($msg); $ex->setPath($this->getPath()); throw $ex; } elseif (isset($v[$this->keyAttribute])) { $k = $v[$this->keyAttribute]; // remove the key attribute when required if ($this->removeKeyAttribute) { unset($v[$this->keyAttribute]); } // if only "value" is left if (1 == count($v) && isset($v['value'])) { $v = $v['value']; } } if (array_key_exists($k, $normalized)) { $msg = sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath()); $ex = new DuplicateKeyException($msg); $ex->setPath($this->getPath()); throw $ex; } } $this->prototype->setName($k); if (null !== $this->keyAttribute || $isAssoc) { $normalized[$k] = $this->prototype->normalize($v); } else { $normalized[] = $this->prototype->normalize($v); } } return $normalized; } /** * Merges values together. * * @param mixed $leftSide The left side to merge. * @param mixed $rightSide The right side to merge. * * @return mixed The merged values * * @throws InvalidConfigurationException * @throws \RuntimeException */ protected function mergeValues($leftSide, $rightSide) { if (false === $rightSide) { // if this is still false after the last config has been merged the // finalization pass will take care of removing this key entirely return false; } if (false === $leftSide || !$this->performDeepMerging) { return $rightSide; } foreach ($rightSide as $k => $v) { // prototype, and key is irrelevant, so simply append the element if (null === $this->keyAttribute) { $leftSide[] = $v; continue; } // no conflict if (!array_key_exists($k, $leftSide)) { if (!$this->allowNewKeys) { $ex = new InvalidConfigurationException(sprintf( 'You are not allowed to define new elements for path "%s". ' . 'Please define all elements for this path in one config file.', $this->getPath() )); $ex->setPath($this->getPath()); throw $ex; } $leftSide[$k] = $v; continue; } $this->prototype->setName($k); $leftSide[$k] = $this->prototype->merge($leftSide[$k], $v); } return $leftSide; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; /** * This interface must be implemented by nodes which can be used as prototypes. * * @author Johannes M. Schmitt */ interface PrototypeNodeInterface extends NodeInterface { /** * Sets the name of the node. * * @param string $name The name of the node */ public function setName($name); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; /** * Dumps a reference configuration for the given configuration/node instance. * * Currently, only YML format is supported. * * @author Kevin Bond */ class ReferenceDumper { private $reference; public function dump(ConfigurationInterface $configuration) { return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree()); } public function dumpNode(NodeInterface $node) { $this->reference = ''; $this->writeNode($node); $ref = $this->reference; $this->reference = null; return $ref; } /** * @param NodeInterface $node * @param integer $depth */ private function writeNode(NodeInterface $node, $depth = 0) { $comments = array(); $default = ''; $defaultArray = null; $children = null; $example = $node->getExample(); // defaults if ($node instanceof ArrayNode) { $children = $node->getChildren(); if ($node instanceof PrototypedArrayNode) { $prototype = $node->getPrototype(); if ($prototype instanceof ArrayNode) { $children = $prototype->getChildren(); } // check for attribute as key if ($key = $node->getKeyAttribute()) { $keyNode = new ArrayNode($key, $node); $keyNode->setInfo('Prototype'); // add children foreach ($children as $childNode) { $keyNode->addChild($childNode); } $children = array($key => $keyNode); } } if (!$children) { if ($node->hasDefaultValue() && count($defaultArray = $node->getDefaultValue())) { $default = ''; } elseif (!is_array($example)) { $default = '[]'; } } } else { $default = '~'; if ($node->hasDefaultValue()) { $default = $node->getDefaultValue(); if (true === $default) { $default = 'true'; } elseif (false === $default) { $default = 'false'; } elseif (null === $default) { $default = '~'; } elseif (is_array($default)) { if ($node->hasDefaultValue() && count($defaultArray = $node->getDefaultValue())) { $default = ''; } elseif (!is_array($example)) { $default = '[]'; } } } } // required? if ($node->isRequired()) { $comments[] = 'Required'; } // example if ($example && !is_array($example)) { $comments[] = 'Example: '.$example; } $default = (string) $default != '' ? ' '.$default : ''; $comments = count($comments) ? '# '.implode(', ', $comments) : ''; $text = rtrim(sprintf('%-20s %s %s', $node->getName() . ':', $default, $comments), ' '); if ($info = $node->getInfo()) { $this->writeLine(''); // indenting multi-line info $info = str_replace("\n", sprintf("\n%" . $depth * 4 . "s# ", ' '), $info); $this->writeLine('# '.$info, $depth * 4); } $this->writeLine($text, $depth * 4); // output defaults if ($defaultArray) { $this->writeLine(''); $message = count($defaultArray) > 1 ? 'Defaults' : 'Default'; $this->writeLine('# '.$message.':', $depth * 4 + 4); $this->writeArray($defaultArray, $depth + 1); } if (is_array($example)) { $this->writeLine(''); $message = count($example) > 1 ? 'Examples' : 'Example'; $this->writeLine('# '.$message.':', $depth * 4 + 4); $this->writeArray($example, $depth + 1); } if ($children) { foreach ($children as $childNode) { $this->writeNode($childNode, $depth + 1); } } } /** * Outputs a single config reference line * * @param string $text * @param int $indent */ private function writeLine($text, $indent = 0) { $indent = strlen($text) + $indent; $format = '%'.$indent.'s'; $this->reference .= sprintf($format, $text)."\n"; } private function writeArray(array $array, $depth) { $isIndexed = array_values($array) === $array; foreach ($array as $key => $value) { if (is_array($value)) { $val = ''; } else { $val = $value; } if ($isIndexed) { $this->writeLine('- '.$val, $depth * 4); } else { $this->writeLine(sprintf('%-20s %s', $key.':', $val), $depth * 4); } if (is_array($value)) { $this->writeArray($value, $depth + 1); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\VariableNode; use Symfony\Component\Config\Definition\Exception\InvalidTypeException; /** * This node represents a scalar value in the config tree. * * The following values are considered scalars: * * booleans * * strings * * null * * integers * * floats * * @author Johannes M. Schmitt */ class ScalarNode extends VariableNode { /** * {@inheritDoc} */ protected function validateType($value) { if (!is_scalar($value) && null !== $value) { $ex = new InvalidTypeException(sprintf( 'Invalid type for path "%s". Expected scalar, but got %s.', $this->getPath(), gettype($value) )); $ex->setPath($this->getPath()); throw $ex; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Definition; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; /** * This node represents a value of variable type in the config tree. * * This node is intended for values of arbitrary type. * Any PHP type is accepted as a value. * * @author Jeremy Mikola */ class VariableNode extends BaseNode implements PrototypeNodeInterface { protected $defaultValueSet = false; protected $defaultValue; protected $allowEmptyValue = true; /** * {@inheritDoc} */ public function setDefaultValue($value) { $this->defaultValueSet = true; $this->defaultValue = $value; } /** * {@inheritDoc} */ public function hasDefaultValue() { return $this->defaultValueSet; } /** * {@inheritDoc} */ public function getDefaultValue() { return $this->defaultValue instanceof \Closure ? call_user_func($this->defaultValue) : $this->defaultValue; } /** * Sets if this node is allowed to have an empty value. * * @param Boolean $boolean True if this entity will accept empty values. */ public function setAllowEmptyValue($boolean) { $this->allowEmptyValue = (Boolean) $boolean; } /** * {@inheritDoc} */ public function setName($name) { $this->name = $name; } /** * {@inheritDoc} */ protected function validateType($value) { } /** * {@inheritDoc} */ protected function finalizeValue($value) { if (!$this->allowEmptyValue && empty($value)) { $ex = new InvalidConfigurationException(sprintf( 'The path "%s" cannot contain an empty value, but got %s.', $this->getPath(), json_encode($value) )); $ex->setPath($this->getPath()); throw $ex; } return $value; } /** * {@inheritDoc} */ protected function normalizeValue($value) { return $value; } /** * {@inheritDoc} */ protected function mergeValues($leftSide, $rightSide) { return $rightSide; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Exception; /** * Exception class for when a circular reference is detected when importing resources. * * @author Fabien Potencier */ class FileLoaderImportCircularReferenceException extends FileLoaderLoadException { public function __construct(array $resources, $code = null, $previous = null) { $message = sprintf('Circular reference detected in "%s" ("%s" > "%s").', $this->varToString($resources[0]), implode('" > "', $resources), $resources[0]); call_user_func('Exception::__construct', $message, $code, $previous); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Exception; /** * Exception class for when a resource cannot be loaded or imported. * * @author Ryan Weaver */ class FileLoaderLoadException extends \Exception { /** * @param string $resource The resource that could not be imported * @param string $sourceResource The original resource importing the new resource * @param integer $code The error code * @param \Exception $previous A previous exception */ public function __construct($resource, $sourceResource = null, $code = null, $previous = null) { if (null === $sourceResource) { $message = sprintf('Cannot load resource "%s".', $this->varToString($resource)); } else { $message = sprintf('Cannot import resource "%s" from "%s".', $this->varToString($resource), $this->varToString($sourceResource)); } // Is the resource located inside a bundle? if ('@' === $resource[0]) { $parts = explode(DIRECTORY_SEPARATOR, $resource); $bundle = substr($parts[0], 1); $message .= ' '.sprintf('Make sure the "%s" bundle is correctly registered and loaded in the application kernel class.', $bundle); } parent::__construct($message, $code, $previous); } protected function varToString($var) { if (is_object($var)) { return sprintf('Object(%s)', get_class($var)); } if (is_array($var)) { $a = array(); foreach ($var as $k => $v) { $a[] = sprintf('%s => %s', $k, $this->varToString($v)); } return sprintf("Array(%s)", implode(', ', $a)); } if (is_resource($var)) { return sprintf('Resource(%s)', get_resource_type($var)); } if (null === $var) { return 'null'; } if (false === $var) { return 'false'; } if (true === $var) { return 'true'; } return (string) $var; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config; /** * FileLocator uses an array of pre-defined paths to find files. * * @author Fabien Potencier */ class FileLocator implements FileLocatorInterface { protected $paths; /** * Constructor. * * @param string|array $paths A path or an array of paths where to look for resources */ public function __construct($paths = array()) { $this->paths = (array) $paths; } /** * Returns a full path for a given file name. * * @param mixed $name The file name to locate * @param string $currentPath The current path * @param Boolean $first Whether to return the first occurrence or an array of filenames * * @return string|array The full path to the file|An array of file paths * * @throws \InvalidArgumentException When file is not found */ public function locate($name, $currentPath = null, $first = true) { if ($this->isAbsolutePath($name)) { if (!file_exists($name)) { throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $name)); } return $name; } $filepaths = array(); if (null !== $currentPath && file_exists($file = $currentPath.DIRECTORY_SEPARATOR.$name)) { if (true === $first) { return $file; } $filepaths[] = $file; } foreach ($this->paths as $path) { if (file_exists($file = $path.DIRECTORY_SEPARATOR.$name)) { if (true === $first) { return $file; } $filepaths[] = $file; } } if (!$filepaths) { throw new \InvalidArgumentException(sprintf('The file "%s" does not exist (in: %s%s).', $name, null !== $currentPath ? $currentPath.', ' : '', implode(', ', $this->paths))); } return array_values(array_unique($filepaths)); } /** * Returns whether the file path is an absolute path. * * @param string $file A file path * * @return Boolean */ private function isAbsolutePath($file) { if ($file[0] == '/' || $file[0] == '\\' || (strlen($file) > 3 && ctype_alpha($file[0]) && $file[1] == ':' && ($file[2] == '\\' || $file[2] == '/') ) || null !== parse_url($file, PHP_URL_SCHEME) ) { return true; } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config; /** * @author Fabien Potencier */ interface FileLocatorInterface { /** * Returns a full path for a given file name. * * @param mixed $name The file name to locate * @param string $currentPath The current path * @param Boolean $first Whether to return the first occurrence or an array of filenames * * @return string|array The full path to the file|An array of file paths * * @throws \InvalidArgumentException When file is not found */ public function locate($name, $currentPath = null, $first = true); } Copyright (c) 2004-2013 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Loader; use Symfony\Component\Config\Exception\FileLoaderLoadException; /** * DelegatingLoader delegates loading to other loaders using a loader resolver. * * This loader acts as an array of LoaderInterface objects - each having * a chance to load a given resource (handled by the resolver) * * @author Fabien Potencier */ class DelegatingLoader extends Loader { /** * Constructor. * * @param LoaderResolverInterface $resolver A LoaderResolverInterface instance */ public function __construct(LoaderResolverInterface $resolver) { $this->resolver = $resolver; } /** * Loads a resource. * * @param mixed $resource A resource * @param string $type The resource type * * @return mixed * * @throws FileLoaderLoadException if no loader is found. */ public function load($resource, $type = null) { if (false === $loader = $this->resolver->resolve($resource, $type)) { throw new FileLoaderLoadException($resource); } return $loader->load($resource, $type); } /** * {@inheritdoc} */ public function supports($resource, $type = null) { return false === $this->resolver->resolve($resource, $type) ? false : true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Loader; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Config\Exception\FileLoaderLoadException; use Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException; /** * FileLoader is the abstract class used by all built-in loaders that are file based. * * @author Fabien Potencier */ abstract class FileLoader extends Loader { protected static $loading = array(); protected $locator; private $currentDir; /** * Constructor. * * @param FileLocatorInterface $locator A FileLocatorInterface instance */ public function __construct(FileLocatorInterface $locator) { $this->locator = $locator; } public function setCurrentDir($dir) { $this->currentDir = $dir; } public function getLocator() { return $this->locator; } /** * Imports a resource. * * @param mixed $resource A Resource * @param string $type The resource type * @param Boolean $ignoreErrors Whether to ignore import errors or not * @param string $sourceResource The original resource importing the new resource * * @return mixed * * @throws FileLoaderLoadException * @throws FileLoaderImportCircularReferenceException */ public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null) { try { $loader = $this->resolve($resource, $type); if ($loader instanceof FileLoader && null !== $this->currentDir) { $resource = $this->locator->locate($resource, $this->currentDir); } if (isset(self::$loading[$resource])) { throw new FileLoaderImportCircularReferenceException(array_keys(self::$loading)); } self::$loading[$resource] = true; $ret = $loader->load($resource, $type); unset(self::$loading[$resource]); return $ret; } catch (FileLoaderImportCircularReferenceException $e) { throw $e; } catch (\Exception $e) { if (!$ignoreErrors) { // prevent embedded imports from nesting multiple exceptions if ($e instanceof FileLoaderLoadException) { throw $e; } throw new FileLoaderLoadException($resource, $sourceResource, null, $e); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Loader; use Symfony\Component\Config\Exception\FileLoaderLoadException; /** * Loader is the abstract class used by all built-in loaders. * * @author Fabien Potencier */ abstract class Loader implements LoaderInterface { protected $resolver; /** * Gets the loader resolver. * * @return LoaderResolverInterface A LoaderResolverInterface instance */ public function getResolver() { return $this->resolver; } /** * Sets the loader resolver. * * @param LoaderResolverInterface $resolver A LoaderResolverInterface instance */ public function setResolver(LoaderResolverInterface $resolver) { $this->resolver = $resolver; } /** * Imports a resource. * * @param mixed $resource A Resource * @param string $type The resource type * * @return mixed */ public function import($resource, $type = null) { return $this->resolve($resource)->load($resource, $type); } /** * Finds a loader able to load an imported resource. * * @param mixed $resource A Resource * @param string $type The resource type * * @return LoaderInterface A LoaderInterface instance * * @throws FileLoaderLoadException if no loader is found */ public function resolve($resource, $type = null) { if ($this->supports($resource, $type)) { return $this; } $loader = null === $this->resolver ? false : $this->resolver->resolve($resource, $type); if (false === $loader) { throw new FileLoaderLoadException($resource); } return $loader; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Loader; /** * LoaderInterface is the interface implemented by all loader classes. * * @author Fabien Potencier */ interface LoaderInterface { /** * Loads a resource. * * @param mixed $resource The resource * @param string $type The resource type */ public function load($resource, $type = null); /** * Returns true if this class supports the given resource. * * @param mixed $resource A resource * @param string $type The resource type * * @return Boolean true if this class supports the given resource, false otherwise */ public function supports($resource, $type = null); /** * Gets the loader resolver. * * @return LoaderResolverInterface A LoaderResolverInterface instance */ public function getResolver(); /** * Sets the loader resolver. * * @param LoaderResolverInterface $resolver A LoaderResolverInterface instance */ public function setResolver(LoaderResolverInterface $resolver); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Loader; /** * LoaderResolver selects a loader for a given resource. * * A resource can be anything (e.g. a full path to a config file or a Closure). * Each loader determines whether it can load a resource and how. * * @author Fabien Potencier */ class LoaderResolver implements LoaderResolverInterface { /** * @var LoaderInterface[] An array of LoaderInterface objects */ private $loaders; /** * Constructor. * * @param LoaderInterface[] $loaders An array of loaders */ public function __construct(array $loaders = array()) { $this->loaders = array(); foreach ($loaders as $loader) { $this->addLoader($loader); } } /** * Returns a loader able to load the resource. * * @param mixed $resource A resource * @param string $type The resource type * * @return LoaderInterface|false A LoaderInterface instance */ public function resolve($resource, $type = null) { foreach ($this->loaders as $loader) { if ($loader->supports($resource, $type)) { return $loader; } } return false; } /** * Adds a loader. * * @param LoaderInterface $loader A LoaderInterface instance */ public function addLoader(LoaderInterface $loader) { $this->loaders[] = $loader; $loader->setResolver($this); } /** * Returns the registered loaders. * * @return LoaderInterface[] An array of LoaderInterface instances */ public function getLoaders() { return $this->loaders; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Loader; /** * LoaderResolverInterface selects a loader for a given resource. * * @author Fabien Potencier */ interface LoaderResolverInterface { /** * Returns a loader able to load the resource. * * @param mixed $resource A resource * @param string $type The resource type * * @return LoaderInterface A LoaderInterface instance */ public function resolve($resource, $type = null); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Resource; /** * DirectoryResource represents a resources stored in a subdirectory tree. * * @author Fabien Potencier */ class DirectoryResource implements ResourceInterface, \Serializable { private $resource; private $pattern; /** * Constructor. * * @param string $resource The file path to the resource * @param string $pattern A pattern to restrict monitored files */ public function __construct($resource, $pattern = null) { $this->resource = $resource; $this->pattern = $pattern; } /** * Returns a string representation of the Resource. * * @return string A string representation of the Resource */ public function __toString() { return (string) $this->resource; } /** * Returns the resource tied to this Resource. * * @return mixed The resource */ public function getResource() { return $this->resource; } public function getPattern() { return $this->pattern; } /** * Returns true if the resource has not been updated since the given timestamp. * * @param integer $timestamp The last time the resource was loaded * * @return Boolean true if the resource has not been updated, false otherwise */ public function isFresh($timestamp) { if (!is_dir($this->resource)) { return false; } $newestMTime = filemtime($this->resource); foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->resource), \RecursiveIteratorIterator::SELF_FIRST) as $file) { // if regex filtering is enabled only check matching files if ($this->pattern && $file->isFile() && !preg_match($this->pattern, $file->getBasename())) { continue; } // always monitor directories for changes, except the .. entries // (otherwise deleted files wouldn't get detected) if ($file->isDir() && '/..' === substr($file, -3)) { continue; } $newestMTime = max($file->getMTime(), $newestMTime); } return $newestMTime < $timestamp; } public function serialize() { return serialize(array($this->resource, $this->pattern)); } public function unserialize($serialized) { list($this->resource, $this->pattern) = unserialize($serialized); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Resource; /** * FileResource represents a resource stored on the filesystem. * * The resource can be a file or a directory. * * @author Fabien Potencier */ class FileResource implements ResourceInterface, \Serializable { private $resource; /** * Constructor. * * @param string $resource The file path to the resource */ public function __construct($resource) { $this->resource = realpath($resource); } /** * Returns a string representation of the Resource. * * @return string A string representation of the Resource */ public function __toString() { return (string) $this->resource; } /** * Returns the resource tied to this Resource. * * @return mixed The resource */ public function getResource() { return $this->resource; } /** * Returns true if the resource has not been updated since the given timestamp. * * @param integer $timestamp The last time the resource was loaded * * @return Boolean true if the resource has not been updated, false otherwise */ public function isFresh($timestamp) { if (!file_exists($this->resource)) { return false; } return filemtime($this->resource) < $timestamp; } public function serialize() { return serialize($this->resource); } public function unserialize($serialized) { $this->resource = unserialize($serialized); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Resource; /** * ResourceInterface is the interface that must be implemented by all Resource classes. * * @author Fabien Potencier */ interface ResourceInterface { /** * Returns a string representation of the Resource. * * @return string A string representation of the Resource */ public function __toString(); /** * Returns true if the resource has not been updated since the given timestamp. * * @param integer $timestamp The last time the resource was loaded * * @return Boolean true if the resource has not been updated, false otherwise */ public function isFresh($timestamp); /** * Returns the resource tied to this Resource. * * @return mixed The resource */ public function getResource(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Config\Util; /** * XMLUtils is a bunch of utility methods to XML operations. * * This class contains static methods only and is not meant to be instantiated. * * @author Fabien Potencier * @author Martin Hasoň */ class XmlUtils { /** * This class should not be instantiated */ private function __construct() { } /** * Loads an XML file. * * @param string $file An XML file path * @param string|callable $schemaOrCallable An XSD schema file path or callable * * @return \DOMDocument * * @throws \InvalidArgumentException When loading of XML file returns error */ public static function loadFile($file, $schemaOrCallable = null) { $internalErrors = libxml_use_internal_errors(true); $disableEntities = libxml_disable_entity_loader(true); libxml_clear_errors(); $dom = new \DOMDocument(); $dom->validateOnParse = true; if (!$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) { libxml_disable_entity_loader($disableEntities); throw new \InvalidArgumentException(implode("\n", static::getXmlErrors($internalErrors))); } $dom->normalizeDocument(); libxml_use_internal_errors($internalErrors); libxml_disable_entity_loader($disableEntities); foreach ($dom->childNodes as $child) { if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { throw new \InvalidArgumentException('Document types are not allowed.'); } } if (null !== $schemaOrCallable) { $internalErrors = libxml_use_internal_errors(true); libxml_clear_errors(); $e = null; if (is_callable($schemaOrCallable)) { try { $valid = call_user_func($schemaOrCallable, $dom, $internalErrors); } catch (\Exception $e) { $valid = false; } } elseif (!is_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) { $valid = @$dom->schemaValidate($schemaOrCallable); } else { libxml_use_internal_errors($internalErrors); throw new \InvalidArgumentException('The schemaOrCallable argument has to be a valid path to XSD file or callable.'); } if (!$valid) { $messages = static::getXmlErrors($internalErrors); if (empty($messages)) { $messages = array(sprintf('The XML file "%s" is not valid.', $file)); } throw new \InvalidArgumentException(implode("\n", $messages), 0, $e); } libxml_use_internal_errors($internalErrors); } return $dom; } /** * Converts a \DomElement object to a PHP array. * * The following rules applies during the conversion: * * * Each tag is converted to a key value or an array * if there is more than one "value" * * * The content of a tag is set under a "value" key (bar) * if the tag also has some nested tags * * * The attributes are converted to keys () * * * The nested-tags are converted to keys (bar) * * @param \DomElement $element A \DomElement instance * @param Boolean $checkPrefix Check prefix in an element or an attribute name * * @return array A PHP array */ public static function convertDomElementToArray(\DomElement $element, $checkPrefix = true) { $prefix = (string) $element->prefix; $empty = true; $config = array(); foreach ($element->attributes as $name => $node) { if ($checkPrefix && !in_array((string) $node->prefix, array('', $prefix), true)) { continue; } $config[$name] = static::phpize($node->value); $empty = false; } $nodeValue = false; foreach ($element->childNodes as $node) { if ($node instanceof \DOMText) { if (trim($node->nodeValue)) { $nodeValue = trim($node->nodeValue); $empty = false; } } elseif ($checkPrefix && $prefix != (string) $node->prefix) { continue; } elseif (!$node instanceof \DOMComment) { $value = static::convertDomElementToArray($node, $checkPrefix); $key = $node->localName; if (isset($config[$key])) { if (!is_array($config[$key]) || !is_int(key($config[$key]))) { $config[$key] = array($config[$key]); } $config[$key][] = $value; } else { $config[$key] = $value; } $empty = false; } } if (false !== $nodeValue) { $value = static::phpize($nodeValue); if (count($config)) { $config['value'] = $value; } else { $config = $value; } } return !$empty ? $config : null; } /** * Converts an xml value to a php type. * * @param mixed $value * * @return mixed */ public static function phpize($value) { $value = (string) $value; $lowercaseValue = strtolower($value); switch (true) { case 'null' === $lowercaseValue: return null; case ctype_digit($value): $raw = $value; $cast = intval($value); return '0' == $value[0] ? octdec($value) : (((string) $raw == (string) $cast) ? $cast : $raw); case 'true' === $lowercaseValue: return true; case 'false' === $lowercaseValue: return false; case is_numeric($value): return '0x' == $value[0].$value[1] ? hexdec($value) : floatval($value); case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $value): return floatval(str_replace(',', '', $value)); default: return $value; } } protected static function getXmlErrors($internalErrors) { $errors = array(); foreach (libxml_get_errors() as $error) { $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', $error->code, trim($error->message), $error->file ? $error->file : 'n/a', $error->line, $error->column ); } libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return $errors; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Helper\ProgressHelper; /** * An Application is the container for a collection of commands. * * It is the main entry point of a Console application. * * This class is optimized for a standard CLI environment. * * Usage: * * $app = new Application('myapp', '1.0 (stable)'); * $app->add(new SimpleCommand()); * $app->run(); * * @author Fabien Potencier * * @api */ class Application { private $commands; private $wantHelps = false; private $runningCommand; private $name; private $version; private $catchExceptions; private $autoExit; private $definition; private $helperSet; /** * Constructor. * * @param string $name The name of the application * @param string $version The version of the application * * @api */ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { $this->name = $name; $this->version = $version; $this->catchExceptions = true; $this->autoExit = true; $this->commands = array(); $this->helperSet = $this->getDefaultHelperSet(); $this->definition = $this->getDefaultInputDefinition(); foreach ($this->getDefaultCommands() as $command) { $this->add($command); } } /** * Runs the current application. * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return integer 0 if everything went fine, or an error code * * @throws \Exception When doRun returns Exception * * @api */ public function run(InputInterface $input = null, OutputInterface $output = null) { if (null === $input) { $input = new ArgvInput(); } if (null === $output) { $output = new ConsoleOutput(); } try { $statusCode = $this->doRun($input, $output); } catch (\Exception $e) { if (!$this->catchExceptions) { throw $e; } if ($output instanceof ConsoleOutputInterface) { $this->renderException($e, $output->getErrorOutput()); } else { $this->renderException($e, $output); } $statusCode = $e->getCode(); $statusCode = is_numeric($statusCode) && $statusCode ? $statusCode : 1; } if ($this->autoExit) { if ($statusCode > 255) { $statusCode = 255; } // @codeCoverageIgnoreStart exit($statusCode); // @codeCoverageIgnoreEnd } return $statusCode; } /** * Runs the current application. * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return integer 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output) { $name = $this->getCommandName($input); if (true === $input->hasParameterOption(array('--ansi'))) { $output->setDecorated(true); } elseif (true === $input->hasParameterOption(array('--no-ansi'))) { $output->setDecorated(false); } if (true === $input->hasParameterOption(array('--help', '-h'))) { if (!$name) { $name = 'help'; $input = new ArrayInput(array('command' => 'help')); } else { $this->wantHelps = true; } } if (true === $input->hasParameterOption(array('--no-interaction', '-n'))) { $input->setInteractive(false); } if (function_exists('posix_isatty') && $this->getHelperSet()->has('dialog')) { $inputStream = $this->getHelperSet()->get('dialog')->getInputStream(); if (!posix_isatty($inputStream)) { $input->setInteractive(false); } } if (true === $input->hasParameterOption(array('--quiet', '-q'))) { $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); } elseif (true === $input->hasParameterOption(array('--verbose', '-v'))) { $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); } if (true === $input->hasParameterOption(array('--version', '-V'))) { $output->writeln($this->getLongVersion()); return 0; } if (!$name) { $name = 'list'; $input = new ArrayInput(array('command' => 'list')); } // the command name MUST be the first element of the input $command = $this->find($name); $this->runningCommand = $command; $statusCode = $command->run($input, $output); $this->runningCommand = null; return is_numeric($statusCode) ? $statusCode : 0; } /** * Set a helper set to be used with the command. * * @param HelperSet $helperSet The helper set * * @api */ public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; } /** * Get the helper set associated with the command. * * @return HelperSet The HelperSet instance associated with this command * * @api */ public function getHelperSet() { return $this->helperSet; } /** * Set an input definition set to be used with this application * * @param InputDefinition $definition The input definition * * @api */ public function setDefinition(InputDefinition $definition) { $this->definition = $definition; } /** * Gets the InputDefinition related to this Application. * * @return InputDefinition The InputDefinition instance */ public function getDefinition() { return $this->definition; } /** * Gets the help message. * * @return string A help message. */ public function getHelp() { $messages = array( $this->getLongVersion(), '', 'Usage:', ' [options] command [arguments]', '', 'Options:', ); foreach ($this->getDefinition()->getOptions() as $option) { $messages[] = sprintf(' %-29s %s %s', '--'.$option->getName().'', $option->getShortcut() ? '-'.$option->getShortcut().'' : ' ', $option->getDescription() ); } return implode(PHP_EOL, $messages); } /** * Sets whether to catch exceptions or not during commands execution. * * @param Boolean $boolean Whether to catch exceptions or not during commands execution * * @api */ public function setCatchExceptions($boolean) { $this->catchExceptions = (Boolean) $boolean; } /** * Sets whether to automatically exit after a command execution or not. * * @param Boolean $boolean Whether to automatically exit after a command execution or not * * @api */ public function setAutoExit($boolean) { $this->autoExit = (Boolean) $boolean; } /** * Gets the name of the application. * * @return string The application name * * @api */ public function getName() { return $this->name; } /** * Sets the application name. * * @param string $name The application name * * @api */ public function setName($name) { $this->name = $name; } /** * Gets the application version. * * @return string The application version * * @api */ public function getVersion() { return $this->version; } /** * Sets the application version. * * @param string $version The application version * * @api */ public function setVersion($version) { $this->version = $version; } /** * Returns the long version of the application. * * @return string The long application version * * @api */ public function getLongVersion() { if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { return sprintf('%s version %s', $this->getName(), $this->getVersion()); } return 'Console Tool'; } /** * Registers a new command. * * @param string $name The command name * * @return Command The newly created command * * @api */ public function register($name) { return $this->add(new Command($name)); } /** * Adds an array of command objects. * * @param Command[] $commands An array of commands * * @api */ public function addCommands(array $commands) { foreach ($commands as $command) { $this->add($command); } } /** * Adds a command object. * * If a command with the same name already exists, it will be overridden. * * @param Command $command A Command object * * @return Command The registered command * * @api */ public function add(Command $command) { $command->setApplication($this); if (!$command->isEnabled()) { $command->setApplication(null); return; } $this->commands[$command->getName()] = $command; foreach ($command->getAliases() as $alias) { $this->commands[$alias] = $command; } return $command; } /** * Returns a registered command by name or alias. * * @param string $name The command name or alias * * @return Command A Command object * * @throws \InvalidArgumentException When command name given does not exist * * @api */ public function get($name) { if (!isset($this->commands[$name])) { throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); } $command = $this->commands[$name]; if ($this->wantHelps) { $this->wantHelps = false; $helpCommand = $this->get('help'); $helpCommand->setCommand($command); return $helpCommand; } return $command; } /** * Returns true if the command exists, false otherwise. * * @param string $name The command name or alias * * @return Boolean true if the command exists, false otherwise * * @api */ public function has($name) { return isset($this->commands[$name]); } /** * Returns an array of all unique namespaces used by currently registered commands. * * It does not returns the global namespace which always exists. * * @return array An array of namespaces */ public function getNamespaces() { $namespaces = array(); foreach ($this->commands as $command) { $namespaces[] = $this->extractNamespace($command->getName()); foreach ($command->getAliases() as $alias) { $namespaces[] = $this->extractNamespace($alias); } } return array_values(array_unique(array_filter($namespaces))); } /** * Finds a registered namespace by a name or an abbreviation. * * @param string $namespace A namespace or abbreviation to search for * * @return string A registered namespace * * @throws \InvalidArgumentException When namespace is incorrect or ambiguous */ public function findNamespace($namespace) { $allNamespaces = array(); foreach ($this->getNamespaces() as $n) { $allNamespaces[$n] = explode(':', $n); } $found = array(); foreach (explode(':', $namespace) as $i => $part) { $abbrevs = static::getAbbreviations(array_unique(array_values(array_filter(array_map(function ($p) use ($i) { return isset($p[$i]) ? $p[$i] : ''; }, $allNamespaces))))); if (!isset($abbrevs[$part])) { $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); if (1 <= $i) { $part = implode(':', $found).':'.$part; } if ($alternatives = $this->findAlternativeNamespace($part, $abbrevs)) { if (1 == count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { $message .= "\n\nDid you mean one of these?\n "; } $message .= implode("\n ", $alternatives); } throw new \InvalidArgumentException($message); } if (count($abbrevs[$part]) > 1) { throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($abbrevs[$part]))); } $found[] = $abbrevs[$part][0]; } return implode(':', $found); } /** * Finds a command by name or alias. * * Contrary to get, this command tries to find the best * match if you give it an abbreviation of a name or alias. * * @param string $name A command name or a command alias * * @return Command A Command instance * * @throws \InvalidArgumentException When command name is incorrect or ambiguous * * @api */ public function find($name) { // namespace $namespace = ''; $searchName = $name; if (false !== $pos = strrpos($name, ':')) { $namespace = $this->findNamespace(substr($name, 0, $pos)); $searchName = $namespace.substr($name, $pos); } // name $commands = array(); foreach ($this->commands as $command) { $extractedNamespace = $this->extractNamespace($command->getName()); if ($extractedNamespace === $namespace || !empty($namespace) && 0 === strpos($extractedNamespace, $namespace) ) { $commands[] = $command->getName(); } } $abbrevs = static::getAbbreviations(array_unique($commands)); if (isset($abbrevs[$searchName]) && 1 == count($abbrevs[$searchName])) { return $this->get($abbrevs[$searchName][0]); } if (isset($abbrevs[$searchName]) && count($abbrevs[$searchName]) > 1) { $suggestions = $this->getAbbreviationSuggestions($abbrevs[$searchName]); throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); } // aliases $aliases = array(); foreach ($this->commands as $command) { foreach ($command->getAliases() as $alias) { $extractedNamespace = $this->extractNamespace($alias); if ($extractedNamespace === $namespace || !empty($namespace) && 0 === strpos($extractedNamespace, $namespace) ) { $aliases[] = $alias; } } } $aliases = static::getAbbreviations(array_unique($aliases)); if (!isset($aliases[$searchName])) { $message = sprintf('Command "%s" is not defined.', $name); if ($alternatives = $this->findAlternativeCommands($searchName, $abbrevs)) { if (1 == count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { $message .= "\n\nDid you mean one of these?\n "; } $message .= implode("\n ", $alternatives); } throw new \InvalidArgumentException($message); } if (count($aliases[$searchName]) > 1) { throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $this->getAbbreviationSuggestions($aliases[$searchName]))); } return $this->get($aliases[$searchName][0]); } /** * Gets the commands (registered in the given namespace if provided). * * The array keys are the full names and the values the command instances. * * @param string $namespace A namespace name * * @return Command[] An array of Command instances * * @api */ public function all($namespace = null) { if (null === $namespace) { return $this->commands; } $commands = array(); foreach ($this->commands as $name => $command) { if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { $commands[$name] = $command; } } return $commands; } /** * Returns an array of possible abbreviations given a set of names. * * @param array $names An array of names * * @return array An array of abbreviations */ public static function getAbbreviations($names) { $abbrevs = array(); foreach ($names as $name) { for ($len = strlen($name) - 1; $len > 0; --$len) { $abbrev = substr($name, 0, $len); if (!isset($abbrevs[$abbrev])) { $abbrevs[$abbrev] = array($name); } else { $abbrevs[$abbrev][] = $name; } } } // Non-abbreviations always get entered, even if they aren't unique foreach ($names as $name) { $abbrevs[$name] = array($name); } return $abbrevs; } /** * Returns a text representation of the Application. * * @param string $namespace An optional namespace name * @param boolean $raw Whether to return raw command list * * @return string A string representing the Application */ public function asText($namespace = null, $raw = false) { $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands; $width = 0; foreach ($commands as $command) { $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; } $width += 2; if ($raw) { $messages = array(); foreach ($this->sortCommands($commands) as $space => $commands) { foreach ($commands as $name => $command) { $messages[] = sprintf("%-${width}s %s", $name, $command->getDescription()); } } return implode(PHP_EOL, $messages); } $messages = array($this->getHelp(), ''); if ($namespace) { $messages[] = sprintf("Available commands for the \"%s\" namespace:", $namespace); } else { $messages[] = 'Available commands:'; } // add commands by namespace foreach ($this->sortCommands($commands) as $space => $commands) { if (!$namespace && '_global' !== $space) { $messages[] = ''.$space.''; } foreach ($commands as $name => $command) { $messages[] = sprintf(" %-${width}s %s", $name, $command->getDescription()); } } return implode(PHP_EOL, $messages); } /** * Returns an XML representation of the Application. * * @param string $namespace An optional namespace name * @param Boolean $asDom Whether to return a DOM or an XML string * * @return string|DOMDocument An XML string representing the Application */ public function asXml($namespace = null, $asDom = false) { $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands; $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $dom->appendChild($xml = $dom->createElement('symfony')); $xml->appendChild($commandsXML = $dom->createElement('commands')); if ($namespace) { $commandsXML->setAttribute('namespace', $namespace); } else { $namespacesXML = $dom->createElement('namespaces'); $xml->appendChild($namespacesXML); } // add commands by namespace foreach ($this->sortCommands($commands) as $space => $commands) { if (!$namespace) { $namespaceArrayXML = $dom->createElement('namespace'); $namespacesXML->appendChild($namespaceArrayXML); $namespaceArrayXML->setAttribute('id', $space); } foreach ($commands as $name => $command) { if ($name !== $command->getName()) { continue; } if (!$namespace) { $commandXML = $dom->createElement('command'); $namespaceArrayXML->appendChild($commandXML); $commandXML->appendChild($dom->createTextNode($name)); } $node = $command->asXml(true)->getElementsByTagName('command')->item(0); $node = $dom->importNode($node, true); $commandsXML->appendChild($node); } } return $asDom ? $dom : $dom->saveXml(); } /** * Renders a caught exception. * * @param Exception $e An exception instance * @param OutputInterface $output An OutputInterface instance */ public function renderException($e, $output) { $strlen = function ($string) { if (!function_exists('mb_strlen')) { return strlen($string); } if (false === $encoding = mb_detect_encoding($string)) { return strlen($string); } return mb_strlen($string, $encoding); }; do { $title = sprintf(' [%s] ', get_class($e)); $len = $strlen($title); $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; $lines = array(); foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { foreach (str_split($line, $width - 4) as $line) { $lines[] = sprintf(' %s ', $line); $len = max($strlen($line) + 4, $len); } } $messages = array(str_repeat(' ', $len), $title.str_repeat(' ', max(0, $len - $strlen($title)))); foreach ($lines as $line) { $messages[] = $line.str_repeat(' ', $len - $strlen($line)); } $messages[] = str_repeat(' ', $len); $output->writeln(""); $output->writeln(""); foreach ($messages as $message) { $output->writeln(''.$message.''); } $output->writeln(""); $output->writeln(""); if (OutputInterface::VERBOSITY_VERBOSE === $output->getVerbosity()) { $output->writeln('Exception trace:'); // exception related properties $trace = $e->getTrace(); array_unshift($trace, array( 'function' => '', 'file' => $e->getFile() != null ? $e->getFile() : 'n/a', 'line' => $e->getLine() != null ? $e->getLine() : 'n/a', 'args' => array(), )); for ($i = 0, $count = count($trace); $i < $count; $i++) { $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; $function = $trace[$i]['function']; $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; $output->writeln(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line)); } $output->writeln(""); $output->writeln(""); } } while ($e = $e->getPrevious()); if (null !== $this->runningCommand) { $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName()))); $output->writeln(""); $output->writeln(""); } } /** * Tries to figure out the terminal width in which this application runs * * @return int|null */ protected function getTerminalWidth() { $dimensions = $this->getTerminalDimensions(); return $dimensions[0]; } /** * Tries to figure out the terminal height in which this application runs * * @return int|null */ protected function getTerminalHeight() { $dimensions = $this->getTerminalDimensions(); return $dimensions[1]; } /** * Tries to figure out the terminal dimensions based on the current environment * * @return array Array containing width and height */ public function getTerminalDimensions() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { // extract [w, H] from "wxh (WxH)" if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { return array((int) $matches[1], (int) $matches[2]); } // extract [w, h] from "wxh" if (preg_match('/^(\d+)x(\d+)$/', $this->getConsoleMode(), $matches)) { return array((int) $matches[1], (int) $matches[2]); } } if ($sttyString = $this->getSttyColumns()) { // extract [w, h] from "rows h; columns w;" if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { return array((int) $matches[2], (int) $matches[1]); } // extract [w, h] from "; h rows; w columns" if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { return array((int) $matches[2], (int) $matches[1]); } } return array(null, null); } /** * Gets the name of the command based on input. * * @param InputInterface $input The input interface * * @return string The command name */ protected function getCommandName(InputInterface $input) { return $input->getFirstArgument(); } /** * Gets the default input definition. * * @return InputDefinition An InputDefinition instance */ protected function getDefaultInputDefinition() { return new InputDefinition(array( new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message.'), new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Increase verbosity of messages.'), new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version.'), new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output.'), new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output.'), new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question.'), )); } /** * Gets the default commands that should always be available. * * @return Command[] An array of default Command instances */ protected function getDefaultCommands() { return array(new HelpCommand(), new ListCommand()); } /** * Gets the default helper set with the helpers that should always be available. * * @return HelperSet A HelperSet instance */ protected function getDefaultHelperSet() { return new HelperSet(array( new FormatterHelper(), new DialogHelper(), new ProgressHelper(), )); } /** * Runs and parses stty -a if it's available, suppressing any error output * * @return string */ private function getSttyColumns() { if (!function_exists('proc_open')) { return; } $descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); if (is_resource($process)) { $info = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); return $info; } } /** * Runs and parses mode CON if it's available, suppressing any error output * * @return string x or null if it could not be parsed */ private function getConsoleMode() { if (!function_exists('proc_open')) { return; } $descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); if (is_resource($process)) { $info = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { return $matches[2].'x'.$matches[1]; } } } /** * Sorts commands in alphabetical order. * * @param array $commands An associative array of commands to sort * * @return array A sorted array of commands */ private function sortCommands($commands) { $namespacedCommands = array(); foreach ($commands as $name => $command) { $key = $this->extractNamespace($name, 1); if (!$key) { $key = '_global'; } $namespacedCommands[$key][$name] = $command; } ksort($namespacedCommands); foreach ($namespacedCommands as &$commands) { ksort($commands); } return $namespacedCommands; } /** * Returns abbreviated suggestions in string format. * * @param array $abbrevs Abbreviated suggestions to convert * * @return string A formatted string of abbreviated suggestions */ private function getAbbreviationSuggestions($abbrevs) { return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); } /** * Returns the namespace part of the command name. * * @param string $name The full name of the command * @param string $limit The maximum number of parts of the namespace * * @return string The namespace of the command */ private function extractNamespace($name, $limit = null) { $parts = explode(':', $name); array_pop($parts); return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); } /** * Finds alternative commands of $name * * @param string $name The full name of the command * @param array $abbrevs The abbreviations * * @return array A sorted array of similar commands */ private function findAlternativeCommands($name, $abbrevs) { $callback = function($item) { return $item->getName(); }; return $this->findAlternatives($name, $this->commands, $abbrevs, $callback); } /** * Finds alternative namespace of $name * * @param string $name The full name of the namespace * @param array $abbrevs The abbreviations * * @return array A sorted array of similar namespace */ private function findAlternativeNamespace($name, $abbrevs) { return $this->findAlternatives($name, $this->getNamespaces(), $abbrevs); } /** * Finds alternative of $name among $collection, * if nothing is found in $collection, try in $abbrevs * * @param string $name The string * @param array|Traversable $collection The collection * @param array $abbrevs The abbreviations * @param Closure|string|array $callback The callable to transform collection item before comparison * * @return array A sorted array of similar string */ private function findAlternatives($name, $collection, $abbrevs, $callback = null) { $alternatives = array(); foreach ($collection as $item) { if (null !== $callback) { $item = call_user_func($callback, $item); } $lev = levenshtein($name, $item); if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { $alternatives[$item] = $lev; } } if (!$alternatives) { foreach ($abbrevs as $key => $values) { $lev = levenshtein($name, $key); if ($lev <= strlen($name) / 3 || false !== strpos($key, $name)) { foreach ($values as $value) { $alternatives[$value] = $lev; } } } } asort($alternatives); return array_keys($alternatives); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\HelperSet; /** * Base class for all commands. * * @author Fabien Potencier * * @api */ class Command { private $application; private $name; private $aliases; private $definition; private $help; private $description; private $ignoreValidationErrors; private $applicationDefinitionMerged; private $code; private $synopsis; private $helperSet; /** * Constructor. * * @param string $name The name of the command * * @throws \LogicException When the command name is empty * * @api */ public function __construct($name = null) { $this->definition = new InputDefinition(); $this->ignoreValidationErrors = false; $this->applicationDefinitionMerged = false; $this->aliases = array(); if (null !== $name) { $this->setName($name); } $this->configure(); if (!$this->name) { throw new \LogicException('The command name cannot be empty.'); } } /** * Ignores validation errors. * * This is mainly useful for the help command. */ public function ignoreValidationErrors() { $this->ignoreValidationErrors = true; } /** * Sets the application instance for this command. * * @param Application $application An Application instance * * @api */ public function setApplication(Application $application = null) { $this->application = $application; if ($application) { $this->setHelperSet($application->getHelperSet()); } else { $this->helperSet = null; } } /** * Sets the helper set. * * @param HelperSet $helperSet A HelperSet instance */ public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; } /** * Gets the helper set. * * @return HelperSet A HelperSet instance */ public function getHelperSet() { return $this->helperSet; } /** * Gets the application instance for this command. * * @return Application An Application instance * * @api */ public function getApplication() { return $this->application; } /** * Checks whether the command is enabled or not in the current environment * * Override this to check for x or y and return false if the command can not * run properly under the current conditions. * * @return Boolean */ public function isEnabled() { return true; } /** * Configures the current command. */ protected function configure() { } /** * Executes the current command. * * This method is not abstract because you can use this class * as a concrete class. In this case, instead of defining the * execute() method, you set the code to execute by passing * a Closure to the setCode() method. * * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance * * @return null|integer null or 0 if everything went fine, or an error code * * @throws \LogicException When this abstract method is not implemented * @see setCode() */ protected function execute(InputInterface $input, OutputInterface $output) { throw new \LogicException('You must override the execute() method in the concrete command class.'); } /** * Interacts with the user. * * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance */ protected function interact(InputInterface $input, OutputInterface $output) { } /** * Initializes the command just after the input has been validated. * * This is mainly useful when a lot of commands extends one main command * where some things need to be initialized based on the input arguments and options. * * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance */ protected function initialize(InputInterface $input, OutputInterface $output) { } /** * Runs the command. * * The code to execute is either defined directly with the * setCode() method or by overriding the execute() method * in a sub-class. * * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance * * @return integer The command exit code * * @throws \Exception * * @see setCode() * @see execute() * * @api */ public function run(InputInterface $input, OutputInterface $output) { // force the creation of the synopsis before the merge with the app definition $this->getSynopsis(); // add the application arguments and options $this->mergeApplicationDefinition(); // bind the input against the command specific arguments/options try { $input->bind($this->definition); } catch (\Exception $e) { if (!$this->ignoreValidationErrors) { throw $e; } } $this->initialize($input, $output); if ($input->isInteractive()) { $this->interact($input, $output); } $input->validate(); if ($this->code) { $statusCode = call_user_func($this->code, $input, $output); } else { $statusCode = $this->execute($input, $output); } return is_numeric($statusCode) ? $statusCode : 0; } /** * Sets the code to execute when running this command. * * If this method is used, it overrides the code defined * in the execute() method. * * @param callable $code A callable(InputInterface $input, OutputInterface $output) * * @return Command The current instance * * @throws \InvalidArgumentException * * @see execute() * * @api */ public function setCode($code) { if (!is_callable($code)) { throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.'); } $this->code = $code; return $this; } /** * Merges the application definition with the command definition. * * @param Boolean $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments */ private function mergeApplicationDefinition($mergeArgs = true) { if (null === $this->application || true === $this->applicationDefinitionMerged) { return; } if ($mergeArgs) { $currentArguments = $this->definition->getArguments(); $this->definition->setArguments($this->application->getDefinition()->getArguments()); $this->definition->addArguments($currentArguments); } $this->definition->addOptions($this->application->getDefinition()->getOptions()); $this->applicationDefinitionMerged = true; } /** * Sets an array of argument and option instances. * * @param array|InputDefinition $definition An array of argument and option instances or a definition instance * * @return Command The current instance * * @api */ public function setDefinition($definition) { if ($definition instanceof InputDefinition) { $this->definition = $definition; } else { $this->definition->setDefinition($definition); } $this->applicationDefinitionMerged = false; return $this; } /** * Gets the InputDefinition attached to this Command. * * @return InputDefinition An InputDefinition instance * * @api */ public function getDefinition() { return $this->definition; } /** * Gets the InputDefinition to be used to create XML and Text representations of this Command. * * Can be overridden to provide the original command representation when it would otherwise * be changed by merging with the application InputDefinition. * * @return InputDefinition An InputDefinition instance */ protected function getNativeDefinition() { return $this->getDefinition(); } /** * Adds an argument. * * @param string $name The argument name * @param integer $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL * @param string $description A description text * @param mixed $default The default value (for InputArgument::OPTIONAL mode only) * * @return Command The current instance * * @api */ public function addArgument($name, $mode = null, $description = '', $default = null) { $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); return $this; } /** * Adds an option. * * @param string $name The option name * @param string $shortcut The shortcut (can be null) * @param integer $mode The option mode: One of the InputOption::VALUE_* constants * @param string $description A description text * @param mixed $default The default value (must be null for InputOption::VALUE_REQUIRED or InputOption::VALUE_NONE) * * @return Command The current instance * * @api */ public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) { $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); return $this; } /** * Sets the name of the command. * * This method can set both the namespace and the name if * you separate them by a colon (:) * * $command->setName('foo:bar'); * * @param string $name The command name * * @return Command The current instance * * @throws \InvalidArgumentException When command name given is empty * * @api */ public function setName($name) { $this->validateName($name); $this->name = $name; return $this; } /** * Returns the command name. * * @return string The command name * * @api */ public function getName() { return $this->name; } /** * Sets the description for the command. * * @param string $description The description for the command * * @return Command The current instance * * @api */ public function setDescription($description) { $this->description = $description; return $this; } /** * Returns the description for the command. * * @return string The description for the command * * @api */ public function getDescription() { return $this->description; } /** * Sets the help for the command. * * @param string $help The help for the command * * @return Command The current instance * * @api */ public function setHelp($help) { $this->help = $help; return $this; } /** * Returns the help for the command. * * @return string The help for the command * * @api */ public function getHelp() { return $this->help; } /** * Returns the processed help for the command replacing the %command.name% and * %command.full_name% patterns with the real values dynamically. * * @return string The processed help for the command */ public function getProcessedHelp() { $name = $this->name; $placeholders = array( '%command.name%', '%command.full_name%' ); $replacements = array( $name, $_SERVER['PHP_SELF'].' '.$name ); return str_replace($placeholders, $replacements, $this->getHelp()); } /** * Sets the aliases for the command. * * @param array $aliases An array of aliases for the command * * @return Command The current instance * * @api */ public function setAliases($aliases) { foreach ($aliases as $alias) { $this->validateName($alias); } $this->aliases = $aliases; return $this; } /** * Returns the aliases for the command. * * @return array An array of aliases for the command * * @api */ public function getAliases() { return $this->aliases; } /** * Returns the synopsis for the command. * * @return string The synopsis */ public function getSynopsis() { if (null === $this->synopsis) { $this->synopsis = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis())); } return $this->synopsis; } /** * Gets a helper instance by name. * * @param string $name The helper name * * @return mixed The helper value * * @throws \InvalidArgumentException if the helper is not defined * * @api */ public function getHelper($name) { return $this->helperSet->get($name); } /** * Returns a text representation of the command. * * @return string A string representing the command */ public function asText() { if ($this->application && !$this->applicationDefinitionMerged) { $this->getSynopsis(); $this->mergeApplicationDefinition(false); } $messages = array( 'Usage:', ' '.$this->getSynopsis(), '', ); if ($this->getAliases()) { $messages[] = 'Aliases: '.implode(', ', $this->getAliases()).''; } $messages[] = $this->getNativeDefinition()->asText(); if ($help = $this->getProcessedHelp()) { $messages[] = 'Help:'; $messages[] = ' '.str_replace("\n", "\n ", $help)."\n"; } return implode("\n", $messages); } /** * Returns an XML representation of the command. * * @param Boolean $asDom Whether to return a DOM or an XML string * * @return string|DOMDocument An XML string representing the command */ public function asXml($asDom = false) { if ($this->application && !$this->applicationDefinitionMerged) { $this->getSynopsis(); $this->mergeApplicationDefinition(false); } $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $dom->appendChild($commandXML = $dom->createElement('command')); $commandXML->setAttribute('id', $this->name); $commandXML->setAttribute('name', $this->name); $commandXML->appendChild($usageXML = $dom->createElement('usage')); $usageXML->appendChild($dom->createTextNode(sprintf($this->getSynopsis(), ''))); $commandXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $this->getDescription()))); $commandXML->appendChild($helpXML = $dom->createElement('help')); $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $this->getProcessedHelp()))); $commandXML->appendChild($aliasesXML = $dom->createElement('aliases')); foreach ($this->getAliases() as $alias) { $aliasesXML->appendChild($aliasXML = $dom->createElement('alias')); $aliasXML->appendChild($dom->createTextNode($alias)); } $definition = $this->getNativeDefinition()->asXml(true); $commandXML->appendChild($dom->importNode($definition->getElementsByTagName('arguments')->item(0), true)); $commandXML->appendChild($dom->importNode($definition->getElementsByTagName('options')->item(0), true)); return $asDom ? $dom : $dom->saveXml(); } private function validateName($name) { if (!preg_match('/^[^\:]+(\:[^\:]+)*$/', $name)) { throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Command\Command; /** * HelpCommand displays the help for a given command. * * @author Fabien Potencier */ class HelpCommand extends Command { private $command; /** * {@inheritdoc} */ protected function configure() { $this->ignoreValidationErrors(); $this ->setName('help') ->setDefinition(array( new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'), )) ->setDescription('Displays help for a command') ->setHelp(<<%command.name% command displays help for a given command: php %command.full_name% list You can also output the help as XML by using the --xml option: php %command.full_name% --xml list To display the list of available commands, please use the list command. EOF ) ; } /** * Sets the command * * @param Command $command The command to set */ public function setCommand(Command $command) { $this->command = $command; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if (null === $this->command) { $this->command = $this->getApplication()->find($input->getArgument('command_name')); } if ($input->getOption('xml')) { $output->writeln($this->command->asXml(), OutputInterface::OUTPUT_RAW); } else { $output->writeln($this->command->asText()); } $this->command = null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputDefinition; /** * ListCommand displays the list of all available commands for the application. * * @author Fabien Potencier */ class ListCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('list') ->setDefinition($this->createDefinition()) ->setDescription('Lists commands') ->setHelp(<<%command.name% command lists all commands: php %command.full_name% You can also display the commands for a specific namespace: php %command.full_name% test You can also output the information as XML by using the --xml option: php %command.full_name% --xml It's also possible to get raw list of commands (useful for embedding command runner): php %command.full_name% --raw EOF ) ; } /** * {@inheritdoc} */ protected function getNativeDefinition() { return $this->createDefinition(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('xml')) { $output->writeln($this->getApplication()->asXml($input->getArgument('namespace')), OutputInterface::OUTPUT_RAW); } else { $output->writeln($this->getApplication()->asText($input->getArgument('namespace'), $input->getOption('raw'))); } } private function createDefinition() { return new InputDefinition(array( new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), )); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter class for console output. * * @author Konstantin Kudryashov * * @api */ class OutputFormatter implements OutputFormatterInterface { /** * The pattern to phrase the format. */ const FORMAT_PATTERN = '#(\\\\?)<(/?)([a-z][a-z0-9_=;-]+)?>((?: [^<\\\\]+ | (?!<(?:/?[a-z]|/>)). | .(?<=\\\\<) )*)#isx'; private $decorated; private $styles = array(); private $styleStack; /** * Escapes "<" special char in given text. * * @param string $text Text to escape * * @return string Escaped text */ public static function escape($text) { return preg_replace('/([^\\\\]?) FormatterStyle" instances * * @api */ public function __construct($decorated = null, array $styles = array()) { $this->decorated = (Boolean) $decorated; $this->setStyle('error', new OutputFormatterStyle('white', 'red')); $this->setStyle('info', new OutputFormatterStyle('green')); $this->setStyle('comment', new OutputFormatterStyle('yellow')); $this->setStyle('question', new OutputFormatterStyle('black', 'cyan')); foreach ($styles as $name => $style) { $this->setStyle($name, $style); } $this->styleStack = new OutputFormatterStyleStack(); } /** * Sets the decorated flag. * * @param Boolean $decorated Whether to decorate the messages or not * * @api */ public function setDecorated($decorated) { $this->decorated = (Boolean) $decorated; } /** * Gets the decorated flag. * * @return Boolean true if the output will decorate messages, false otherwise * * @api */ public function isDecorated() { return $this->decorated; } /** * Sets a new style. * * @param string $name The style name * @param OutputFormatterStyleInterface $style The style instance * * @api */ public function setStyle($name, OutputFormatterStyleInterface $style) { $this->styles[strtolower($name)] = $style; } /** * Checks if output formatter has style with specified name. * * @param string $name * * @return Boolean * * @api */ public function hasStyle($name) { return isset($this->styles[strtolower($name)]); } /** * Gets style options from style with specified name. * * @param string $name * * @return OutputFormatterStyleInterface * * @throws \InvalidArgumentException When style isn't defined * * @api */ public function getStyle($name) { if (!$this->hasStyle($name)) { throw new \InvalidArgumentException('Undefined style: '.$name); } return $this->styles[strtolower($name)]; } /** * Formats a message according to the given styles. * * @param string $message The message to style * * @return string The styled message * * @api */ public function format($message) { $message = preg_replace_callback(self::FORMAT_PATTERN, array($this, 'replaceStyle'), $message); return str_replace('\\<', '<', $message); } /** * @return OutputFormatterStyleStack */ public function getStyleStack() { return $this->styleStack; } /** * Replaces style of the output. * * @param array $match * * @return string The replaced style */ private function replaceStyle($match) { // we got "\<" escaped char if ('\\' === $match[1]) { return $this->applyCurrentStyle($match[0]); } if ('' === $match[3]) { if ('/' === $match[2]) { // we got "" tag $this->styleStack->pop(); return $this->applyCurrentStyle($match[4]); } // we got "<>" tag return '<>'.$this->applyCurrentStyle($match[4]); } if (isset($this->styles[strtolower($match[3])])) { $style = $this->styles[strtolower($match[3])]; } else { $style = $this->createStyleFromString($match[3]); if (false === $style) { return $this->applyCurrentStyle($match[0]); } } if ('/' === $match[2]) { $this->styleStack->pop($style); } else { $this->styleStack->push($style); } return $this->applyCurrentStyle($match[4]); } /** * Tries to create new style instance from string. * * @param string $string * * @return OutputFormatterStyle|Boolean false if string is not format string */ private function createStyleFromString($string) { if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { return false; } $style = new OutputFormatterStyle(); foreach ($matches as $match) { array_shift($match); if ('fg' == $match[0]) { $style->setForeground($match[1]); } elseif ('bg' == $match[0]) { $style->setBackground($match[1]); } else { $style->setOption($match[1]); } } return $style; } /** * Applies current style from stack to text, if must be applied. * * @param string $text Input text * * @return string string Styled text */ private function applyCurrentStyle($text) { return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter interface for console output. * * @author Konstantin Kudryashov * * @api */ interface OutputFormatterInterface { /** * Sets the decorated flag. * * @param Boolean $decorated Whether to decorate the messages or not * * @api */ public function setDecorated($decorated); /** * Gets the decorated flag. * * @return Boolean true if the output will decorate messages, false otherwise * * @api */ public function isDecorated(); /** * Sets a new style. * * @param string $name The style name * @param OutputFormatterStyleInterface $style The style instance * * @api */ public function setStyle($name, OutputFormatterStyleInterface $style); /** * Checks if output formatter has style with specified name. * * @param string $name * * @return Boolean * * @api */ public function hasStyle($name); /** * Gets style options from style with specified name. * * @param string $name * * @return OutputFormatterStyleInterface * * @api */ public function getStyle($name); /** * Formats a message according to the given styles. * * @param string $message The message to style * * @return string The styled message * * @api */ public function format($message); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter style class for defining styles. * * @author Konstantin Kudryashov * * @api */ class OutputFormatterStyle implements OutputFormatterStyleInterface { private static $availableForegroundColors = array( 'black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37 ); private static $availableBackgroundColors = array( 'black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47 ); private static $availableOptions = array( 'bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8 ); private $foreground; private $background; private $options = array(); /** * Initializes output formatter style. * * @param string $foreground The style foreground color name * @param string $background The style background color name * @param array $options The style options * * @api */ public function __construct($foreground = null, $background = null, array $options = array()) { if (null !== $foreground) { $this->setForeground($foreground); } if (null !== $background) { $this->setBackground($background); } if (count($options)) { $this->setOptions($options); } } /** * Sets style foreground color. * * @param string $color The color name * * @throws \InvalidArgumentException When the color name isn't defined * * @api */ public function setForeground($color = null) { if (null === $color) { $this->foreground = null; return; } if (!isset(static::$availableForegroundColors[$color])) { throw new \InvalidArgumentException(sprintf( 'Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors)) )); } $this->foreground = static::$availableForegroundColors[$color]; } /** * Sets style background color. * * @param string $color The color name * * @throws \InvalidArgumentException When the color name isn't defined * * @api */ public function setBackground($color = null) { if (null === $color) { $this->background = null; return; } if (!isset(static::$availableBackgroundColors[$color])) { throw new \InvalidArgumentException(sprintf( 'Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors)) )); } $this->background = static::$availableBackgroundColors[$color]; } /** * Sets some specific style option. * * @param string $option The option name * * @throws \InvalidArgumentException When the option name isn't defined * * @api */ public function setOption($option) { if (!isset(static::$availableOptions[$option])) { throw new \InvalidArgumentException(sprintf( 'Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)) )); } if (false === array_search(static::$availableOptions[$option], $this->options)) { $this->options[] = static::$availableOptions[$option]; } } /** * Unsets some specific style option. * * @param string $option The option name * * @throws \InvalidArgumentException When the option name isn't defined * */ public function unsetOption($option) { if (!isset(static::$availableOptions[$option])) { throw new \InvalidArgumentException(sprintf( 'Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)) )); } $pos = array_search(static::$availableOptions[$option], $this->options); if (false !== $pos) { unset($this->options[$pos]); } } /** * Sets multiple style options at once. * * @param array $options */ public function setOptions(array $options) { $this->options = array(); foreach ($options as $option) { $this->setOption($option); } } /** * Applies the style to a given text. * * @param string $text The text to style * * @return string */ public function apply($text) { $codes = array(); if (null !== $this->foreground) { $codes[] = $this->foreground; } if (null !== $this->background) { $codes[] = $this->background; } if (count($this->options)) { $codes = array_merge($codes, $this->options); } if (0 === count($codes)) { return $text; } return sprintf("\033[%sm%s\033[0m", implode(';', $codes), $text); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter style interface for defining styles. * * @author Konstantin Kudryashov * * @api */ interface OutputFormatterStyleInterface { /** * Sets style foreground color. * * @param string $color The color name * * @api */ public function setForeground($color = null); /** * Sets style background color. * * @param string $color The color name * * @api */ public function setBackground($color = null); /** * Sets some specific style option. * * @param string $option The option name * * @api */ public function setOption($option); /** * Unsets some specific style option. * * @param string $option The option name */ public function unsetOption($option); /** * Sets multiple style options at once. * * @param array $options */ public function setOptions(array $options); /** * Applies the style to a given text. * * @param string $text The text to style * * @return string */ public function apply($text); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * @author Jean-François Simon */ class OutputFormatterStyleStack { /** * @var OutputFormatterStyleInterface[] */ private $styles; /** * @var OutputFormatterStyleInterface */ private $emptyStyle; /** * Constructor. * * @param OutputFormatterStyleInterface $emptyStyle */ public function __construct(OutputFormatterStyleInterface $emptyStyle = null) { $this->emptyStyle = $emptyStyle ?: new OutputFormatterStyle(); $this->reset(); } /** * Resets stack (ie. empty internal arrays). */ public function reset() { $this->styles = array(); } /** * Pushes a style in the stack. * * @param OutputFormatterStyleInterface $style */ public function push(OutputFormatterStyleInterface $style) { $this->styles[] = $style; } /** * Pops a style from the stack. * * @param OutputFormatterStyleInterface $style * * @return OutputFormatterStyleInterface * * @throws \InvalidArgumentException When style tags incorrectly nested */ public function pop(OutputFormatterStyleInterface $style = null) { if (empty($this->styles)) { return $this->emptyStyle; } if (null === $style) { return array_pop($this->styles); } foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { if ($style->apply('') === $stackedStyle->apply('')) { $this->styles = array_slice($this->styles, 0, $index); return $stackedStyle; } } throw new \InvalidArgumentException('Incorrectly nested style tag found.'); } /** * Computes current style with stacks top codes. * * @return OutputFormatterStyle */ public function getCurrent() { if (empty($this->styles)) { return $this->emptyStyle; } return $this->styles[count($this->styles)-1]; } /** * @param OutputFormatterStyleInterface $emptyStyle * * @return OutputFormatterStyleStack */ public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle) { $this->emptyStyle = $emptyStyle; return $this; } /** * @return OutputFormatterStyleInterface */ public function getEmptyStyle() { return $this->emptyStyle; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatterStyle; /** * The Dialog class provides helpers to interact with the user. * * @author Fabien Potencier */ class DialogHelper extends Helper { private $inputStream; private static $shell; private static $stty; /** * Asks the user to select a value. * * @param OutputInterface $output An Output instance * @param string|array $question The question to ask * @param array $choices List of choices to pick from * @param Boolean $default The default answer if the user enters nothing * @param Boolean|integer $attempts Max number of times to ask before giving up (false by default, which means infinite) * @param string $errorMessage Message which will be shown if invalid value from choice list would be picked * * @return integer|string The selected value (the key of the choices array) * * @throws \InvalidArgumentException */ public function select(OutputInterface $output, $question, $choices, $default = null, $attempts = false, $errorMessage = 'Value "%s" is invalid') { $width = max(array_map('strlen', array_keys($choices))); $messages = (array) $question; foreach ($choices as $key => $value) { $messages[] = sprintf(" [%-${width}s] %s", $key, $value); } $output->writeln($messages); $result = $this->askAndValidate($output, '> ', function ($picked) use ($choices, $errorMessage) { if (empty($choices[$picked])) { throw new \InvalidArgumentException(sprintf($errorMessage, $picked)); } return $picked; }, $attempts, $default); return $result; } /** * Asks a question to the user. * * @param OutputInterface $output An Output instance * @param string|array $question The question to ask * @param string $default The default answer if none is given by the user * @param array $autocomplete List of values to autocomplete * * @return string The user answer * * @throws \RuntimeException If there is no data to read in the input stream */ public function ask(OutputInterface $output, $question, $default = null, array $autocomplete = null) { $output->write($question); $inputStream = $this->inputStream ?: STDIN; if (null === $autocomplete || !$this->hasSttyAvailable()) { $ret = fgets($inputStream, 4096); if (false === $ret) { throw new \RuntimeException('Aborted'); } $ret = trim($ret); } else { $ret = ''; $i = 0; $ofs = -1; $matches = $autocomplete; $numMatches = count($matches); $sttyMode = shell_exec('stty -g'); // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) shell_exec('stty -icanon -echo'); // Add highlighted text style $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); // Read a keypress while ($c = fread($inputStream, 1)) { // Backspace Character if ("\177" === $c) { if (0 === $numMatches && 0 !== $i) { $i--; // Move cursor backwards $output->write("\033[1D"); } if ($i === 0) { $ofs = -1; $matches = $autocomplete; $numMatches = count($matches); } else { $numMatches = 0; } // Pop the last character off the end of our string $ret = substr($ret, 0, $i); } elseif ("\033" === $c) { // Did we read an escape sequence? $c .= fread($inputStream, 2); // A = Up Arrow. B = Down Arrow if ('A' === $c[2] || 'B' === $c[2]) { if ('A' === $c[2] && -1 === $ofs) {